From 4d5ced1ce6a4652c673098fc16990caf58c6a912 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 2 Nov 2024 18:13:38 +0100 Subject: [PATCH 01/81] maple: add Full Controller with 2 analog sticks and all buttons add AllStatusReq string for purupuru device. Issue #1171 --- core/hw/maple/maple_cfg.cpp | 3 +- core/hw/maple/maple_cfg.h | 1 + core/hw/maple/maple_devs.cpp | 141 +++++++++--------- core/lua/lua.cpp | 6 + core/serialize.h | 3 +- core/ui/gui.cpp | 8 +- .../emulator/FlycastViewController.mm | 2 + shell/libretro/libretro.cpp | 61 ++++++-- 8 files changed, 141 insertions(+), 84 deletions(-) diff --git a/core/hw/maple/maple_cfg.cpp b/core/hw/maple/maple_cfg.cpp index b006a9b20..9ce4199de 100644 --- a/core/hw/maple/maple_cfg.cpp +++ b/core/hw/maple/maple_cfg.cpp @@ -319,7 +319,8 @@ static void createDreamcastDevices() switch (config::MapleMainDevices[bus]) { case MDT_SegaController: - mcfg_Create(MDT_SegaController, bus, 5); + case MDT_SegaControllerXL: + mcfg_Create(config::MapleMainDevices[bus], bus, 5); if (config::MapleExpansionDevices[bus][0] != MDT_None) mcfg_Create(config::MapleExpansionDevices[bus][0], bus, 0); if (config::MapleExpansionDevices[bus][1] != MDT_None) diff --git a/core/hw/maple/maple_cfg.h b/core/hw/maple/maple_cfg.h index 2332c7fcf..404d7e86f 100644 --- a/core/hw/maple/maple_cfg.h +++ b/core/hw/maple/maple_cfg.h @@ -22,6 +22,7 @@ enum MapleDeviceType MDT_RacingController = 15, MDT_DenshaDeGoController = 16, MDT_Dreameye = 17, + MDT_SegaControllerXL = 18, MDT_Count }; diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index 70332305e..9b1c5c3e3 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -978,7 +978,15 @@ struct maple_sega_purupuru : maple_base //2 w16(0x0640); // 160 mA - return cmd == MDC_DeviceRequest ? MDRS_DeviceStatus : MDRS_DeviceStatusAll; + if (cmd == MDC_AllStatusReq) + { + const char *extra = "Version 1.000,1998/11/10,315-6211-AH ,Vibration Motor:1,Fm:4 - 30Hz,Pow:7 "; + wptr(extra, strlen(extra)); + return MDRS_DeviceStatusAll; + } + else { + return MDRS_DeviceStatus; + } //get last vibration case MDCF_GetCondition: @@ -1598,6 +1606,51 @@ struct maple_densha_controller: maple_sega_controller } }; +struct FullController : maple_sega_controller +{ + u32 get_capabilities() override + { + // byte 0: 0 0 0 0 0 0 0 0 + // byte 1: 0 0 a5 a4 a3 a2 a1 a0 + // byte 2: R2 L2 D2 U2 D X Y Z + // byte 3: R L D U St A B C + return 0xffff3f00; // 6 axes, all buttons + } + + u16 getButtonState(const PlainJoystickState &pjs) override + { + u32 kcode = pjs.kcode; + mutualExclusion(kcode, DC_DPAD_UP | DC_DPAD_DOWN); + mutualExclusion(kcode, DC_DPAD_LEFT | DC_DPAD_RIGHT); + mutualExclusion(kcode, DC_DPAD2_UP | DC_DPAD2_DOWN); + mutualExclusion(kcode, DC_DPAD2_LEFT | DC_DPAD2_RIGHT); + return kcode; + } + + u32 getAnalogAxis(int index, const PlainJoystickState &pjs) override + { + if (index == 4 || index == 5) + { + // Limit the magnitude of the analog axes to 128 + s8 xaxis = pjs.joy[PJAI_X2] - 128; + s8 yaxis = pjs.joy[PJAI_Y2] - 128; + limit_joystick_magnitude<128>(xaxis, yaxis); + if (index == 4) + return xaxis + 128; + else + return yaxis + 128; + } + return maple_sega_controller::getAnalogAxis(index, pjs); + } + + const char *get_device_name() override { + return "Dreamcast Controller XL"; + } + + MapleDeviceType get_device_type() override { + return MDT_SegaControllerXL; + } +}; // Emulates a 838-14245-92 maple to RS232 converter // wired to a 838-14243 RFID reader/writer (apparently Saxa HW210) @@ -1981,84 +2034,38 @@ const u8 *getRfidCardData(int playerNum) maple_device* maple_Create(MapleDeviceType type) { - maple_device* rv=0; switch(type) { case MDT_SegaController: if (!settings.platform.isAtomiswave()) - rv = new maple_sega_controller(); + return new maple_sega_controller(); else - rv = new maple_atomiswave_controller(); - break; - - case MDT_Microphone: - rv=new maple_microphone(); - break; - - case MDT_SegaVMU: - rv = new maple_sega_vmu(); - break; - - case MDT_PurupuruPack: - rv = new maple_sega_purupuru(); - break; - - case MDT_Keyboard: - rv = new maple_keyboard(); - break; - - case MDT_Mouse: - rv = new maple_mouse(); - break; - + return new maple_atomiswave_controller(); + case MDT_Microphone: return new maple_microphone(); + case MDT_SegaVMU: return new maple_sega_vmu(); + case MDT_PurupuruPack: return new maple_sega_purupuru(); + case MDT_Keyboard: return new maple_keyboard(); + case MDT_Mouse: return new maple_mouse(); case MDT_LightGun: if (!settings.platform.isAtomiswave()) - rv = new maple_lightgun(); + return new maple_lightgun(); else - rv = new atomiswave_lightgun(); - break; - - case MDT_NaomiJamma: - rv = new maple_naomi_jamma(); - break; - - case MDT_TwinStick: - rv = new maple_sega_twinstick(); - break; - - case MDT_AsciiStick: - rv = new maple_ascii_stick(); - break; - - case MDT_MaracasController: - rv = new maple_maracas_controller(); - break; - - case MDT_FishingController: - rv = new maple_fishing_controller(); - break; - - case MDT_PopnMusicController: - rv = new maple_popnmusic_controller(); - break; - - case MDT_RacingController: - rv = new maple_racing_controller(); - break; - - case MDT_DenshaDeGoController: - rv = new maple_densha_controller(); - break; - - case MDT_RFIDReaderWriter: - rv = new RFIDReaderWriter(); - break; + return new atomiswave_lightgun(); + case MDT_NaomiJamma: return new maple_naomi_jamma(); + case MDT_TwinStick: return new maple_sega_twinstick(); + case MDT_AsciiStick: return new maple_ascii_stick(); + case MDT_MaracasController: return new maple_maracas_controller(); + case MDT_FishingController: return new maple_fishing_controller(); + case MDT_PopnMusicController: return new maple_popnmusic_controller(); + case MDT_RacingController: return new maple_racing_controller(); + case MDT_DenshaDeGoController: return new maple_densha_controller(); + case MDT_SegaControllerXL: return new FullController(); + case MDT_RFIDReaderWriter: return new RFIDReaderWriter(); default: ERROR_LOG(MAPLE, "Invalid device type %d", type); die("Invalid maple device type"); break; } - - return rv; + return nullptr; } diff --git a/core/lua/lua.cpp b/core/lua/lua.cpp index 194eb1542..e5512a043 100644 --- a/core/lua/lua.cpp +++ b/core/lua/lua.cpp @@ -229,6 +229,12 @@ static void setMapleType(int bus, int type, lua_State *L) case MDT_Mouse: case MDT_LightGun: case MDT_TwinStick: + case MDT_MaracasController: + case MDT_FishingController: + case MDT_PopnMusicController: + case MDT_RacingController: + case MDT_DenshaDeGoController: + case MDT_SegaControllerXL: case MDT_None: config::MapleMainDevices[bus - 1] = (MapleDeviceType)type; maple_ReconnectDevices(); diff --git a/core/serialize.h b/core/serialize.h index cc397c68c..e367f8fce 100644 --- a/core/serialize.h +++ b/core/serialize.h @@ -62,7 +62,8 @@ class SerializeBase V49, V50, V51, - Current = V51, + V52, + Current = V52, Next = Current + 1, }; diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index b1fc5c97a..5381fdf9c 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -811,6 +811,7 @@ const char *maple_device_types[] = "Pop'n Music controller", "Racing Controller", "Densha de Go! Controller", + "Full Controller", // "Dreameye", }; @@ -848,8 +849,10 @@ static const char *maple_device_name(MapleDeviceType type) return maple_device_types[10]; case MDT_DenshaDeGoController: return maple_device_types[11]; + case MDT_SegaControllerXL: + return maple_device_types[12]; case MDT_Dreameye: -// return maple_device_types[12]; +// return maple_device_types[13]; case MDT_None: default: return maple_device_types[0]; @@ -883,6 +886,8 @@ static MapleDeviceType maple_device_type_from_index(int idx) case 11: return MDT_DenshaDeGoController; case 12: + return MDT_SegaControllerXL; + case 13: return MDT_Dreameye; case 0: default: @@ -2022,6 +2027,7 @@ static void gui_settings_controls(bool& maple_devices_changed) int port_count = 0; switch (config::MapleMainDevices[bus]) { case MDT_SegaController: + case MDT_SegaControllerXL: port_count = 2; break; case MDT_LightGun: diff --git a/shell/apple/emulator-ios/emulator/FlycastViewController.mm b/shell/apple/emulator-ios/emulator/FlycastViewController.mm index fb437bead..dc76c26b1 100644 --- a/shell/apple/emulator-ios/emulator/FlycastViewController.mm +++ b/shell/apple/emulator-ios/emulator/FlycastViewController.mm @@ -92,6 +92,7 @@ static void updateAudioSession(Event event, void *) switch (config::MapleMainDevices[bus]) { case MDT_SegaController: + case MDT_SegaControllerXL: for (int port = 0; port < 2; port++) if (config::MapleExpansionDevices[bus][port] == MDT_Microphone) hasMicrophone = true; @@ -99,6 +100,7 @@ static void updateAudioSession(Event event, void *) case MDT_LightGun: case MDT_AsciiStick: case MDT_TwinStick: + case MDT_RacingController: if (config::MapleExpansionDevices[bus][0] == MDT_Microphone) hasMicrophone = true; break; diff --git a/shell/libretro/libretro.cpp b/shell/libretro/libretro.cpp index ca2c8209f..9b4122f7e 100644 --- a/shell/libretro/libretro.cpp +++ b/shell/libretro/libretro.cpp @@ -76,6 +76,7 @@ constexpr char slash = path_default_slash_c(); #define RETRO_DEVICE_POPNMUSIC RETRO_DEVICE_SUBCLASS( RETRO_DEVICE_JOYPAD, 6 ) #define RETRO_DEVICE_RACING RETRO_DEVICE_SUBCLASS( RETRO_DEVICE_JOYPAD, 7 ) #define RETRO_DEVICE_DENSHA RETRO_DEVICE_SUBCLASS( RETRO_DEVICE_JOYPAD, 8 ) +#define RETRO_DEVICE_FULL_CONTROLLER RETRO_DEVICE_SUBCLASS( RETRO_DEVICE_JOYPAD, 9 ) #define RETRO_ENVIRONMENT_RETROARCH_START_BLOCK 0x800000 @@ -294,13 +295,13 @@ void retro_set_environment(retro_environment_t cb) { "Pop'n Music", RETRO_DEVICE_POPNMUSIC }, { "Race Controller", RETRO_DEVICE_RACING }, { "Densha de Go!", RETRO_DEVICE_DENSHA }, - { 0 }, + { "Full Controller", RETRO_DEVICE_FULL_CONTROLLER }, }; static const struct retro_controller_info ports[] = { - { ports_default, 13 }, - { ports_default, 13 }, - { ports_default, 13 }, - { ports_default, 13 }, + { ports_default, std::size(ports_default) }, + { ports_default, std::size(ports_default) }, + { ports_default, std::size(ports_default) }, + { ports_default, std::size(ports_default) }, { 0 }, }; environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); @@ -573,13 +574,15 @@ static bool set_variable_visibility(void) || config::MapleMainDevices[i] == MDT_LightGun || config::MapleMainDevices[i] == MDT_TwinStick || config::MapleMainDevices[i] == MDT_AsciiStick - || config::MapleMainDevices[i] == MDT_RacingController); + || config::MapleMainDevices[i] == MDT_RacingController + || config::MapleMainDevices[i] == MDT_SegaControllerXL); snprintf(key, sizeof(key), CORE_OPTION_NAME "_device_port%d_slot1", i + 1); environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display); - // Only the regular controller has 2 expansion slots - option_display.visible = platformIsDreamcast && config::MapleMainDevices[i] == MDT_SegaController; + // Only the regular controller (and the XL version) has 2 expansion slots + option_display.visible = platformIsDreamcast + && (config::MapleMainDevices[i] == MDT_SegaController || config::MapleMainDevices[i] == MDT_SegaControllerXL); snprintf(key, sizeof(key), CORE_OPTION_NAME "_device_port%d_slot2", i + 1); environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display); @@ -946,12 +949,13 @@ static void update_variables(bool first_startup) || config::MapleMainDevices[i] == MDT_LightGun || config::MapleMainDevices[i] == MDT_TwinStick || config::MapleMainDevices[i] == MDT_AsciiStick - || config::MapleMainDevices[i] == MDT_RacingController) + || config::MapleMainDevices[i] == MDT_RacingController + || config::MapleMainDevices[i] == MDT_SegaControllerXL) { for (int slot = 0; slot < 2; slot++) { // Only regular controller has a 2nd slot - if (slot == 1 && config::MapleMainDevices[i] != MDT_SegaController) + if (slot == 1 && config::MapleMainDevices[i] != MDT_SegaController && config::MapleMainDevices[i] != MDT_SegaControllerXL) { config::MapleExpansionDevices[i][1] = MDT_None; continue; @@ -1321,7 +1325,7 @@ static uint32_t map_gamepad_button(unsigned device, unsigned id) { /* JOYPAD_B */ DC_BTN_A, /* JOYPAD_Y */ DC_BTN_X, - /* JOYPAD_SELECT */ 0, + /* JOYPAD_SELECT */ DC_BTN_D, /* JOYPAD_START */ DC_BTN_START, /* JOYPAD_UP */ DC_DPAD_UP, /* JOYPAD_DOWN */ DC_DPAD_DOWN, @@ -1329,6 +1333,8 @@ static uint32_t map_gamepad_button(unsigned device, unsigned id) /* JOYPAD_RIGHT */ DC_DPAD_RIGHT, /* JOYPAD_A */ DC_BTN_B, /* JOYPAD_X */ DC_BTN_Y, + /* JOYPAD_L */ DC_BTN_C, + /* JOYPAD_R */ DC_BTN_Z, }; static const uint32_t dc_lg_joymap[] = @@ -1805,6 +1811,28 @@ static void set_input_descriptors() desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }; break; + case MDT_SegaControllerXL: + // No DPad2 + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "D-Pad Left" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "D-Pad Up" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "D-Pad Down" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "D-Pad Right" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "A" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "B" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X, "Y" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y, "X" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L, "C" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R, "Z" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT,"D" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L2, "L Trigger" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2, "R Trigger" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X, "Analog X" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y, "Analog Y" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X, "R. Analog X" }; + desc[descriptor_index++] = { i, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y, "R. Analog Y" }; + break; + default: break; } @@ -2444,6 +2472,9 @@ void retro_set_controller_port_device(unsigned in_port, unsigned device) case RETRO_DEVICE_DENSHA: config::MapleMainDevices[in_port] = MDT_DenshaDeGoController; break; + case RETRO_DEVICE_FULL_CONTROLLER: + config::MapleMainDevices[in_port] = MDT_SegaControllerXL; + break; default: config::MapleMainDevices[in_port] = MDT_None; break; @@ -2974,15 +3005,17 @@ static void UpdateInputState(u32 port) switch (config::MapleMainDevices[port]) { case MDT_SegaController: + case MDT_SegaControllerXL: { int16_t ret = getBitmask(port, RETRO_DEVICE_JOYPAD); // -- buttons - for (int id = RETRO_DEVICE_ID_JOYPAD_B; id <= RETRO_DEVICE_ID_JOYPAD_X; ++id) + for (int id = RETRO_DEVICE_ID_JOYPAD_B; id <= RETRO_DEVICE_ID_JOYPAD_R; ++id) setDeviceButtonStateFromBitmap(ret, port, RETRO_DEVICE_JOYPAD, id); - // -- analog stick - get_analog_stick( input_cb, port, RETRO_DEVICE_INDEX_ANALOG_LEFT, &(joyx[port]), &(joyy[port]) ); + // -- analog sticks + get_analog_stick(input_cb, port, RETRO_DEVICE_INDEX_ANALOG_LEFT, &joyx[port], &joyy[port]); + get_analog_stick(input_cb, port, RETRO_DEVICE_INDEX_ANALOG_RIGHT, &joyrx[port], &joyry[port]); // -- triggers if ( digital_triggers ) From 96aac661336405a9c00a2d907fe041d7fb1a11d3 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 2 Nov 2024 18:46:49 +0100 Subject: [PATCH 02/81] naomi: improve performance of M1 and M4 carts dma Limit buffer size to 1 or 2 KB instead of 32 KB. Only decrypt right before dma transfer. Fixes frame drops and audio underruns in vf4evoct attract mode. Issue #1717 --- core/hw/naomi/m1cartridge.cpp | 3 ++- core/hw/naomi/m1cartridge.h | 4 ++-- core/hw/naomi/m4cartridge.cpp | 2 ++ core/hw/naomi/m4cartridge.h | 4 +++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/hw/naomi/m1cartridge.cpp b/core/hw/naomi/m1cartridge.cpp index bd82664ce..e32dd46d5 100644 --- a/core/hw/naomi/m1cartridge.cpp +++ b/core/hw/naomi/m1cartridge.cpp @@ -39,7 +39,6 @@ void M1Cartridge::AdvancePtr(u32 size) has_history = true; buffer_actual_size = 0; } - enc_fill(); } else NaomiCartridge::AdvancePtr(size); @@ -147,11 +146,13 @@ void M1Cartridge::Serialize(Serializer& ser) const void M1Cartridge::Deserialize(Deserializer& deser) { deser >> buffer; + deser.skip(32768 - sizeof(buffer), Deserializer::V52); deser >> dict; deser >> hist; deser >> avail_val; deser >> rom_cur_address; deser >> buffer_actual_size; + buffer_actual_size = std::min(buffer_actual_size, sizeof(buffer)); deser >> avail_bits; deser >> stream_ended; deser >> has_history; diff --git a/core/hw/naomi/m1cartridge.h b/core/hw/naomi/m1cartridge.h index f35497806..1ba034fce 100644 --- a/core/hw/naomi/m1cartridge.h +++ b/core/hw/naomi/m1cartridge.h @@ -26,6 +26,7 @@ class M1Cartridge : public NaomiCartridge { if (encryption) { + enc_fill(); size = std::min(size, (u32)sizeof(buffer)); return buffer; } @@ -48,7 +49,6 @@ class M1Cartridge : public NaomiCartridge //printf("M1 ENCRYPTION ON @ %08x\n", dma_offset); encryption = true; enc_reset(); - enc_fill(); } else encryption = false; @@ -100,7 +100,7 @@ class M1Cartridge : public NaomiCartridge u16 actel_id; - u8 buffer[32768]; + u8 buffer[1024]; u8 dict[111], hist[2]; u64 avail_val; u32 rom_cur_address, buffer_actual_size, avail_bits; diff --git a/core/hw/naomi/m4cartridge.cpp b/core/hw/naomi/m4cartridge.cpp index 20f86ef89..4e1068505 100644 --- a/core/hw/naomi/m4cartridge.cpp +++ b/core/hw/naomi/m4cartridge.cpp @@ -321,8 +321,10 @@ void M4Cartridge::Serialize(Serializer& ser) const void M4Cartridge::Deserialize(Deserializer& deser) { deser >> buffer; + deser.skip(32768 - sizeof(buffer), Deserializer::V52); deser >> rom_cur_address; deser >> buffer_actual_size; + buffer_actual_size = std::min(buffer_actual_size, sizeof(buffer)); deser >> iv; deser >> counter; deser >> encryption; diff --git a/core/hw/naomi/m4cartridge.h b/core/hw/naomi/m4cartridge.h index 7f21974a0..614f273c6 100644 --- a/core/hw/naomi/m4cartridge.h +++ b/core/hw/naomi/m4cartridge.h @@ -69,7 +69,7 @@ class M4Cartridge: public NaomiCartridge { u16 subkey2 = 0; u16 one_round[0x10000]; - u8 buffer[32768]; + u8 buffer[2048]; u32 rom_cur_address, buffer_actual_size; u16 iv; u8 counter; @@ -80,6 +80,8 @@ class M4Cartridge: public NaomiCartridge { void enc_init(); void enc_fill(); u16 decrypt_one_round(u16 word, u16 subkey); + + static_assert(sizeof(RomBootID) <= sizeof(buffer)); }; #endif /* CORE_HW_NAOMI_M4CARTRIDGE_H_ */ From 037dc3b4b9c048ed2b272f6e26cd41693d206251 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sun, 3 Nov 2024 16:55:15 +0100 Subject: [PATCH 03/81] atomiswave: set dma speed to 7 MB/s for all aw games and conversions AW dma transfer rate seems to be lower than naomi. Fixes Force Five freeze when starting a game. Fixes opening sequence animation being truncated in kofxi. Issue #1724 --- core/hw/naomi/naomi.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/hw/naomi/naomi.cpp b/core/hw/naomi/naomi.cpp index 797755570..20bad2613 100644 --- a/core/hw/naomi/naomi.cpp +++ b/core/hw/naomi/naomi.cpp @@ -180,8 +180,9 @@ static void Naomi_DmaStart(u32 addr, u32 data) void Naomi_setDmaDelay() { - if (settings.content.gameId == "FORCE FIVE") - // 7 MB/s + if (settings.platform.isAtomiswave() || settings.content.gameId == "FORCE FIVE" + || settings.content.gameId == "KENJU") + // 7 MB/s for Atomiwave games and conversions dmaXferDelay = 27; else dmaXferDelay = 10; From 5fc84acdfb2fbd7613f477f8df1859c09d4174fd Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sun, 3 Nov 2024 17:14:31 +0100 Subject: [PATCH 04/81] maple: base vmu file name on game ID for multidisk games Build vmu file name with game ID so that all disks of multidisk games share the same vmu (A1 only, or all with libretro when enabled). Rename existing vmu file to new format if none exists. Issue #1556 --- core/hw/maple/maple_devs.cpp | 72 ++++++++++++++++++++++++------------ core/oslib/oslib.cpp | 49 +++++++++++++++++++----- core/oslib/oslib.h | 2 +- shell/libretro/oslib.cpp | 42 ++++++++++++++------- 4 files changed, 118 insertions(+), 47 deletions(-) diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index 9b1c5c3e3..79a5d5306 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -5,6 +5,7 @@ #include "hw/pvr/spg.h" #include "audio/audiostream.h" #include "oslib/oslib.h" +#include "oslib/storage.h" #include "hw/aica/sgc_if.h" #include "cfg/option.h" #include @@ -361,10 +362,25 @@ struct maple_sega_vmu: maple_base break; } } + + bool fullSave() + { + if (file == nullptr) + return false; + if (std::fseek(file, 0, SEEK_SET) != 0) { + ERROR_LOG(MAPLE, "VMU %s: I/O error", logical_port); + return false; + } + if (std::fwrite(flash_data, sizeof(flash_data), 1, file) != 1) { + ERROR_LOG(MAPLE, "Failed to write the VMU %s to disk", logical_port); + return false; + } + return true; + } void initializeVmu() { - INFO_LOG(MAPLE, "Initialising empty VMU..."); + INFO_LOG(MAPLE, "Initialising empty VMU %s...", logical_port); uLongf dec_sz = sizeof(flash_data); int rv = uncompress(flash_data, &dec_sz, vmu_default, sizeof(vmu_default)); @@ -372,34 +388,44 @@ struct maple_sega_vmu: maple_base verify(rv == Z_OK); verify(dec_sz == sizeof(flash_data)); - if (file != nullptr) - { - if (std::fwrite(flash_data, sizeof(flash_data), 1, file) != 1) - WARN_LOG(MAPLE, "Failed to write the VMU to disk"); - if (std::fseek(file, 0, SEEK_SET) != 0) - WARN_LOG(MAPLE, "VMU: I/O error"); - } + fullSave(); } void OnSetup() override { memset(flash_data, 0, sizeof(flash_data)); memset(lcd_data, 0, sizeof(lcd_data)); - std::string apath = hostfs::getVmuPath(logical_port); - - file = nowide::fopen(apath.c_str(), "rb+"); - if (file == nullptr) - { - INFO_LOG(MAPLE, "Unable to open VMU save file \"%s\", creating new file", apath.c_str()); - file = nowide::fopen(apath.c_str(), "wb+"); - if (file == nullptr) - ERROR_LOG(MAPLE, "Failed to create VMU save file \"%s\"", apath.c_str()); - initializeVmu(); - } - - if (file != nullptr) - if (std::fread(flash_data, sizeof(flash_data), 1, file) != 1) - WARN_LOG(MAPLE, "Failed to read the VMU from disk"); + + // Load existing vmu file if found + std::string rpath = hostfs::getVmuPath(logical_port, false); + // this might be a storage url + FILE *rfile = hostfs::storage().openFile(rpath, "rb"); + if (rfile == nullptr) { + INFO_LOG(MAPLE, "Unable to open VMU file \"%s\", creating new file", rpath.c_str()); + } + else + { + if (std::fread(flash_data, sizeof(flash_data), 1, rfile) != 1) + WARN_LOG(MAPLE, "Failed to read the VMU file \"%s\" from disk", rpath.c_str()); + std::fclose(rfile); + } + // Open or create the vmu file to save to + std::string wpath = hostfs::getVmuPath(logical_port, true); + file = nowide::fopen(wpath.c_str(), "rb+"); + if (file == nullptr) + { + file = nowide::fopen(wpath.c_str(), "wb+"); + if (file == nullptr) { + ERROR_LOG(MAPLE, "Failed to create VMU save file \"%s\"", wpath.c_str()); + } + else if (rfile != nullptr) + { + // VMU file is being renamed so save it fully now + // and delete the old file + if (fullSave()) + nowide::remove(rpath.c_str()); + } + } u8 sum = 0; for (u32 i = 0; i < sizeof(flash_data); i++) diff --git a/core/oslib/oslib.cpp b/core/oslib/oslib.cpp index 05d33657d..fa7a3793e 100644 --- a/core/oslib/oslib.cpp +++ b/core/oslib/oslib.cpp @@ -44,18 +44,49 @@ namespace hostfs { -std::string getVmuPath(const std::string& port) +std::string getVmuPath(const std::string& port, bool save) { - if (port == "A1" && config::PerGameVmu && !settings.content.path.empty()) - return get_game_save_prefix() + "_vmu_save_A1.bin"; + if (port == "A1" && config::PerGameVmu) + { + if (settings.platform.isConsole() && !settings.content.gameId.empty()) + { + constexpr std::string_view INVALID_CHARS { " /\\:*?|<>" }; + std::string vmuName = settings.content.gameId; + for (char &c: vmuName) + if (INVALID_CHARS.find(c) != INVALID_CHARS.npos) + c = '_'; + vmuName += "_vmu_save_A1.bin"; + std::string wpath = get_writable_data_path(vmuName); + if (save || file_exists(wpath)) + return wpath; + std::string rpath = get_readonly_data_path(vmuName); + if (hostfs::storage().exists(rpath)) + return rpath; + if (!settings.content.path.empty()) + { + // Legacy path using the rom file name + rpath = get_game_save_prefix() + "_vmu_save_A1.bin"; + if (file_exists(rpath)) + return rpath; + } + return wpath; + } + if (!settings.content.path.empty()) + return get_game_save_prefix() + "_vmu_save_A1.bin"; + } - char tempy[512]; - sprintf(tempy, "vmu_save_%s.bin", port.c_str()); + std::string vmuName = "vmu_save_" + port + ".bin"; + std::string wpath = get_writable_data_path(vmuName); + if (save || file_exists(wpath)) + return wpath; + std::string rpath = get_readonly_data_path(vmuName); + if (hostfs::storage().exists(rpath)) + return rpath; // VMU saves used to be stored in .reicast, not in .reicast/data - std::string apath = get_writable_config_path(tempy); - if (!file_exists(apath)) - apath = get_writable_data_path(tempy); - return apath; + rpath = get_readonly_config_path(vmuName); + if (file_exists(rpath)) + return rpath; + return wpath; } std::string getArcadeFlashPath() diff --git a/core/oslib/oslib.h b/core/oslib/oslib.h index 916b33e98..7074c8346 100644 --- a/core/oslib/oslib.h +++ b/core/oslib/oslib.h @@ -47,7 +47,7 @@ u32 static inline bitscanrev(u32 v) namespace hostfs { - std::string getVmuPath(const std::string& port); + std::string getVmuPath(const std::string& port, bool save); std::string getArcadeFlashPath(); diff --git a/shell/libretro/oslib.cpp b/shell/libretro/oslib.cpp index 17c3957c9..736cd8315 100644 --- a/shell/libretro/oslib.cpp +++ b/shell/libretro/oslib.cpp @@ -34,21 +34,35 @@ extern std::string arcadeFlashPath; namespace hostfs { -std::string getVmuPath(const std::string& port) +std::string getVmuPath(const std::string& port, bool save) { - char filename[PATH_MAX + 8]; - - if ((per_content_vmus == 1 && port == "A1") - || per_content_vmus == 2) - { - sprintf(filename, "%s.%s.bin", content_name, port.c_str()); - return std::string(vmu_dir_no_slash) + std::string(path_default_slash()) + filename; - } - else - { - sprintf(filename, "vmu_save_%s.bin", port.c_str()); - return std::string(game_dir_no_slash) + std::string(path_default_slash()) + filename; - } + if ((per_content_vmus == 1 && port == "A1") + || per_content_vmus == 2) + { + std::string vmuDir = vmu_dir_no_slash + std::string(path_default_slash()); + if (settings.platform.isConsole() && !settings.content.gameId.empty()) + { + constexpr std::string_view INVALID_CHARS { " /\\:*?|<>" }; + std::string vmuName = settings.content.gameId; + for (char &c: vmuName) + if (INVALID_CHARS.find(c) != INVALID_CHARS.npos) + c = '_'; + vmuName += "." + port + ".bin"; + std::string wpath = vmuDir + vmuName; + if (save || file_exists(wpath.c_str())) + return wpath; + // Legacy path with rom name + std::string rpath = vmuDir + std::string(content_name) + "." + port + ".bin"; + if (file_exists(rpath.c_str())) + return rpath; + else + return wpath; + } + return vmuDir + std::string(content_name) + "." + port + ".bin"; + } + else { + return std::string(game_dir_no_slash) + std::string(path_default_slash()) + "vmu_save_" + port + ".bin"; + } } std::string getArcadeFlashPath() From fc6142b3329b9ad3a6a08c5b47b4f14d6a82a3e5 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 4 Nov 2024 19:19:31 +0100 Subject: [PATCH 05/81] maple: do a full save of the vmu after loading a state A partial save might corrupt the vmu file system. --- core/hw/maple/maple_devs.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index 79a5d5306..9e2eb242c 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -332,10 +332,11 @@ u8 vmu_default[] = { struct maple_sega_vmu: maple_base { - FILE* file; + FILE *file = nullptr; u8 flash_data[128_KB]; u8 lcd_data[192]; u8 lcd_data_decoded[48*32]; + bool fullSaveNeeded = false; MapleDeviceType get_device_type() override { @@ -361,6 +362,7 @@ struct maple_sega_vmu: maple_base config->SetImage(lcd_data_decoded); break; } + fullSaveNeeded = true; } bool fullSave() @@ -375,6 +377,7 @@ struct maple_sega_vmu: maple_base ERROR_LOG(MAPLE, "Failed to write the VMU %s to disk", logical_port); return false; } + fullSaveNeeded = false; return true; } @@ -434,6 +437,7 @@ struct maple_sega_vmu: maple_base if (sum == 0) // This means the existing VMU file is completely empty and needs to be recreated initializeVmu(); + fullSaveNeeded = false; } ~maple_sega_vmu() override @@ -659,17 +663,19 @@ struct maple_sega_vmu: maple_base if (file != nullptr) { - if (std::fseek(file, write_adr, SEEK_SET) != 0 + if (fullSaveNeeded) { + if (!fullSave()) + return MDRE_FileError; + } + else if (std::fseek(file, write_adr, SEEK_SET) != 0 || std::fwrite(&flash_data[write_adr], write_len, 1, file) != 1) { - WARN_LOG(MAPLE, "Failed to save VMU %s: I/O error", logical_port); + ERROR_LOG(MAPLE, "Failed to save VMU %s: I/O error", logical_port); return MDRE_FileError; // I/O error } - std::fflush(file); } - else - { - INFO_LOG(MAPLE, "Failed to save VMU %s data", logical_port); + else { + WARN_LOG(MAPLE, "Failed to save VMU %s data", logical_port); } return MDRS_DeviceReply; } From cd8a88366d9609fa710fa5701791503179315da4 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 6 Nov 2024 16:59:16 +0100 Subject: [PATCH 06/81] get rid of TARGET_NO_EXCEPTIONS --- core/build.h | 2 +- core/hw/mem/addrspace.cpp | 4 +--- core/hw/sh4/dyna/blockmanager.cpp | 4 ---- core/linux/common.cpp | 12 ------------ core/linux/context.cpp | 5 +---- shell/switch/libnx_vmem.cpp | 3 --- 6 files changed, 3 insertions(+), 27 deletions(-) diff --git a/core/build.h b/core/build.h index eddcecf13..b01192b21 100755 --- a/core/build.h +++ b/core/build.h @@ -100,7 +100,7 @@ #endif #endif -#if !defined(LIBRETRO) && !defined(TARGET_NO_EXCEPTIONS) +#if !defined(LIBRETRO) #define USE_GGPO #endif diff --git a/core/hw/mem/addrspace.cpp b/core/hw/mem/addrspace.cpp index fde6c74b4..976826904 100644 --- a/core/hw/mem/addrspace.cpp +++ b/core/hw/mem/addrspace.cpp @@ -385,11 +385,9 @@ bool reserve() if (ram_base != nullptr) return true; - // Use vmem only if settings mandate so, and if we have proper exception handlers. -#if !defined(TARGET_NO_EXCEPTIONS) + // Use vmem only if settings mandate so. if (!settings.dynarec.disable_nvmem) virtmem::init((void**)&ram_base, (void**)&p_sh4rcb, RAM_SIZE_MAX + VRAM_SIZE_MAX + ARAM_SIZE_MAX + elan::ERAM_SIZE_MAX); -#endif return true; } diff --git a/core/hw/sh4/dyna/blockmanager.cpp b/core/hw/sh4/dyna/blockmanager.cpp index 794119537..905871b4c 100644 --- a/core/hw/sh4/dyna/blockmanager.cpp +++ b/core/hw/sh4/dyna/blockmanager.cpp @@ -431,10 +431,6 @@ void RuntimeBlockInfo::Discard() void RuntimeBlockInfo::SetProtectedFlags() { -#ifdef TARGET_NO_EXCEPTIONS - this->read_only = false; - return; -#endif // Don't write protect rom and BIOS/IP.BIN (Grandia II) if (!IsOnRam(addr) || (addr & 0x1FFF0000) == 0x0c000000) { diff --git a/core/linux/common.cpp b/core/linux/common.cpp index 0ca150af0..6e7ac28e8 100644 --- a/core/linux/common.cpp +++ b/core/linux/common.cpp @@ -30,8 +30,6 @@ extern "C" char __start__; #define siginfo_t switch_siginfo_t #endif // __SWITCH__ -#if !defined(TARGET_NO_EXCEPTIONS) - void context_from_segfault(host_context_t* hctx, void* segfault_ctx); void context_to_segfault(host_context_t* hctx, void* segfault_ctx); @@ -110,16 +108,6 @@ void os_UninstallFaultHandler() #endif } -#else // !defined(TARGET_NO_EXCEPTIONS) - -void os_InstallFaultHandler() -{ -} -void os_UninstallFaultHandler() -{ -} -#endif // !defined(TARGET_NO_EXCEPTIONS) - #if !defined(__unix__) && !defined(LIBRETRO) && !defined(__SWITCH__) [[noreturn]] void os_DebugBreak() { diff --git a/core/linux/context.cpp b/core/linux/context.cpp index 6cd6c6eb9..c61d504e7 100644 --- a/core/linux/context.cpp +++ b/core/linux/context.cpp @@ -8,7 +8,7 @@ #define __USE_GNU 1 #endif - #if !defined(TARGET_NO_EXCEPTIONS) && !defined(__OpenBSD__) + #if !defined(__OpenBSD__) #include #endif @@ -39,7 +39,6 @@ static void bicopy(Tctx& ctx, Tseg& seg) template static void context_segfault(host_context_t* hostctx, void* segfault_ctx) { -#if !defined(TARGET_NO_EXCEPTIONS) #if HOST_CPU == CPU_ARM #if defined(__FreeBSD__) bicopy(hostctx->pc, MCTX(.__gregs[_REG_PC])); @@ -133,8 +132,6 @@ static void context_segfault(host_context_t* hostctx, void* segfault_ctx) #else #error Unsupported HOST_CPU #endif - #endif - } void context_from_segfault(host_context_t* hostctx, void* segfault_ctx) { diff --git a/shell/switch/libnx_vmem.cpp b/shell/switch/libnx_vmem.cpp index 395079f12..3c5320429 100644 --- a/shell/switch/libnx_vmem.cpp +++ b/shell/switch/libnx_vmem.cpp @@ -283,8 +283,6 @@ void release_jit_block(void *code_area1, void *code_area2, size_t size) } // namespace virtmem -#ifndef TARGET_NO_EXCEPTIONS - #include void fault_handler(int sn, siginfo_t * si, void *segfault_ctx); @@ -325,7 +323,6 @@ void __libnx_exception_handler(ThreadExceptionDump *ctx) context_switch_aarch64(ptr); } } -#endif // TARGET_NO_EXCEPTIONS #ifndef LIBRETRO [[noreturn]] void os_DebugBreak() From c77ddba10913cb13e97dbc5f5a2e8c2533b9134c Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 6 Nov 2024 17:03:01 +0100 Subject: [PATCH 07/81] get rid of settings.dynarec.disable_nvmem --- core/hw/mem/addrspace.cpp | 4 +--- core/types.h | 5 ----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/core/hw/mem/addrspace.cpp b/core/hw/mem/addrspace.cpp index 976826904..391f48834 100644 --- a/core/hw/mem/addrspace.cpp +++ b/core/hw/mem/addrspace.cpp @@ -385,9 +385,7 @@ bool reserve() if (ram_base != nullptr) return true; - // Use vmem only if settings mandate so. - if (!settings.dynarec.disable_nvmem) - virtmem::init((void**)&ram_base, (void**)&p_sh4rcb, RAM_SIZE_MAX + VRAM_SIZE_MAX + ARAM_SIZE_MAX + elan::ERAM_SIZE_MAX); + virtmem::init((void**)&ram_base, (void**)&p_sh4rcb, RAM_SIZE_MAX + VRAM_SIZE_MAX + ARAM_SIZE_MAX + elan::ERAM_SIZE_MAX); return true; } diff --git a/core/types.h b/core/types.h index e518762a0..b9aba6131 100644 --- a/core/types.h +++ b/core/types.h @@ -160,11 +160,6 @@ struct settings_t float uiScale = 1.f; } display; - struct - { - bool disable_nvmem; - } dynarec; - struct { bool muteAudio; From aa38771cd0785c71cfa6fc30f3a3f7971323f21c Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 6 Nov 2024 18:01:01 +0100 Subject: [PATCH 08/81] refactor sh4_sr_GetFull/sh4_sr_SetFull --- core/debug/debug_agent.h | 6 +++--- core/hw/sh4/dyna/decoder.cpp | 7 ++----- core/hw/sh4/interpr/sh4_opcodes.cpp | 8 ++++---- core/hw/sh4/sh4_if.h | 24 +++++++++++------------- core/hw/sh4/sh4_interrupts.cpp | 4 ++-- 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/core/debug/debug_agent.h b/core/debug/debug_agent.h index 9783edc56..12af1ecd4 100644 --- a/core/debug/debug_agent.h +++ b/core/debug/debug_agent.h @@ -122,7 +122,7 @@ class DebugAgent for (u32 i = 0; i < Sh4RegList.size(); i++) { if (Sh4RegList[i] == reg_sr_status) - allregs[i] = sh4_sr_GetFull(); + allregs[i] = p_sh4rcb->cntx.sr.getFull(); else if (Sh4RegList[i] != NoReg) allregs[i] = *GetRegPtr(Sh4RegList[i]); } @@ -143,7 +143,7 @@ class DebugAgent return 0; Sh4RegType reg = Sh4RegList[regNum]; if (reg == reg_sr_status) - return sh4_sr_GetFull(); + return p_sh4rcb->cntx.sr.getFull(); if (reg != NoReg) return *GetRegPtr(reg); return 0; @@ -154,7 +154,7 @@ class DebugAgent return; Sh4RegType reg = Sh4RegList[regNum]; if (reg == reg_sr_status) - sh4_sr_SetFull(value); + p_sh4rcb->cntx.sr.setFull(value); else if (reg != NoReg) *GetRegPtr(reg) = value; } diff --git a/core/hw/sh4/dyna/decoder.cpp b/core/hw/sh4/dyna/decoder.cpp index 457996ee3..6503e3fd2 100644 --- a/core/hw/sh4/dyna/decoder.cpp +++ b/core/hw/sh4/dyna/decoder.cpp @@ -95,9 +95,6 @@ static void dec_End(u32 dst, BlockEndType flags, bool delaySlot) verify(state.JumpAddr != NullAddress); } -#define SR_STATUS_MASK STATUS_MASK -#define SR_T_MASK 1 - static u32 dec_jump_simm8(u32 op) { return state.cpu.rpc + GetSImm8(op)*2 + 4; @@ -114,8 +111,8 @@ static u32 dec_set_pr() } static void dec_write_sr(shil_param src) { - Emit(shop_and,mk_reg(reg_sr_status),src,mk_imm(SR_STATUS_MASK)); - Emit(shop_and,mk_reg(reg_sr_T),src,mk_imm(SR_T_MASK)); + Emit(shop_and, mk_reg(reg_sr_status), src, mk_imm(sr_t::MASK)); + Emit(shop_and, mk_reg(reg_sr_T), src, mk_imm(1)); } //bf sh4dec(i1000_1011_iiii_iiii) diff --git a/core/hw/sh4/interpr/sh4_opcodes.cpp b/core/hw/sh4/interpr/sh4_opcodes.cpp index 7495d077e..dc4b24db3 100644 --- a/core/hw/sh4/interpr/sh4_opcodes.cpp +++ b/core/hw/sh4/interpr/sh4_opcodes.cpp @@ -1875,7 +1875,7 @@ sh4op(i0100_nnnn_0001_1011) sh4op(i0000_nnnn_0000_0010)//0002 { u32 n = GetN(op); - r[n] = sh4_sr_GetFull(); + r[n] = sr.getFull(); } //sts FPSCR, @@ -1897,7 +1897,7 @@ sh4op(i0100_nnnn_0110_0010) sh4op(i0100_nnnn_0000_0011) { u32 n = GetN(op); - WriteMemU32(r[n] - 4, sh4_sr_GetFull()); + WriteMemU32(r[n] - 4, sr.getFull()); r[n] -= 4; } @@ -1920,7 +1920,7 @@ sh4op(i0100_nnnn_0000_0111) u32 sr_t; ReadMemU32(sr_t,r[n]); - sh4_sr_SetFull(sr_t); + sr.setFull(sr_t); r[n] += 4; if (UpdateSR()) UpdateINTC(); @@ -1938,7 +1938,7 @@ sh4op(i0100_nnnn_0110_1010) sh4op(i0100_nnnn_0000_1110) { u32 n = GetN(op); - sh4_sr_SetFull(r[n]); + sr.setFull(r[n]); if (UpdateSR()) UpdateINTC(); } diff --git a/core/hw/sh4/sh4_if.h b/core/hw/sh4/sh4_if.h index 096365c11..d52bf766a 100644 --- a/core/hw/sh4/sh4_if.h +++ b/core/hw/sh4/sh4_if.h @@ -145,8 +145,6 @@ union sr_status_t u32 status; }; -#define STATUS_MASK 0x700083F2 - // Status register with isolated T bit. // Used in place of the normal SR bitfield so that the T bit can be // handled as a regular register. This simplifies dynarec implementations. @@ -173,6 +171,17 @@ struct sr_t u32 status; }; u32 T; + + static constexpr u32 MASK = 0x700083F2; + + u32 getFull() const { + return (status & MASK) | T; + } + + void setFull(u32 v) { + status = v & MASK; + T = v & 1; + } }; // FPSCR (fpu status and control register) @@ -300,17 +309,6 @@ struct alignas(PAGE_SIZE) Sh4RCB extern Sh4RCB* p_sh4rcb; -static inline u32 sh4_sr_GetFull() -{ - return (p_sh4rcb->cntx.sr.status & STATUS_MASK) | p_sh4rcb->cntx.sr.T; -} - -static inline void sh4_sr_SetFull(u32 value) -{ - p_sh4rcb->cntx.sr.status=value & STATUS_MASK; - p_sh4rcb->cntx.sr.T=value&1; -} - #define do_sqw_nommu sh4rcb.do_sqw_nommu #define sh4rcb (*p_sh4rcb) diff --git a/core/hw/sh4/sh4_interrupts.cpp b/core/hw/sh4/sh4_interrupts.cpp index 6274f4272..115493f01 100644 --- a/core/hw/sh4/sh4_interrupts.cpp +++ b/core/hw/sh4/sh4_interrupts.cpp @@ -185,7 +185,7 @@ static void Do_Interrupt(Sh4ExceptionCode intEvn) { CCN_INTEVT = intEvn; - ssr = sh4_sr_GetFull(); + ssr = sr.getFull(); spc = next_pc; sgr = r[15]; sr.BL = 1; @@ -204,7 +204,7 @@ void Do_Exception(u32 epc, Sh4ExceptionCode expEvn) throw FlycastException("Fatal: SH4 exception when blocked"); CCN_EXPEVT = expEvn; - ssr = sh4_sr_GetFull(); + ssr = sr.getFull(); spc = epc; sgr = r[15]; sr.BL = 1; From 897e06b887e44b37ebb30871efaed8d998f45584 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 6 Nov 2024 19:26:04 +0100 Subject: [PATCH 09/81] sh4: refactor GetDR/SetDR and access to fp registers --- core/hw/sh4/interpr/sh4_fpu.cpp | 6 +- core/hw/sh4/interpr/sh4_interpreter.cpp | 14 ++--- core/hw/sh4/modules/ccn.cpp | 2 + core/hw/sh4/sh4_core.h | 83 +------------------------ core/hw/sh4/sh4_if.h | 48 ++++++++++++++ core/hw/sh4/sh4_interpreter.h | 1 - core/rec-x64/rec_x64.cpp | 1 + core/reios/gdrom_hle.h | 1 + tests/src/sh4_ops.h | 16 ++--- 9 files changed, 70 insertions(+), 102 deletions(-) diff --git a/core/hw/sh4/interpr/sh4_fpu.cpp b/core/hw/sh4/interpr/sh4_fpu.cpp index 27bb2b175..bc5fee8ad 100644 --- a/core/hw/sh4/interpr/sh4_fpu.cpp +++ b/core/hw/sh4/interpr/sh4_fpu.cpp @@ -16,13 +16,13 @@ static u32 GetM(u32 op) { } static double getDRn(u32 op) { - return GetDR((op >> 9) & 7); + return p_sh4rcb->cntx.getDR((op >> 9) & 7); } static double getDRm(u32 op) { - return GetDR((op >> 5) & 7); + return p_sh4rcb->cntx.getDR((op >> 5) & 7); } static void setDRn(u32 op, double d) { - SetDR((op >> 9) & 7, d); + p_sh4rcb->cntx.setDR((op >> 9) & 7, d); } static void iNimp(const char *str); diff --git a/core/hw/sh4/interpr/sh4_interpreter.cpp b/core/hw/sh4/interpr/sh4_interpreter.cpp index 780f971aa..989df4455 100644 --- a/core/hw/sh4/interpr/sh4_interpreter.cpp +++ b/core/hw/sh4/interpr/sh4_interpreter.cpp @@ -117,7 +117,7 @@ static void Sh4_int_Reset(bool hard) gbr=ssr=spc=sgr=dbr=vbr=0; mac.full=pr=fpul=0; - sh4_sr_SetFull(0x700000F0); + sr.setFull(0x700000F0); old_sr.status=sr.status; UpdateSR(); @@ -163,7 +163,7 @@ void ExecuteDelayslot_RTE() // instruction execution. The STC and STC.L SR instructions access all SR bits after modification. u32 op = ReadNexOp(); // Now restore all SR bits - sh4_sr_SetFull(ssr); + sr.setFull(ssr); // And execute ExecuteOpcode(op); } catch (const SH4ThrownException&) { @@ -175,18 +175,12 @@ void ExecuteDelayslot_RTE() } // every SH4_TIMESLICE cycles -int UpdateSystem() +int UpdateSystem_INTC() { Sh4cntx.sh4_sched_next -= SH4_TIMESLICE; if (Sh4cntx.sh4_sched_next < 0) sh4_sched_tick(SH4_TIMESLICE); - - return Sh4cntx.interrupt_pend; -} - -int UpdateSystem_INTC() -{ - if (UpdateSystem()) + if (Sh4cntx.interrupt_pend) return UpdateINTC(); else return 0; diff --git a/core/hw/sh4/modules/ccn.cpp b/core/hw/sh4/modules/ccn.cpp index 23e4ba51e..708f33110 100644 --- a/core/hw/sh4/modules/ccn.cpp +++ b/core/hw/sh4/modules/ccn.cpp @@ -7,6 +7,7 @@ #include "hw/sh4/sh4_mmr.h" #include "hw/sh4/sh4_core.h" #include "hw/sh4/sh4_cache.h" +#include "cfg/option.h" CCNRegisters ccn; @@ -82,6 +83,7 @@ static void CCN_CCR_write(u32 addr, u32 value) static u32 CPU_VERSION_read(u32 addr) { return 0x040205c1; // this is what a real SH7091 in a Dreamcast returns - the later Naomi BIOSes check and care! + // 0x040204d0 on a PAL VA1 dreamcast, also works } static u32 CCN_PRR_read(u32 addr) diff --git a/core/hw/sh4/sh4_core.h b/core/hw/sh4/sh4_core.h index c1994f3d3..ab0571d10 100644 --- a/core/hw/sh4/sh4_core.h +++ b/core/hw/sh4/sh4_core.h @@ -1,7 +1,6 @@ #pragma once #include "types.h" #include "sh4_if.h" -#include "cfg/option.h" #define r Sh4cntx.r #define r_bank Sh4cntx.r_bank @@ -33,68 +32,6 @@ bool UpdateSR(); void RestoreHostRoundingMode(); void setDefaultRoundingMode(); -union DoubleReg -{ - f64 dbl; - f32 sgl[2]; -}; - -static inline f64 GetDR(u32 n) -{ -#ifdef TRACE - if (n>7) - INFO_LOG(SH4, "DR_r INDEX OVERRUN %d >7", n); -#endif - DoubleReg t; - - t.sgl[1]=fr[(n<<1) + 0]; - t.sgl[0]=fr[(n<<1) + 1]; - - return t.dbl; -} - -static inline f64 GetXD(u32 n) -{ -#ifdef TRACE - if (n>7) - INFO_LOG(SH4, "XD_r INDEX OVERRUN %d >7", n); -#endif - DoubleReg t; - - t.sgl[1]=xf[(n<<1) + 0]; - t.sgl[0]=xf[(n<<1) + 1]; - - return t.dbl; -} - -static inline void SetDR(u32 n,f64 val) -{ -#ifdef TRACE - if (n>7) - INFO_LOG(SH4, "DR_w INDEX OVERRUN %d >7", n); -#endif - DoubleReg t; - t.dbl=val; - - - fr[(n<<1) | 1]=t.sgl[0]; - fr[(n<<1) | 0]=t.sgl[1]; -} - -static inline void SetXD(u32 n,f64 val) -{ -#ifdef TRACE - if (n>7) - INFO_LOG(SH4, "XD_w INDEX OVERRUN %d >7", n); -#endif - - DoubleReg t; - t.dbl=val; - - xf[(n<<1) | 1]=t.sgl[0]; - xf[(n<<1) | 0]=t.sgl[1]; -} - struct SH4ThrownException { SH4ThrownException(u32 epc, Sh4ExceptionCode expEvn) : epc(epc), expEvn(expEvn) { } @@ -117,20 +54,13 @@ static inline void AdjustDelaySlotException(SH4ThrownException& ex) ex.expEvn = Sh4Ex_SlotIllegalInstr; } -// The SH4 sets the signaling bit to 0 for qNaN (unlike all recent CPUs). Some games rely on this. +// The SH4 sets the signaling bit to 0 for qNaN (unlike all recent CPUs). static inline f32 fixNaN(f32 f) { #ifdef STRICT_MODE u32& hex = *(u32 *)&f; -#ifdef __FAST_MATH__ - // fast-math - if ((hex & 0x7fffffff) > 0x7f800000) - hex = 0x7fbfffff; -#else - // no fast-math - if (f != f) + if (std::isnan(f)) hex = 0x7fbfffff; -#endif #endif return f; } @@ -139,15 +69,8 @@ static inline f64 fixNaN64(f64 f) { #ifdef STRICT_MODE u64& hex = *(u64 *)&f; -#ifdef __FAST_MATH__ - // fast-math - if ((hex & 0x7fffffffffffffffll) > 0x7ff0000000000000ll) - hex = 0x7ff7ffffffffffffll; -#else - // no fast-math - if (f != f) + if (std::isnan(f)) hex = 0x7ff7ffffffffffffll; -#endif #endif return f; } diff --git a/core/hw/sh4/sh4_if.h b/core/hw/sh4/sh4_if.h index d52bf766a..60d000a0d 100644 --- a/core/hw/sh4/sh4_if.h +++ b/core/hw/sh4/sh4_if.h @@ -1,6 +1,7 @@ #pragma once #include "types.h" #include "stdclass.h" +#include enum Sh4RegType { @@ -276,6 +277,53 @@ struct alignas(64) Sh4Context }; u64 raw[64-8]; }; + + f32& fr(int idx) { + assert(idx >= 0 && idx <= 15); + return xffr[idx + 16]; + } + f32& xf(int idx) { + assert(idx >= 0 && idx <= 15); + return xffr[idx]; + } + u32& fr_hex(int idx) { + assert(idx >= 0 && idx <= 15); + return reinterpret_cast(fr(idx)); + } + u64& dr_hex(int idx) { + assert(idx >= 0 && idx <= 7); + return *reinterpret_cast(&fr(idx * 2)); + } + u64& xd_hex(int idx) { + assert(idx >= 0 && idx <= 7); + return *reinterpret_cast(&xf(idx * 2)); + } + + f64 getDR(u32 n) + { + assert(n <= 7); + DoubleReg t; + t.sgl[1] = fr(n * 2); + t.sgl[0] = fr(n * 2 + 1); + + return t.dbl; + } + + void setDR(u32 n, f64 val) + { + assert(n <= 7); + DoubleReg t; + t.dbl = val; + fr(n * 2) = t.sgl[1]; + fr(n * 2 + 1) = t.sgl[0]; + } + +private: + union DoubleReg + { + f64 dbl; + f32 sgl[2]; + }; }; static_assert(sizeof(Sh4Context) == 448, "Invalid Sh4Context size"); diff --git a/core/hw/sh4/sh4_interpreter.h b/core/hw/sh4/sh4_interpreter.h index 80605b04f..4c5edabb3 100644 --- a/core/hw/sh4/sh4_interpreter.h +++ b/core/hw/sh4/sh4_interpreter.h @@ -38,5 +38,4 @@ void ExecuteDelayslot_RTE(); #define SH4_TIMESLICE 448 // at 112 Bangai-O doesn't start. 224 is ok -int UpdateSystem(); int UpdateSystem_INTC(); diff --git a/core/rec-x64/rec_x64.cpp b/core/rec-x64/rec_x64.cpp index ffbaa2573..bbad35e0d 100644 --- a/core/rec-x64/rec_x64.cpp +++ b/core/rec-x64/rec_x64.cpp @@ -20,6 +20,7 @@ using namespace Xbyak::util; #include "xbyak_base.h" #include "oslib/unwind_info.h" #include "oslib/virtmem.h" +#include "cfg/option.h" static void (*mainloop)(); static void (*handleException)(); diff --git a/core/reios/gdrom_hle.h b/core/reios/gdrom_hle.h index 2f5d98785..c2e63d5a6 100644 --- a/core/reios/gdrom_hle.h +++ b/core/reios/gdrom_hle.h @@ -1,5 +1,6 @@ #pragma once #include "serialize.h" +#include "cfg/option.h" #define SYSCALL_GDROM 0x00 diff --git a/tests/src/sh4_ops.h b/tests/src/sh4_ops.h index ba0e4f20b..af5ab2921 100644 --- a/tests/src/sh4_ops.h +++ b/tests/src/sh4_ops.h @@ -48,7 +48,7 @@ class Sh4OpTest : public ::testing::Test { r(i) = REG_MAGIC; for (int i = 0; i < 32; i++) *(u32 *)&ctx->xffr[i] = REG_MAGIC; - sh4_sr_SetFull(0x700000F0); + sr().setFull(0x700000F0); mac() = 0; gbr() = REG_MAGIC; checkedRegs.clear(); @@ -78,16 +78,16 @@ class Sh4OpTest : public ::testing::Test { u32& mach() { return ctx->mac.l; } u32& macl() { return ctx->mac.h; } sr_t& sr() { return ctx->sr; } - f32& fr(int regNum) { checkedRegs.insert((u32 *)&ctx->xffr[regNum + 16]); return ctx->xffr[regNum + 16]; } + f32& fr(int regNum) { checkedRegs.insert((u32 *)&ctx->fr(regNum)); return ctx->fr(regNum); } double getDr(int regNum) { - checkedRegs.insert((u32 *)&ctx->xffr[regNum * 2 + 16]); - checkedRegs.insert((u32 *)&ctx->xffr[regNum * 2 + 1 + 16]); - return GetDR(regNum); + checkedRegs.insert((u32 *)&ctx->fr(regNum * 2)); + checkedRegs.insert((u32 *)&ctx->fr(regNum * 2 + 1)); + return ctx->getDR(regNum); } void setDr(int regNum, double d) { - checkedRegs.insert((u32 *)&ctx->xffr[regNum * 2 + 16]); - checkedRegs.insert((u32 *)&ctx->xffr[regNum * 2 + 1 + 16]); - SetDR(regNum, d); + checkedRegs.insert((u32 *)&ctx->fr(regNum * 2)); + checkedRegs.insert((u32 *)&ctx->fr(regNum * 2 + 1)); + ctx->setDR(regNum, d); } Sh4Context *ctx; From 7eec648690b0f74e9cdab985eb4f63e93d4bb14f Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 6 Nov 2024 20:46:32 +0100 Subject: [PATCH 10/81] sh4: move Sh4RegType & GetRegPtr into sh4/dyna rec-arm: use getRegOffset/reg_nofs instead of GetRegPtr --- core/hw/sh4/dyna/shil.cpp | 74 +++++++++++++++----- core/hw/sh4/dyna/shil.h | 128 +++++++++++++++++++++++++++++++++- core/hw/sh4/sh4_core_regs.cpp | 102 --------------------------- core/hw/sh4/sh4_if.h | 122 -------------------------------- core/rec-ARM/rec_arm.cpp | 10 +-- 5 files changed, 189 insertions(+), 247 deletions(-) diff --git a/core/hw/sh4/dyna/shil.cpp b/core/hw/sh4/dyna/shil.cpp index 6c20e8b54..76c40a290 100644 --- a/core/hw/sh4/dyna/shil.cpp +++ b/core/hw/sh4/dyna/shil.cpp @@ -5,22 +5,6 @@ #include "hw/sh4/sh4_mmr.h" #include "ngen.h" -#include "hw/sh4/sh4_core.h" - -#define SHIL_MODE 1 -#include "shil_canonical.h" - -#define SHIL_MODE 4 -#include "shil_canonical.h" - -//#define SHIL_MODE 2 -//#include "shil_canonical.h" - -#if FEAT_SHREC != DYNAREC_NONE -#define SHIL_MODE 3 -#include "shil_canonical.h" -#endif - #include "ssa.h" void AnalyseBlock(RuntimeBlockInfo* blk) @@ -29,6 +13,48 @@ void AnalyseBlock(RuntimeBlockInfo* blk) optim.Optimize(); } +u32 getRegOffset(Sh4RegType reg) +{ + if (reg >= reg_r0 && reg <= reg_r15) + return offsetof(Sh4Context, r[reg - reg_r0]); + if (reg >= reg_r0_Bank && reg <= reg_r7_Bank) + return offsetof(Sh4Context, r_bank[reg - reg_r0_Bank]); + if (reg >= reg_fr_0 && reg <= reg_fr_15) + return offsetof(Sh4Context, xffr[reg - reg_fr_0 + 16]); + if (reg >= reg_xf_0 && reg <= reg_xf_15) + return offsetof(Sh4Context, xffr[reg - reg_xf_0]); + switch (reg) + { + case reg_gbr: return offsetof(Sh4Context, gbr); + case reg_vbr: return offsetof(Sh4Context, vbr); + case reg_ssr: return offsetof(Sh4Context, ssr); + case reg_spc: return offsetof(Sh4Context, spc); + case reg_sgr: return offsetof(Sh4Context, sgr); + case reg_dbr: return offsetof(Sh4Context, dbr); + case reg_mach: return offsetof(Sh4Context, mac.h); + case reg_macl: return offsetof(Sh4Context, mac.l); + case reg_pr: return offsetof(Sh4Context, pr); + case reg_fpul: return offsetof(Sh4Context, fpul); + case reg_nextpc: return offsetof(Sh4Context, pc); + case reg_sr_status: return offsetof(Sh4Context, sr.status); + case reg_sr_T: return offsetof(Sh4Context, sr.T); + case reg_old_fpscr: return offsetof(Sh4Context, old_fpscr.full); + case reg_fpscr: return offsetof(Sh4Context, fpscr.full); + case reg_pc_dyn: return offsetof(Sh4Context, jdyn); + case reg_temp: return offsetof(Sh4Context, temp_reg); + // TODO case reg_sq_buffer: return offsetof(Sh4Context, sq_buffer); + default: + ERROR_LOG(SH4, "Unknown register ID %d", reg); + die("Invalid reg"); + return 0; + } +} + +u32* GetRegPtr(u32 reg) +{ + return (u32 *)((u8 *)&p_sh4rcb->cntx + getRegOffset((Sh4RegType)reg)); +} + std::string name_reg(Sh4RegType reg) { std::stringstream ss; @@ -119,6 +145,22 @@ static std::string dissasm_param(const shil_param& prm, bool comma) return ss.str(); } +#include "hw/sh4/sh4_core.h" + +#define SHIL_MODE 1 +#include "shil_canonical.h" + +#define SHIL_MODE 4 +#include "shil_canonical.h" + +//#define SHIL_MODE 2 +//#include "shil_canonical.h" + +#if FEAT_SHREC != DYNAREC_NONE +#define SHIL_MODE 3 +#include "shil_canonical.h" +#endif + std::string shil_opcode::dissasm() const { std::stringstream ss; diff --git a/core/hw/sh4/dyna/shil.h b/core/hw/sh4/dyna/shil.h index 9e2112bbf..308e393b0 100644 --- a/core/hw/sh4/dyna/shil.h +++ b/core/hw/sh4/dyna/shil.h @@ -5,6 +5,131 @@ struct shil_opcode; typedef void shil_chfp(shil_opcode* op); extern shil_chfp* shil_chf[]; +enum Sh4RegType +{ + //GPRs + reg_r0, + reg_r1, + reg_r2, + reg_r3, + reg_r4, + reg_r5, + reg_r6, + reg_r7, + reg_r8, + reg_r9, + reg_r10, + reg_r11, + reg_r12, + reg_r13, + reg_r14, + reg_r15, + + //FPU, bank 0 + reg_fr_0, + reg_fr_1, + reg_fr_2, + reg_fr_3, + reg_fr_4, + reg_fr_5, + reg_fr_6, + reg_fr_7, + reg_fr_8, + reg_fr_9, + reg_fr_10, + reg_fr_11, + reg_fr_12, + reg_fr_13, + reg_fr_14, + reg_fr_15, + + //FPU, bank 1 + reg_xf_0, + reg_xf_1, + reg_xf_2, + reg_xf_3, + reg_xf_4, + reg_xf_5, + reg_xf_6, + reg_xf_7, + reg_xf_8, + reg_xf_9, + reg_xf_10, + reg_xf_11, + reg_xf_12, + reg_xf_13, + reg_xf_14, + reg_xf_15, + + //GPR Interrupt bank + reg_r0_Bank, + reg_r1_Bank, + reg_r2_Bank, + reg_r3_Bank, + reg_r4_Bank, + reg_r5_Bank, + reg_r6_Bank, + reg_r7_Bank, + + //Misc regs + reg_gbr, + reg_ssr, + reg_spc, + reg_sgr, + reg_dbr, + reg_vbr, + reg_mach, + reg_macl, + reg_pr, + reg_fpul, + reg_nextpc, + reg_sr_status, //Only the status bits + reg_sr_T, //Only T + reg_old_fpscr, + reg_fpscr, + + reg_pc_dyn, //Write only, for dynarec only (dynamic block exit address) + reg_temp, + + sh4_reg_count, + + /* + These are virtual registers, used by the dynarec decoder + */ + regv_dr_0, + regv_dr_2, + regv_dr_4, + regv_dr_6, + regv_dr_8, + regv_dr_10, + regv_dr_12, + regv_dr_14, + + regv_xd_0, + regv_xd_2, + regv_xd_4, + regv_xd_6, + regv_xd_8, + regv_xd_10, + regv_xd_12, + regv_xd_14, + + regv_fv_0, + regv_fv_4, + regv_fv_8, + regv_fv_12, + + regv_xmtrx, + regv_fmtrx, + + //reg_sq_buffer, + + NoReg=-1 +}; + +u32 getRegOffset(Sh4RegType reg); +u32* GetRegPtr(u32 reg); + enum shil_param_type { FMT_NULL, @@ -27,7 +152,6 @@ enum shil_param_type param types: r32, r64 */ - #define SHIL_MODE 0 #include "shil_canonical.h" @@ -108,7 +232,7 @@ struct shil_param bool is_imm_s8() const { return is_imm() && (int8_t)_imm == (int32_t)_imm; } u32* reg_ptr() const { verify(is_reg()); return GetRegPtr(_reg); } - s32 reg_nofs() const { verify(is_reg()); return (s32)((u8*)GetRegPtr(_reg) - (u8*)GetRegPtr(reg_xf_0)-sizeof(Sh4cntx)); } + s32 reg_nofs() const { verify(is_reg()); return (int)getRegOffset(_reg) - sizeof(Sh4Context); } u32 reg_aofs() const { return -reg_nofs(); } u32 imm_value() const { verify(is_imm()); return _imm; } diff --git a/core/hw/sh4/sh4_core_regs.cpp b/core/hw/sh4/sh4_core_regs.cpp index 6c94093f5..2fbd1584b 100644 --- a/core/hw/sh4/sh4_core_regs.cpp +++ b/core/hw/sh4/sh4_core_regs.cpp @@ -161,105 +161,3 @@ void setDefaultRoundingMode() fpscr.RM = savedRM; fpscr.DN = savedDN; } - -static u32* Sh4_int_GetRegisterPtr(Sh4RegType reg) -{ - if ((reg>=reg_r0) && (reg<=reg_r15)) - { - return &r[reg-reg_r0]; - } - else if ((reg>=reg_r0_Bank) && (reg<=reg_r7_Bank)) - { - return &r_bank[reg-reg_r0_Bank]; - } - else if ((reg>=reg_fr_0) && (reg<=reg_fr_15)) - { - return &fr_hex[reg-reg_fr_0]; - } - else if ((reg>=reg_xf_0) && (reg<=reg_xf_15)) - { - return &xf_hex[reg-reg_xf_0]; - } - else - { - switch(reg) - { - case reg_gbr : - return &gbr; - break; - case reg_vbr : - return &vbr; - break; - - case reg_ssr : - return &ssr; - break; - - case reg_spc : - return &spc; - break; - - case reg_sgr : - return &sgr; - break; - - case reg_dbr : - return &dbr; - break; - - case reg_mach : - return &mac.h; - break; - - case reg_macl : - return &mac.l; - break; - - case reg_pr : - return ≺ - break; - - case reg_fpul : - return &fpul; - break; - - - case reg_nextpc : - return &next_pc; - break; - - case reg_sr_status : - return &sr.status; - break; - - case reg_sr_T : - return &sr.T; - break; - - case reg_old_fpscr : - return &old_fpscr.full; - break; - - case reg_fpscr : - return &fpscr.full; - break; - - case reg_pc_dyn: - return &Sh4cntx.jdyn; - - case reg_temp: - return &Sh4cntx.temp_reg; - - default: - ERROR_LOG(SH4, "Unknown register ID %d", reg); - die("Invalid reg"); - return 0; - break; - } - } -} - -u32* GetRegPtr(u32 reg) -{ - return Sh4_int_GetRegisterPtr((Sh4RegType)reg); -} diff --git a/core/hw/sh4/sh4_if.h b/core/hw/sh4/sh4_if.h index 60d000a0d..a71425286 100644 --- a/core/hw/sh4/sh4_if.h +++ b/core/hw/sh4/sh4_if.h @@ -3,126 +3,6 @@ #include "stdclass.h" #include -enum Sh4RegType -{ - //GPRs - reg_r0, - reg_r1, - reg_r2, - reg_r3, - reg_r4, - reg_r5, - reg_r6, - reg_r7, - reg_r8, - reg_r9, - reg_r10, - reg_r11, - reg_r12, - reg_r13, - reg_r14, - reg_r15, - - //FPU, bank 0 - reg_fr_0, - reg_fr_1, - reg_fr_2, - reg_fr_3, - reg_fr_4, - reg_fr_5, - reg_fr_6, - reg_fr_7, - reg_fr_8, - reg_fr_9, - reg_fr_10, - reg_fr_11, - reg_fr_12, - reg_fr_13, - reg_fr_14, - reg_fr_15, - - //FPU, bank 1 - reg_xf_0, - reg_xf_1, - reg_xf_2, - reg_xf_3, - reg_xf_4, - reg_xf_5, - reg_xf_6, - reg_xf_7, - reg_xf_8, - reg_xf_9, - reg_xf_10, - reg_xf_11, - reg_xf_12, - reg_xf_13, - reg_xf_14, - reg_xf_15, - - //GPR Interrupt bank - reg_r0_Bank, - reg_r1_Bank, - reg_r2_Bank, - reg_r3_Bank, - reg_r4_Bank, - reg_r5_Bank, - reg_r6_Bank, - reg_r7_Bank, - - //Misc regs - reg_gbr, - reg_ssr, - reg_spc, - reg_sgr, - reg_dbr, - reg_vbr, - reg_mach, - reg_macl, - reg_pr, - reg_fpul, - reg_nextpc, - reg_sr_status, //Only the status bits - reg_sr_T, //Only T - reg_old_fpscr, - reg_fpscr, - - reg_pc_dyn, //Write only, for dynarec only (dynamic block exit address) - reg_temp, - - sh4_reg_count, - - /* - These are virtual registers, used by the dynarec decoder - */ - regv_dr_0, - regv_dr_2, - regv_dr_4, - regv_dr_6, - regv_dr_8, - regv_dr_10, - regv_dr_12, - regv_dr_14, - - regv_xd_0, - regv_xd_2, - regv_xd_4, - regv_xd_6, - regv_xd_8, - regv_xd_10, - regv_xd_12, - regv_xd_14, - - regv_fv_0, - regv_fv_4, - regv_fv_8, - regv_fv_12, - - regv_xmtrx, - regv_fmtrx, - - NoReg=-1 -}; - // SR (status register) union sr_status_t @@ -366,8 +246,6 @@ extern Sh4RCB* p_sh4rcb; void Get_Sh4Interpreter(sh4_if* cpu); void Get_Sh4Recompiler(sh4_if* cpu); -u32* GetRegPtr(u32 reg); - enum Sh4ExceptionCode : u16 { Sh4Ex_PowerOnReset = 0, diff --git a/core/rec-ARM/rec_arm.cpp b/core/rec-ARM/rec_arm.cpp index ac5c48aa5..b020039e7 100644 --- a/core/rec-ARM/rec_arm.cpp +++ b/core/rec-ARM/rec_arm.cpp @@ -170,14 +170,14 @@ class Arm32Assembler : public MacroAssembler void loadSh4Reg(Register Rt, u32 Sh4_Reg) { - const int shRegOffs = (u8*)GetRegPtr(Sh4_Reg) - (u8*)&p_sh4rcb->cntx - sizeof(Sh4cntx); + const int shRegOffs = getRegOffset((Sh4RegType)Sh4_Reg) - sizeof(Sh4Context); Ldr(Rt, MemOperand(r8, shRegOffs)); } void storeSh4Reg(Register Rt, u32 Sh4_Reg) { - const int shRegOffs = (u8*)GetRegPtr(Sh4_Reg) - (u8*)&p_sh4rcb->cntx - sizeof(Sh4cntx); + const int shRegOffs = getRegOffset((Sh4RegType)Sh4_Reg) - sizeof(Sh4Context); Str(Rt, MemOperand(r8, shRegOffs)); } @@ -393,13 +393,13 @@ void arm_reg_alloc::Writeback(u32 reg, int nreg) void arm_reg_alloc::Preload_FPU(u32 reg, int nreg) { - const s32 shRegOffs = (u8*)GetRegPtr(reg) - (u8*)&p_sh4rcb->cntx - sizeof(Sh4cntx); + const s32 shRegOffs = getRegOffset((Sh4RegType)reg) - sizeof(Sh4Context); ass.Vldr(SRegister(nreg), MemOperand(r8, shRegOffs)); } void arm_reg_alloc::Writeback_FPU(u32 reg, int nreg) { - const s32 shRegOffs = (u8*)GetRegPtr(reg) - (u8*)&p_sh4rcb->cntx - sizeof(Sh4cntx); + const s32 shRegOffs = getRegOffset((Sh4RegType)reg) - sizeof(Sh4Context); ass.Vstr(SRegister(nreg), MemOperand(r8, shRegOffs)); } @@ -649,7 +649,7 @@ void Arm32Assembler::canonCall(const shil_opcode *op, void *function) if (ccParam.type == CPT_ptr && prm.count() == 2 && reg.IsAllocf(prm) && (op->rd._reg == prm._reg || op->rd2._reg == prm._reg)) { // fsca rd param is a pointer to a 64-bit reg so reload the regs if allocated - const int shRegOffs = (u8*)GetRegPtr(prm._reg) - (u8*)&p_sh4rcb->cntx - sizeof(Sh4cntx); + const int shRegOffs = prm.reg_nofs(); Vldr(reg.mapFReg(prm, 0), MemOperand(r8, shRegOffs)); Vldr(reg.mapFReg(prm, 1), MemOperand(r8, shRegOffs + 4)); } From d5aeb482d97ee6406b70852cdb7f25b8f1294a6e Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 6 Nov 2024 22:11:27 +0100 Subject: [PATCH 11/81] sh4: move sq_buffer into sh4 context --- core/hw/sh4/dyna/driver.cpp | 2 +- core/hw/sh4/dyna/shil.cpp | 2 +- core/hw/sh4/dyna/shil.h | 2 +- core/hw/sh4/dyna/shil_canonical.h | 2 +- core/hw/sh4/interpr/sh4_interpreter.cpp | 4 +--- core/hw/sh4/interpr/sh4_opcodes.cpp | 2 +- core/hw/sh4/sh4_core.h | 5 ----- core/hw/sh4/sh4_if.h | 17 +++++++++-------- core/hw/sh4/sh4_mmr.cpp | 14 +++++--------- core/hw/sh4/sh4_mmr.h | 2 -- core/hw/sh4/storeq.cpp | 4 ++-- core/rec-ARM/rec_arm.cpp | 6 +++--- core/rec-ARM64/rec_arm64.cpp | 6 +++--- core/rec-x64/rec_x64.cpp | 4 ++-- core/rec-x86/x86_ops.cpp | 8 ++++---- 15 files changed, 34 insertions(+), 46 deletions(-) diff --git a/core/hw/sh4/dyna/driver.cpp b/core/hw/sh4/dyna/driver.cpp index 8c3a0c732..c57e5eb36 100644 --- a/core/hw/sh4/dyna/driver.cpp +++ b/core/hw/sh4/dyna/driver.cpp @@ -371,7 +371,7 @@ static void recSh4_Init() bm_Init(); if (addrspace::virtmemEnabled()) - verify(&mem_b[0] == ((u8*)p_sh4rcb->sq_buffer + 512 + 0x0C000000)); + verify(&mem_b[0] == ((u8*)p_sh4rcb->cntx.sq_buffer + 512 + 0x0C000000)); // Call the platform-specific magic to make the pages RWX CodeCache = nullptr; diff --git a/core/hw/sh4/dyna/shil.cpp b/core/hw/sh4/dyna/shil.cpp index 76c40a290..635e3277c 100644 --- a/core/hw/sh4/dyna/shil.cpp +++ b/core/hw/sh4/dyna/shil.cpp @@ -42,7 +42,7 @@ u32 getRegOffset(Sh4RegType reg) case reg_fpscr: return offsetof(Sh4Context, fpscr.full); case reg_pc_dyn: return offsetof(Sh4Context, jdyn); case reg_temp: return offsetof(Sh4Context, temp_reg); - // TODO case reg_sq_buffer: return offsetof(Sh4Context, sq_buffer); + case reg_sq_buffer: return offsetof(Sh4Context, sq_buffer); default: ERROR_LOG(SH4, "Unknown register ID %d", reg); die("Invalid reg"); diff --git a/core/hw/sh4/dyna/shil.h b/core/hw/sh4/dyna/shil.h index 308e393b0..cec8498c6 100644 --- a/core/hw/sh4/dyna/shil.h +++ b/core/hw/sh4/dyna/shil.h @@ -122,7 +122,7 @@ enum Sh4RegType regv_xmtrx, regv_fmtrx, - //reg_sq_buffer, + reg_sq_buffer, NoReg=-1 }; diff --git a/core/hw/sh4/dyna/shil_canonical.h b/core/hw/sh4/dyna/shil_canonical.h index e041d7874..0828675f3 100644 --- a/core/hw/sh4/dyna/shil_canonical.h +++ b/core/hw/sh4/dyna/shil_canonical.h @@ -802,7 +802,7 @@ void,f1,(u32 r1), shil_canonical ( void,f2,(u32 r1), - if ((r1>>26) == 0x38) do_sqw_nommu(r1,sq_both); + if ((r1>>26) == 0x38) do_sqw_nommu(r1, p_sh4rcb->cntx.sq_buffer); ) shil_compile diff --git a/core/hw/sh4/interpr/sh4_interpreter.cpp b/core/hw/sh4/interpr/sh4_interpreter.cpp index 989df4455..250d6ca9d 100644 --- a/core/hw/sh4/interpr/sh4_interpreter.cpp +++ b/core/hw/sh4/interpr/sh4_interpreter.cpp @@ -28,7 +28,7 @@ Sh4Cycles sh4cycles(CPU_RATIO); static void ExecuteOpcode(u16 op) { if (sr.FD == 1 && OpDesc[op]->IsFloatingPoint()) - RaiseFPUDisableException(); + throw SH4ThrownException(next_pc - 2, Sh4Ex_FpuDisabled); OpPtr[op](op); sh4cycles.executeCycles(op); } @@ -191,8 +191,6 @@ static void sh4_int_resetcache() { static void Sh4_int_Init() { - static_assert(sizeof(Sh4cntx) == 448, "Invalid Sh4Cntx size"); - memset(&p_sh4rcb->cntx, 0, sizeof(p_sh4rcb->cntx)); } diff --git a/core/hw/sh4/interpr/sh4_opcodes.cpp b/core/hw/sh4/interpr/sh4_opcodes.cpp index dc4b24db3..c315ebd5b 100644 --- a/core/hw/sh4/interpr/sh4_opcodes.cpp +++ b/core/hw/sh4/interpr/sh4_opcodes.cpp @@ -1154,7 +1154,7 @@ sh4op(i0000_nnnn_1000_0011) if (CCN_MMUCR.AT) do_sqw_mmu(Dest); else - do_sqw_nommu(Dest, sq_both); + do_sqw_nommu(Dest, p_sh4rcb->cntx.sq_buffer); } else { diff --git a/core/hw/sh4/sh4_core.h b/core/hw/sh4/sh4_core.h index ab0571d10..cf8daf234 100644 --- a/core/hw/sh4/sh4_core.h +++ b/core/hw/sh4/sh4_core.h @@ -40,11 +40,6 @@ struct SH4ThrownException Sh4ExceptionCode expEvn; }; -static inline void RaiseFPUDisableException() -{ - throw SH4ThrownException(next_pc - 2, Sh4Ex_FpuDisabled); -} - static inline void AdjustDelaySlotException(SH4ThrownException& ex) { ex.epc -= 2; diff --git a/core/hw/sh4/sh4_if.h b/core/hw/sh4/sh4_if.h index a71425286..f7df490f3 100644 --- a/core/hw/sh4/sh4_if.h +++ b/core/hw/sh4/sh4_if.h @@ -115,12 +115,18 @@ struct sh4_if extern sh4_if sh4_cpu; +struct alignas(32) SQBuffer { + u8 data[32]; +}; + struct alignas(64) Sh4Context { union { struct { + SQBuffer sq_buffer[2]; + f32 xffr[32]; u32 r[16]; @@ -155,7 +161,7 @@ struct alignas(64) Sh4Context u32 temp_reg; int cycle_counter; }; - u64 raw[64-8]; + u64 raw[64]; }; f32& fr(int idx) { @@ -205,11 +211,7 @@ struct alignas(64) Sh4Context f32 sgl[2]; }; }; -static_assert(sizeof(Sh4Context) == 448, "Invalid Sh4Context size"); - -struct alignas(32) SQBuffer { - u8 data[32]; -}; +static_assert(sizeof(Sh4Context) == 512, "Invalid Sh4Context size"); void setSqwHandler(); void DYNACALL do_sqw_mmu(u32 dst); @@ -229,9 +231,8 @@ typedef void DYNACALL sqw_fp(u32 dst, const SQBuffer *sqb); struct alignas(PAGE_SIZE) Sh4RCB { void* fpcb[FPCB_SIZE]; - u8 _pad[FPCB_PAD - sizeof(Sh4Context) - sizeof(SQBuffer) * 2 - sizeof(void *)]; + u8 _pad[FPCB_PAD - sizeof(Sh4Context) - sizeof(void *)]; sqw_fp* do_sqw_nommu; - SQBuffer sq_buffer[2]; Sh4Context cntx; }; diff --git a/core/hw/sh4/sh4_mmr.cpp b/core/hw/sh4/sh4_mmr.cpp index 5372003a0..5d60d9406 100644 --- a/core/hw/sh4/sh4_mmr.cpp +++ b/core/hw/sh4/sh4_mmr.cpp @@ -615,7 +615,7 @@ void sh4_mmr_reset(bool hard) ubc.reset(); MMU_reset(); - memset(p_sh4rcb->sq_buffer, 0, sizeof(p_sh4rcb->sq_buffer)); + memset(p_sh4rcb->cntx.sq_buffer, 0, sizeof(p_sh4rcb->cntx.sq_buffer)); } void sh4_mmr_term() @@ -646,10 +646,10 @@ void map_area7() void map_p4() { // Store Queues -- Write only 32bit - addrspace::mapBlock(p_sh4rcb->sq_buffer, 0xE0, 0xE0, 63); - addrspace::mapBlock(p_sh4rcb->sq_buffer, 0xE1, 0xE1, 63); - addrspace::mapBlock(p_sh4rcb->sq_buffer, 0xE2, 0xE2, 63); - addrspace::mapBlock(p_sh4rcb->sq_buffer, 0xE3, 0xE3, 63); + addrspace::mapBlock(p_sh4rcb->cntx.sq_buffer, 0xE0, 0xE0, 63); + addrspace::mapBlock(p_sh4rcb->cntx.sq_buffer, 0xE1, 0xE1, 63); + addrspace::mapBlock(p_sh4rcb->cntx.sq_buffer, 0xE2, 0xE2, 63); + addrspace::mapBlock(p_sh4rcb->cntx.sq_buffer, 0xE3, 0xE3, 63); // sh4 IC, OC and TLB arrays addrspace::handler p4arrays_handler = addrspaceRegisterHandlerTemplate(ReadMem_P4, WriteMem_P4); @@ -685,8 +685,6 @@ void serialize(Serializer& ser) interrupts_serialize(ser); - ser << (*p_sh4rcb).sq_buffer; - ser << (*p_sh4rcb).cntx; sh4_sched_serialize(ser); @@ -721,8 +719,6 @@ void deserialize(Deserializer& deser) CCN_QACR_write<0>(0, CCN_QACR0.reg_data); CCN_QACR_write<1>(0, CCN_QACR1.reg_data); - deser >> (*p_sh4rcb).sq_buffer; - deser >> (*p_sh4rcb).cntx; if (deser.version() >= Deserializer::V19 && deser.version() < Deserializer::V21) deser.skip(); // sh4InterpCycles diff --git a/core/hw/sh4/sh4_mmr.h b/core/hw/sh4/sh4_mmr.h index 04bd7859a..e9967722d 100644 --- a/core/hw/sh4/sh4_mmr.h +++ b/core/hw/sh4/sh4_mmr.h @@ -9,8 +9,6 @@ void map_area7(); void map_p4(); -#define sq_both (sh4rcb.sq_buffer) - void sh4_mmr_init(); void sh4_mmr_reset(bool hard); void sh4_mmr_term(); diff --git a/core/hw/sh4/storeq.cpp b/core/hw/sh4/storeq.cpp index 5a8f9f0ba..885d556f5 100644 --- a/core/hw/sh4/storeq.cpp +++ b/core/hw/sh4/storeq.cpp @@ -53,7 +53,7 @@ void DYNACALL do_sqw(u32 Dest, const SQBuffer *sqb) } void DYNACALL do_sqw_mmu(u32 dst) { - do_sqw(dst, sq_both); + do_sqw(dst, p_sh4rcb->cntx.sq_buffer); } static void DYNACALL do_sqw_simplemmu(u32 dst, const SQBuffer *sqb) { @@ -63,7 +63,7 @@ static void DYNACALL do_sqw_simplemmu(u32 dst, const SQBuffer *sqb) { //yes, this micro optimization makes a difference static void DYNACALL do_sqw_nommu_area_3(u32 dst, const SQBuffer *sqb) { - SQBuffer *pmem = (SQBuffer *)((u8 *)sqb + sizeof(Sh4RCB::sq_buffer) + sizeof(Sh4RCB::cntx) + 0x0C000000); + SQBuffer *pmem = (SQBuffer *)((u8 *)sqb + sizeof(Sh4RCB::cntx) + 0x0C000000); pmem += (dst & (RAM_SIZE_MAX - 1)) >> 5; *pmem = sqb[(dst >> 5) & 1]; } diff --git a/core/rec-ARM/rec_arm.cpp b/core/rec-ARM/rec_arm.cpp index b020039e7..dc52dfe73 100644 --- a/core/rec-ARM/rec_arm.cpp +++ b/core/rec-ARM/rec_arm.cpp @@ -1806,7 +1806,7 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op else { Ldr(r2, MemOperand(r8, rcbOffset(do_sqw_nommu))); - Sub(r1, r8, -rcbOffset(sq_buffer)); + Sub(r1, r8, -getRegOffset(reg_sq_buffer) + sizeof(Sh4Context)); Blx(cc, r2); } } @@ -2513,7 +2513,7 @@ void Arm32Assembler::genMainLoop() And(r1, r0, 0x3F); Add(r1, r1, r8); jump((void *)&addrspace::write64, ne); - Strd(r2, r3, MemOperand(r1, rcbOffset(sq_buffer))); + Strd(r2, r3, MemOperand(r1, getRegOffset(reg_sq_buffer) - sizeof(Sh4Context))); } else { @@ -2524,7 +2524,7 @@ void Arm32Assembler::genMainLoop() if (reg != 0) Mov(ne, r0, Register(reg)); jump((void *)&addrspace::write32, ne); - Str(r1, MemOperand(r3, rcbOffset(sq_buffer))); + Str(r1, MemOperand(r3, getRegOffset(reg_sq_buffer) - sizeof(Sh4Context))); } Bx(lr); } diff --git a/core/rec-ARM64/rec_arm64.cpp b/core/rec-ARM64/rec_arm64.cpp index 485a21c9a..7522fa7ae 100644 --- a/core/rec-ARM64/rec_arm64.cpp +++ b/core/rec-ARM64/rec_arm64.cpp @@ -798,7 +798,7 @@ class Arm64Assembler : public MacroAssembler { Sub(x9, x28, offsetof(Sh4RCB, cntx) - offsetof(Sh4RCB, do_sqw_nommu)); Ldr(x9, MemOperand(x9)); - Sub(x1, x28, offsetof(Sh4RCB, cntx) - offsetof(Sh4RCB, sq_buffer)); + Add(x1, x28, getRegOffset(reg_sq_buffer)); Blr(x9); } Bind(¬_sqw); @@ -1557,7 +1557,7 @@ class Arm64Assembler : public MacroAssembler Cmp(x7, 0x38); GenBranchRuntime(addrspace::write32, Condition::ne); And(x0, x0, 0x3f); - Sub(x7, x0, sizeof(Sh4RCB::sq_buffer), LeaveFlags); + Sub(x7, x0, sizeof(Sh4Context::sq_buffer), LeaveFlags); Str(w1, MemOperand(x28, x7)); Ret(); @@ -1567,7 +1567,7 @@ class Arm64Assembler : public MacroAssembler Cmp(x7, 0x38); GenBranchRuntime(addrspace::write64, Condition::ne); And(x0, x0, 0x3f); - Sub(x7, x0, sizeof(Sh4RCB::sq_buffer), LeaveFlags); + Sub(x7, x0, sizeof(Sh4Context::sq_buffer), LeaveFlags); Str(x1, MemOperand(x28, x7)); Ret(); diff --git a/core/rec-x64/rec_x64.cpp b/core/rec-x64/rec_x64.cpp index bbad35e0d..4b8a9573f 100644 --- a/core/rec-x64/rec_x64.cpp +++ b/core/rec-x64/rec_x64.cpp @@ -378,7 +378,7 @@ class BlockCompiler : public BaseXbyakRec } else { - mov(call_regs64[1], (uintptr_t)sq_both); + mov(call_regs64[1], (uintptr_t)p_sh4rcb->cntx.sq_buffer); mov(rax, (size_t)&do_sqw_nommu); saveXmmRegisters(); call(qword[rax]); @@ -1160,7 +1160,7 @@ class BlockCompiler : public BaseXbyakRec shr(r9d, 26); cmp(r9d, 0x38); jne(no_sqw); - mov(rax, (uintptr_t)p_sh4rcb->sq_buffer); + mov(rax, (uintptr_t)p_sh4rcb->cntx.sq_buffer); and_(call_regs[0], 0x3F); if (size == MemSize::S32) diff --git a/core/rec-x86/x86_ops.cpp b/core/rec-x86/x86_ops.cpp index b6bfc2576..8605ee1b4 100644 --- a/core/rec-x86/x86_ops.cpp +++ b/core/rec-x86/x86_ops.cpp @@ -139,12 +139,12 @@ void X86Compiler::genMemHandlers() and_(ecx, 0x3F); if (size == MemSize::S32) - mov(dword[(size_t)p_sh4rcb->sq_buffer + ecx], edx); + mov(dword[(size_t)p_sh4rcb->cntx.sq_buffer + ecx], edx); else if (size >= MemSize::F32) { - movss(dword[(size_t)p_sh4rcb->sq_buffer + ecx], xmm0); + movss(dword[(size_t)p_sh4rcb->cntx.sq_buffer + ecx], xmm0); if (size == MemSize::F64) - movss(dword[((size_t)p_sh4rcb->sq_buffer + 4) + ecx], xmm1); + movss(dword[((size_t)p_sh4rcb->cntx.sq_buffer + 4) + ecx], xmm1); } ret(); L(no_sqw); @@ -526,7 +526,7 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& } else { - mov(edx, (size_t)sh4rcb.sq_buffer); + mov(edx, (size_t)sh4rcb.cntx.sq_buffer); freezeXMM(); call(dword[&do_sqw_nommu]); thawXMM(); From 52df0133f0138bf46e374e2e0ad05ceddb49da3e Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 6 Nov 2024 23:27:20 +0100 Subject: [PATCH 12/81] sh4: pass sh4 context to sq write handlers --- core/hw/sh4/dyna/shil_canonical.h | 2 +- core/hw/sh4/interpr/sh4_opcodes.cpp | 2 +- core/hw/sh4/sh4_if.h | 4 ++-- core/hw/sh4/storeq.cpp | 33 +++++++++++++++++------------ core/rec-ARM/rec_arm.cpp | 2 +- core/rec-ARM64/rec_arm64.cpp | 2 +- core/rec-x64/rec_x64.cpp | 2 +- core/rec-x86/x86_ops.cpp | 2 +- 8 files changed, 27 insertions(+), 22 deletions(-) diff --git a/core/hw/sh4/dyna/shil_canonical.h b/core/hw/sh4/dyna/shil_canonical.h index 0828675f3..c42397727 100644 --- a/core/hw/sh4/dyna/shil_canonical.h +++ b/core/hw/sh4/dyna/shil_canonical.h @@ -802,7 +802,7 @@ void,f1,(u32 r1), shil_canonical ( void,f2,(u32 r1), - if ((r1>>26) == 0x38) do_sqw_nommu(r1, p_sh4rcb->cntx.sq_buffer); + if ((r1>>26) == 0x38) do_sqw_nommu(r1, &p_sh4rcb->cntx); ) shil_compile diff --git a/core/hw/sh4/interpr/sh4_opcodes.cpp b/core/hw/sh4/interpr/sh4_opcodes.cpp index c315ebd5b..97b71fd81 100644 --- a/core/hw/sh4/interpr/sh4_opcodes.cpp +++ b/core/hw/sh4/interpr/sh4_opcodes.cpp @@ -1154,7 +1154,7 @@ sh4op(i0000_nnnn_1000_0011) if (CCN_MMUCR.AT) do_sqw_mmu(Dest); else - do_sqw_nommu(Dest, p_sh4rcb->cntx.sq_buffer); + do_sqw_nommu(Dest, &p_sh4rcb->cntx); } else { diff --git a/core/hw/sh4/sh4_if.h b/core/hw/sh4/sh4_if.h index f7df490f3..a564c1543 100644 --- a/core/hw/sh4/sh4_if.h +++ b/core/hw/sh4/sh4_if.h @@ -216,7 +216,7 @@ static_assert(sizeof(Sh4Context) == 512, "Invalid Sh4Context size"); void setSqwHandler(); void DYNACALL do_sqw_mmu(u32 dst); -typedef void DYNACALL sqw_fp(u32 dst, const SQBuffer *sqb); +typedef void DYNACALL SQWriteFunc(u32 dst, Sh4Context *ctx); #define FPCB_SIZE (RAM_SIZE_MAX/2) #define FPCB_MASK (FPCB_SIZE -1) @@ -232,7 +232,7 @@ struct alignas(PAGE_SIZE) Sh4RCB { void* fpcb[FPCB_SIZE]; u8 _pad[FPCB_PAD - sizeof(Sh4Context) - sizeof(void *)]; - sqw_fp* do_sqw_nommu; + SQWriteFunc *do_sqw_nommu; Sh4Context cntx; }; diff --git a/core/hw/sh4/storeq.cpp b/core/hw/sh4/storeq.cpp index 885d556f5..126ffc709 100644 --- a/core/hw/sh4/storeq.cpp +++ b/core/hw/sh4/storeq.cpp @@ -22,7 +22,7 @@ static u32 CCN_QACR_TR[2]; template -void DYNACALL do_sqw(u32 Dest, const SQBuffer *sqb) +void DYNACALL do_sqw(u32 Dest, Sh4Context *ctx) { u32 Address; @@ -43,40 +43,45 @@ void DYNACALL do_sqw(u32 Dest, const SQBuffer *sqb) if (((Address >> 26) & 7) != 4)//Area 4 { - const SQBuffer *sq = &sqb[(Dest >> 5) & 1]; + const SQBuffer *sq = &ctx->sq_buffer[(Dest >> 5) & 1]; WriteMemBlock_nommu_sq(Address, sq); } else { - TAWriteSQ(Address, sqb); + TAWriteSQ(Address, ctx->sq_buffer); // TODO pass the correct SQBuffer instead of letting TAWriteSQ deal with it } } void DYNACALL do_sqw_mmu(u32 dst) { - do_sqw(dst, p_sh4rcb->cntx.sq_buffer); + do_sqw(dst, &p_sh4rcb->cntx); } -static void DYNACALL do_sqw_simplemmu(u32 dst, const SQBuffer *sqb) { - do_sqw(dst, sqb); +static void DYNACALL do_sqw_simplemmu(u32 dst, Sh4Context *ctx) { + do_sqw(dst, ctx); } //yes, this micro optimization makes a difference -static void DYNACALL do_sqw_nommu_area_3(u32 dst, const SQBuffer *sqb) +static void DYNACALL do_sqw_nommu_area_3(u32 dst, Sh4Context *ctx) { - SQBuffer *pmem = (SQBuffer *)((u8 *)sqb + sizeof(Sh4RCB::cntx) + 0x0C000000); + SQBuffer *pmem = (SQBuffer *)((u8 *)ctx + sizeof(Sh4Context) + 0x0C000000); pmem += (dst & (RAM_SIZE_MAX - 1)) >> 5; - *pmem = sqb[(dst >> 5) & 1]; + *pmem = ctx->sq_buffer[(dst >> 5) & 1]; } -static void DYNACALL do_sqw_nommu_area_3_nonvmem(u32 dst, const SQBuffer *sqb) +static void DYNACALL do_sqw_nommu_area_3_nonvmem(u32 dst, Sh4Context *ctx) { u8* pmem = &mem_b[0]; - memcpy((SQBuffer *)&pmem[dst & (RAM_MASK - 0x1F)], &sqb[(dst >> 5) & 1], sizeof(SQBuffer)); + memcpy((SQBuffer *)&pmem[dst & (RAM_MASK - 0x1F)], &ctx->sq_buffer[(dst >> 5) & 1], sizeof(SQBuffer)); } -static void DYNACALL do_sqw_nommu_full(u32 dst, const SQBuffer *sqb) { - do_sqw(dst, sqb); +static void DYNACALL do_sqw_nommu_full(u32 dst, Sh4Context *ctx) { + do_sqw(dst, ctx); +} + +static void DYNACALL sqWriteTA(u32 dst, Sh4Context *ctx) +{ + TAWriteSQ(dst, ctx->sq_buffer); } void setSqwHandler() @@ -101,7 +106,7 @@ void setSqwHandler() break; case 4: - do_sqw_nommu = &TAWriteSQ; + do_sqw_nommu = &sqWriteTA; break; default: diff --git a/core/rec-ARM/rec_arm.cpp b/core/rec-ARM/rec_arm.cpp index dc52dfe73..dc6244fb9 100644 --- a/core/rec-ARM/rec_arm.cpp +++ b/core/rec-ARM/rec_arm.cpp @@ -1806,7 +1806,7 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op else { Ldr(r2, MemOperand(r8, rcbOffset(do_sqw_nommu))); - Sub(r1, r8, -getRegOffset(reg_sq_buffer) + sizeof(Sh4Context)); + Sub(r1, r8, sizeof(Sh4Context)); Blx(cc, r2); } } diff --git a/core/rec-ARM64/rec_arm64.cpp b/core/rec-ARM64/rec_arm64.cpp index 7522fa7ae..4602137cc 100644 --- a/core/rec-ARM64/rec_arm64.cpp +++ b/core/rec-ARM64/rec_arm64.cpp @@ -798,7 +798,7 @@ class Arm64Assembler : public MacroAssembler { Sub(x9, x28, offsetof(Sh4RCB, cntx) - offsetof(Sh4RCB, do_sqw_nommu)); Ldr(x9, MemOperand(x9)); - Add(x1, x28, getRegOffset(reg_sq_buffer)); + Mov(x1, x28); Blr(x9); } Bind(¬_sqw); diff --git a/core/rec-x64/rec_x64.cpp b/core/rec-x64/rec_x64.cpp index 4b8a9573f..fa7d4e8a6 100644 --- a/core/rec-x64/rec_x64.cpp +++ b/core/rec-x64/rec_x64.cpp @@ -378,7 +378,7 @@ class BlockCompiler : public BaseXbyakRec } else { - mov(call_regs64[1], (uintptr_t)p_sh4rcb->cntx.sq_buffer); + mov(call_regs64[1], (uintptr_t)&p_sh4rcb->cntx); mov(rax, (size_t)&do_sqw_nommu); saveXmmRegisters(); call(qword[rax]); diff --git a/core/rec-x86/x86_ops.cpp b/core/rec-x86/x86_ops.cpp index 8605ee1b4..26b7ed2f0 100644 --- a/core/rec-x86/x86_ops.cpp +++ b/core/rec-x86/x86_ops.cpp @@ -526,7 +526,7 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& } else { - mov(edx, (size_t)sh4rcb.cntx.sq_buffer); + mov(edx, (size_t)&sh4rcb.cntx); freezeXMM(); call(dword[&do_sqw_nommu]); thawXMM(); From 93ae9d03757006f0989209b2884ed9088b973be9 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 7 Nov 2024 18:14:18 +0100 Subject: [PATCH 13/81] sh4: refactor interpreter and recompiler with Sh4Executor interface --- core/emulator.cpp | 81 +++++++++--------- core/emulator.h | 14 +++- core/hw/mem/addrspace.cpp | 2 - core/hw/pvr/Renderer_if.cpp | 4 +- core/hw/sh4/dyna/driver.cpp | 67 +++++---------- core/hw/sh4/dyna/ngen.h | 23 ++++++ core/hw/sh4/interpr/sh4_interpreter.cpp | 105 ++++++++++-------------- core/hw/sh4/interpr/sh4_opcodes.cpp | 25 +++--- core/hw/sh4/modules/ccn.cpp | 3 +- core/hw/sh4/sh4_cache.h | 2 + core/hw/sh4/sh4_core.h | 1 - core/hw/sh4/sh4_core_regs.cpp | 1 - core/hw/sh4/sh4_cycles.cpp | 68 +++++++++++++++ core/hw/sh4/sh4_cycles.h | 70 +--------------- core/hw/sh4/sh4_if.h | 29 +++---- core/hw/sh4/sh4_interpreter.h | 64 +++++++-------- core/hw/sh4/sh4_opcode_list.h | 33 +++++++- core/hw/sh4/sh4_sched.h | 8 +- core/network/ggpo.cpp | 4 +- core/nullDC.cpp | 2 +- shell/libretro/libretro.cpp | 2 +- tests/src/AicaArmTest.cpp | 2 +- tests/src/CheatManagerTest.cpp | 2 +- tests/src/MmuTest.cpp | 2 +- tests/src/Sh4InterpreterTest.cpp | 6 +- tests/src/div32_test.cpp | 2 +- tests/src/serialize_test.cpp | 2 +- tests/src/sh4_ops.h | 2 +- 28 files changed, 323 insertions(+), 303 deletions(-) diff --git a/core/emulator.cpp b/core/emulator.cpp index 82ddfca1d..1ef865b39 100644 --- a/core/emulator.cpp +++ b/core/emulator.cpp @@ -47,6 +47,8 @@ #ifndef LIBRETRO #include "ui/gui.h" #endif +#include "hw/sh4/sh4_interpreter.h" +#include "hw/sh4/dyna/ngen.h" settings_t settings; @@ -400,7 +402,7 @@ static void loadSpecialSettings() } } -void dc_reset(bool hard) +void Emulator::dc_reset(bool hard) { if (hard) { @@ -411,7 +413,7 @@ void dc_reset(bool hard) sh4_sched_reset(hard); pvr::reset(hard); aica::reset(hard); - sh4_cpu.Reset(true); + getSh4Executor()->Reset(true); mem_Reset(hard); } @@ -487,22 +489,28 @@ void Emulator::init() // the recompiler may start generating code at this point and needs a fully configured machine #if FEAT_SHREC != DYNAREC_NONE - Get_Sh4Recompiler(&sh4_cpu); - sh4_cpu.Init(); // Also initialize the interpreter + recompiler = Get_Sh4Recompiler(); + recompiler->Init(); if(config::DynarecEnabled) - { INFO_LOG(DYNAREC, "Using Recompiler"); - } else #endif - { - Get_Sh4Interpreter(&sh4_cpu); - sh4_cpu.Init(); INFO_LOG(INTERPRETER, "Using Interpreter"); - } + interpreter = Get_Sh4Interpreter(); + interpreter->Init(); state = Init; } +Sh4Executor *Emulator::getSh4Executor() +{ +#if FEAT_SHREC != DYNAREC_NONE + if(config::DynarecEnabled) + return recompiler; + else +#endif + return interpreter; +} + int getGamePlatform(const std::string& filename) { if (settings.naomi.slave) @@ -671,13 +679,13 @@ void Emulator::runInternal() { if (singleStep) { - sh4_cpu.Step(); + getSh4Executor()->Step(); singleStep = false; } else if(stepRangeTo != 0) { while (Sh4cntx.pc >= stepRangeFrom && Sh4cntx.pc <= stepRangeTo) - sh4_cpu.Step(); + getSh4Executor()->Step(); stepRangeFrom = 0; stepRangeTo = 0; @@ -687,7 +695,7 @@ void Emulator::runInternal() do { resetRequested = false; - sh4_cpu.Run(); + getSh4Executor()->Run(); if (resetRequested) { @@ -736,7 +744,18 @@ void Emulator::term() if (state == Init) { debugger::term(); - sh4_cpu.Term(); + if (interpreter != nullptr) + { + interpreter->Term(); + delete interpreter; + interpreter = nullptr; + } + if (recompiler != nullptr) + { + recompiler->Term(); + delete recompiler; + recompiler = nullptr; + } custom_texture.Terminate(); // lr: avoid deadlock on exit (win32) reios_term(); aica::term(); @@ -760,7 +779,7 @@ void Emulator::stop() const std::lock_guard _(mutex); // must be updated after GGPO is stopped since it may run some rollback frames state = Loaded; - sh4_cpu.Stop(); + getSh4Executor()->Stop(); } if (config::ThreadedRendering) { @@ -794,7 +813,7 @@ void Emulator::requestReset() resetRequested = true; if (config::GGPOEnable) NetworkHandshake::term(); - sh4_cpu.Stop(); + getSh4Executor()->Stop(); } void loadGameSpecificSettings() @@ -841,7 +860,7 @@ void Emulator::stepRange(u32 from, u32 to) stop(); } -void dc_loadstate(Deserializer& deser) +void Emulator::loadstate(Deserializer& deser) { custom_texture.Terminate(); #if FEAT_AREC == DYNAREC_JIT @@ -857,7 +876,7 @@ void dc_loadstate(Deserializer& deser) dc_deserialize(deser); mmu_set_state(); - sh4_cpu.ResetCache(); + getSh4Executor()->ResetCache(); KillTex = true; } @@ -871,7 +890,7 @@ void Emulator::setNetworkState(bool online) && config::Sh4Clock != 200) { config::Sh4Clock.override(200); - sh4_cpu.ResetCache(); + getSh4Executor()->ResetCache(); } EventManager::event(Event::Network); } @@ -906,7 +925,7 @@ void Emulator::run() startTime = sh4_sched_now64(); renderTimeout = false; if (!singleStep && stepRangeTo == 0) - sh4_cpu.Start(); + getSh4Executor()->Start(); try { runInternal(); if (ggpo::active()) @@ -914,7 +933,7 @@ void Emulator::run() } catch (...) { setNetworkState(false); state = Error; - sh4_cpu.Stop(); + getSh4Executor()->Stop(); EventManager::event(Event::Pause); throw; } @@ -930,18 +949,6 @@ void Emulator::start() if (config::GGPOEnable && config::ThreadedRendering) // Not supported with GGPO config::EmulateFramebuffer.override(false); -#if FEAT_SHREC != DYNAREC_NONE - if (config::DynarecEnabled) - { - Get_Sh4Recompiler(&sh4_cpu); - INFO_LOG(DYNAREC, "Using Recompiler"); - } - else -#endif - { - Get_Sh4Interpreter(&sh4_cpu); - INFO_LOG(DYNAREC, "Using Interpreter"); - } setupPtyPipe(); memwatch::protect(); @@ -949,7 +956,7 @@ void Emulator::start() if (config::ThreadedRendering) { const std::lock_guard lock(mutex); - sh4_cpu.Start(); + getSh4Executor()->Start(); threadResult = std::async(std::launch::async, [this] { ThreadName _("Flycast-emu"); InitAudio(); @@ -966,7 +973,7 @@ void Emulator::start() TermAudio(); } catch (...) { setNetworkState(false); - sh4_cpu.Stop(); + getSh4Executor()->Stop(); TermAudio(); throw; } @@ -1044,7 +1051,7 @@ void Emulator::vblank() if (ggpo::active()) ggpo::endOfFrame(); else if (!config::ThreadedRendering) - sh4_cpu.Stop(); + getSh4Executor()->Stop(); } bool Emulator::restartCpu() @@ -1052,7 +1059,7 @@ bool Emulator::restartCpu() const std::lock_guard _(mutex); if (state != Running) return false; - sh4_cpu.Start(); + getSh4Executor()->Start(); return true; } diff --git a/core/emulator.h b/core/emulator.h index 354a491db..945fc193d 100644 --- a/core/emulator.h +++ b/core/emulator.h @@ -33,12 +33,10 @@ void loadGameSpecificSettings(); void SaveSettings(); int flycast_init(int argc, char* argv[]); -void dc_reset(bool hard); // for tests only void flycast_term(); void dc_exit(); void dc_savestate(int index = 0, const u8 *pngData = nullptr, u32 pngSize = 0); void dc_loadstate(int index = 0); -void dc_loadstate(Deserializer& deser); time_t dc_getStateCreationDate(int index); void dc_getStateScreenshot(int index, std::vector& pngData); @@ -98,6 +96,8 @@ struct LoadProgress std::atomic progress; }; +class Sh4Executor; + class Emulator { public: @@ -170,6 +170,14 @@ class Emulator * Returns true if the cpu was started */ bool restartCpu(); + /* + * Load the machine state from the passed deserializer + */ + void loadstate(Deserializer& deser); + + Sh4Executor *getSh4Executor(); + + void dc_reset(bool hard); // for tests only private: bool checkStatus(bool wait = false); @@ -193,6 +201,8 @@ class Emulator u32 stepRangeTo = 0; bool stopRequested = false; std::mutex mutex; + Sh4Executor *interpreter = nullptr; + Sh4Executor *recompiler = nullptr; }; extern Emulator emu; diff --git a/core/hw/mem/addrspace.cpp b/core/hw/mem/addrspace.cpp index 391f48834..a0e8f1810 100644 --- a/core/hw/mem/addrspace.cpp +++ b/core/hw/mem/addrspace.cpp @@ -380,8 +380,6 @@ bool bm_lockedWrite(u8* address) bool reserve() { - static_assert((sizeof(Sh4RCB) % PAGE_SIZE) == 0, "sizeof(Sh4RCB) not multiple of PAGE_SIZE"); - if (ram_base != nullptr) return true; diff --git a/core/hw/pvr/Renderer_if.cpp b/core/hw/pvr/Renderer_if.cpp index 86ce77c7d..55289b3ac 100644 --- a/core/hw/pvr/Renderer_if.cpp +++ b/core/hw/pvr/Renderer_if.cpp @@ -237,7 +237,7 @@ class PvrMessageQueue { presented = true; if (!config::ThreadedRendering && !ggpo::active()) - sh4_cpu.Stop(); + emu.getSh4Executor()->Stop(); #ifdef LIBRETRO retro_rend_present(); #endif @@ -321,7 +321,7 @@ bool rend_init_renderer() rendererEnabled = true; if (renderer == nullptr) rend_create_renderer(); - bool success = renderer->Init(); + bool success = renderer != nullptr && renderer->Init(); if (!success) { delete renderer; renderer = rend_norend(); diff --git a/core/hw/sh4/dyna/driver.cpp b/core/hw/sh4/dyna/driver.cpp index c57e5eb36..32b8230b2 100644 --- a/core/hw/sh4/dyna/driver.cpp +++ b/core/hw/sh4/dyna/driver.cpp @@ -40,9 +40,9 @@ ptrdiff_t cc_rx_offset; static std::unordered_set smc_hotspots; -static sh4_if sh4Interp; static Sh4CodeBuffer codeBuffer; Sh4Dynarec *sh4Dynarec; +Sh4Recompiler *Sh4Recompiler::Instance; void *Sh4CodeBuffer::get() { @@ -83,14 +83,14 @@ void Sh4CodeBuffer::reset(bool temporary) lastAddr = 0; } -static void clear_temp_cache(bool full) +void Sh4Recompiler::clear_temp_cache(bool full) { //printf("recSh4:Temp Code Cache clear at %08X\n", curr_pc); codeBuffer.reset(true); bm_ResetTempCache(full); } -static void recSh4_ClearCache() +void Sh4Recompiler::ResetCache() { INFO_LOG(DYNAREC, "recSh4:Dynarec Cache clear at %08X free space %d", next_pc, codeBuffer.getFreeSpace()); codeBuffer.reset(false); @@ -99,7 +99,7 @@ static void recSh4_ClearCache() clear_temp_cache(true); } -static void recSh4_Run() +void Sh4Recompiler::Run() { RestoreHostRoundingMode(); @@ -108,7 +108,7 @@ static void recSh4_Run() sh4Dynarec->mainloop(sh4_dyna_rcb); - sh4_int_bCpuRun = false; + ctx->CpuRunning = false; } void AnalyseBlock(RuntimeBlockInfo* blk); @@ -171,7 +171,7 @@ DynarecCodeEntryPtr rdv_CompilePC(u32 blockcheck_failures) const u32 pc = next_pc; if (codeBuffer.getFreeSpace() < 32_KB || pc == 0x8c0000e0 || pc == 0xac010000 || pc == 0xac008300) - recSh4_ClearCache(); + Sh4Recompiler::Instance->ResetCache(); RuntimeBlockInfo* rbi = sh4Dynarec->allocateBlock(); @@ -185,7 +185,7 @@ DynarecCodeEntryPtr rdv_CompilePC(u32 blockcheck_failures) { codeBuffer.useTempBuffer(true); if (codeBuffer.getFreeSpace() < 32_KB) - clear_temp_cache(false); + Sh4Recompiler::Instance->clear_temp_cache(false); rbi->temp_block = true; if (rbi->read_only) INFO_LOG(DYNAREC, "WARNING: temp block %x (%x) is protected!", rbi->vaddr, rbi->addr); @@ -248,7 +248,7 @@ DynarecCodeEntryPtr DYNACALL rdv_BlockCheckFail(u32 addr) else { next_pc = addr; - recSh4_ClearCache(); + Sh4Recompiler::Instance->ResetCache(); } return (DynarecCodeEntryPtr)CC_RW2RX(rdv_CompilePC(blockcheck_failures)); } @@ -340,34 +340,18 @@ void* DYNACALL rdv_LinkBlock(u8* code,u32 dpc) return (void*)rv; } -static void recSh4_Start() +void Sh4Recompiler::Reset(bool hard) { - sh4Interp.Start(); -} - -static void recSh4_Stop() -{ - sh4Interp.Stop(); -} - -static void recSh4_Step() -{ - sh4Interp.Step(); -} - -static void recSh4_Reset(bool hard) -{ - sh4Interp.Reset(hard); - recSh4_ClearCache(); + super::Reset(hard); + ResetCache(); if (hard) bm_Reset(); } -static void recSh4_Init() +void Sh4Recompiler::Init() { - INFO_LOG(DYNAREC, "recSh4 Init"); - Get_Sh4Interpreter(&sh4Interp); - sh4Interp.Init(); + INFO_LOG(DYNAREC, "Sh4Recompiler::Init"); + super::Init(); bm_Init(); if (addrspace::virtmemEnabled()) @@ -389,9 +373,9 @@ static void recSh4_Init() bm_ResetCache(); } -static void recSh4_Term() +void Sh4Recompiler::Term() { - INFO_LOG(DYNAREC, "recSh4 Term"); + INFO_LOG(DYNAREC, "Sh4Recompiler::Term"); #ifdef FEAT_NO_RWX_PAGES if (CodeCache != nullptr) virtmem::release_jit_block(CodeCache, (u8 *)CodeCache + cc_rx_offset, FULL_SIZE); @@ -402,25 +386,12 @@ static void recSh4_Term() CodeCache = nullptr; TempCodeCache = nullptr; bm_Term(); - sh4Interp.Term(); -} - -static bool recSh4_IsCpuRunning() -{ - return sh4Interp.IsCpuRunning(); + super::Term(); } -void Get_Sh4Recompiler(sh4_if* cpu) +Sh4Executor *Get_Sh4Recompiler() { - cpu->Run = recSh4_Run; - cpu->Start = recSh4_Start; - cpu->Stop = recSh4_Stop; - cpu->Step = recSh4_Step; - cpu->Reset = recSh4_Reset; - cpu->Init = recSh4_Init; - cpu->Term = recSh4_Term; - cpu->IsCpuRunning = recSh4_IsCpuRunning; - cpu->ResetCache = recSh4_ClearCache; + return new Sh4Recompiler(); } static bool translateAddress(u32 addr, int size, u32 access, u32& outAddr, RuntimeBlockInfo* block) diff --git a/core/hw/sh4/dyna/ngen.h b/core/hw/sh4/dyna/ngen.h index 86b8ca7cb..88c8f1d25 100644 --- a/core/hw/sh4/dyna/ngen.h +++ b/core/hw/sh4/dyna/ngen.h @@ -3,6 +3,7 @@ #pragma once #include "blockmanager.h" #include "oslib/host_context.h" +#include "../sh4_interpreter.h" // When NO_RWX is enabled there's two address-spaces, one executable and // one writtable. The emitter and most of the code in rec-* will work with @@ -121,3 +122,25 @@ class Sh4Dynarec }; extern Sh4Dynarec *sh4Dynarec; + +class Sh4Recompiler : public Sh4Interpreter +{ + using super = Sh4Interpreter; + +public: + Sh4Recompiler() { + Instance = this; + } + ~Sh4Recompiler() { + Instance = nullptr; + } + void Run() override; + void ResetCache() override; + void Reset(bool hard) override; + void Init() override; + void Term() override; + + void clear_temp_cache(bool full); + + static Sh4Recompiler *Instance; +}; diff --git a/core/hw/sh4/interpr/sh4_interpreter.cpp b/core/hw/sh4/interpr/sh4_interpreter.cpp index 250d6ca9d..c5c5df60e 100644 --- a/core/hw/sh4/interpr/sh4_interpreter.cpp +++ b/core/hw/sh4/interpr/sh4_interpreter.cpp @@ -14,38 +14,30 @@ #include "debug/gdb_server.h" #include "../sh4_cycles.h" -// SH4 underclock factor when using the interpreter so that it's somewhat usable -#ifdef STRICT_MODE -constexpr int CPU_RATIO = 1; -#else -constexpr int CPU_RATIO = 8; -#endif - Sh4ICache icache; Sh4OCache ocache; -Sh4Cycles sh4cycles(CPU_RATIO); -static void ExecuteOpcode(u16 op) +void Sh4Interpreter::ExecuteOpcode(u16 op) { if (sr.FD == 1 && OpDesc[op]->IsFloatingPoint()) - throw SH4ThrownException(next_pc - 2, Sh4Ex_FpuDisabled); + throw SH4ThrownException(ctx->pc - 2, Sh4Ex_FpuDisabled); OpPtr[op](op); sh4cycles.executeCycles(op); } -static u16 ReadNexOp() +u16 Sh4Interpreter::ReadNexOp() { - if (!mmu_enabled() && (next_pc & 1)) + u32 addr = ctx->pc; + if (!mmu_enabled() && (addr & 1)) // address error - throw SH4ThrownException(next_pc, Sh4Ex_AddressErrorRead); + throw SH4ThrownException(addr, Sh4Ex_AddressErrorRead); - u32 addr = next_pc; - next_pc += 2; + ctx->pc = addr + 2; return IReadMem16(addr); } -static void Sh4_int_Run() +void Sh4Interpreter::Run() { RestoreHostRoundingMode(); @@ -58,34 +50,34 @@ static void Sh4_int_Run() u32 op = ReadNexOp(); ExecuteOpcode(op); - } while (p_sh4rcb->cntx.cycle_counter > 0); - p_sh4rcb->cntx.cycle_counter += SH4_TIMESLICE; + } while (ctx->cycle_counter > 0); + ctx->cycle_counter += SH4_TIMESLICE; UpdateSystem_INTC(); } catch (const SH4ThrownException& ex) { Do_Exception(ex.epc, ex.expEvn); // an exception requires the instruction pipeline to drain, so approx 5 cycles sh4cycles.addCycles(5 * CPU_RATIO); } - } while (sh4_int_bCpuRun); + } while (ctx->CpuRunning); } catch (const debugger::Stop&) { } - sh4_int_bCpuRun = false; + ctx->CpuRunning = false; } -static void Sh4_int_Start() +void Sh4Interpreter::Start() { - sh4_int_bCpuRun = true; + ctx->CpuRunning = true; } -static void Sh4_int_Stop() +void Sh4Interpreter::Stop() { - sh4_int_bCpuRun = false; + ctx->CpuRunning = false; } -static void Sh4_int_Step() +void Sh4Interpreter::Step() { - verify(!sh4_int_bCpuRun); + verify(!ctx->CpuRunning); RestoreHostRoundingMode(); try { @@ -99,26 +91,26 @@ static void Sh4_int_Step() } } -static void Sh4_int_Reset(bool hard) +void Sh4Interpreter::Reset(bool hard) { - verify(!sh4_int_bCpuRun); + verify(!ctx->CpuRunning); if (hard) { - int schedNext = p_sh4rcb->cntx.sh4_sched_next; - memset(&p_sh4rcb->cntx, 0, sizeof(p_sh4rcb->cntx)); - p_sh4rcb->cntx.sh4_sched_next = schedNext; + int schedNext = ctx->sh4_sched_next; + memset(ctx, 0, sizeof(*ctx)); + ctx->sh4_sched_next = schedNext; } - next_pc = 0xA0000000; + ctx->pc = 0xA0000000; - memset(r,0,sizeof(r)); - memset(r_bank,0,sizeof(r_bank)); + memset(r, 0, sizeof(r)); + memset(r_bank, 0, sizeof(r_bank)); - gbr=ssr=spc=sgr=dbr=vbr=0; - mac.full=pr=fpul=0; + gbr = ssr = spc = sgr = dbr = vbr = 0; + mac.full = pr = fpul = 0; sr.setFull(0x700000F0); - old_sr.status=sr.status; + old_sr.status = sr.status; UpdateSR(); fpscr.full = 0x00040001; @@ -127,18 +119,18 @@ static void Sh4_int_Reset(bool hard) icache.Reset(hard); ocache.Reset(hard); sh4cycles.reset(); - p_sh4rcb->cntx.cycle_counter = SH4_TIMESLICE; + ctx->cycle_counter = SH4_TIMESLICE; INFO_LOG(INTERPRETER, "Sh4 Reset"); } -static bool Sh4_int_IsCpuRunning() +bool Sh4Interpreter::IsCpuRunning() { - return sh4_int_bCpuRun; + return ctx->CpuRunning; } //TODO : Check for valid delayslot instruction -void ExecuteDelayslot() +void Sh4Interpreter::ExecuteDelayslot() { try { u32 op = ReadNexOp(); @@ -148,12 +140,12 @@ void ExecuteDelayslot() AdjustDelaySlotException(ex); throw ex; } catch (const debugger::Stop& e) { - next_pc -= 2; // break on previous instruction + ctx->pc -= 2; // break on previous instruction throw e; } } -void ExecuteDelayslot_RTE() +void Sh4Interpreter::ExecuteDelayslot_RTE() { try { // In an RTE delay slot, status register (SR) bits are referenced as follows. @@ -169,7 +161,7 @@ void ExecuteDelayslot_RTE() } catch (const SH4ThrownException&) { throw FlycastException("Fatal: SH4 exception in RTE delay slot"); } catch (const debugger::Stop& e) { - next_pc -= 2; // break on previous instruction + ctx->pc -= 2; // break on previous instruction throw e; } } @@ -186,30 +178,19 @@ int UpdateSystem_INTC() return 0; } -static void sh4_int_resetcache() { -} - -static void Sh4_int_Init() +void Sh4Interpreter::Init() { - memset(&p_sh4rcb->cntx, 0, sizeof(p_sh4rcb->cntx)); + ctx = &p_sh4rcb->cntx; + memset(ctx, 0, sizeof(*ctx)); } -static void Sh4_int_Term() +void Sh4Interpreter::Term() { - Sh4_int_Stop(); + Stop(); INFO_LOG(INTERPRETER, "Sh4 Term"); } -void Get_Sh4Interpreter(sh4_if* cpu) +Sh4Executor *Get_Sh4Interpreter() { - cpu->Start = Sh4_int_Start; - cpu->Run = Sh4_int_Run; - cpu->Stop = Sh4_int_Stop; - cpu->Step = Sh4_int_Step; - cpu->Reset = Sh4_int_Reset; - cpu->Init = Sh4_int_Init; - cpu->Term = Sh4_int_Term; - cpu->IsCpuRunning = Sh4_int_IsCpuRunning; - - cpu->ResetCache = sh4_int_resetcache; + return new Sh4Interpreter(); } diff --git a/core/hw/sh4/interpr/sh4_opcodes.cpp b/core/hw/sh4/interpr/sh4_opcodes.cpp index 97b71fd81..60713c51e 100644 --- a/core/hw/sh4/interpr/sh4_opcodes.cpp +++ b/core/hw/sh4/interpr/sh4_opcodes.cpp @@ -11,6 +11,7 @@ #include "hw/sh4/sh4_interrupts.h" #include "debug/gdb_server.h" #include "hw/sh4/dyna/decoder.h" +#include "emulator.h" #ifdef STRICT_MODE #include "hw/sh4/sh4_cache.h" @@ -802,12 +803,16 @@ sh4op(i0000_0000_0010_1000) mac.full=0; } +static void executeDelaySlot() { + static_cast(emu.getSh4Executor())->ExecuteDelayslot(); +} + //braf sh4op(i0000_nnnn_0010_0011) { u32 n = GetN(op); u32 newpc = r[n] + next_pc + 2;// - ExecuteDelayslot(); //WARN : r[n] can change here + executeDelaySlot(); //WARN : r[n] can change here next_pc = newpc; } //bsrf @@ -817,7 +822,7 @@ sh4op(i0000_nnnn_0000_0011) u32 newpc = r[n] + next_pc +2; u32 newpr = next_pc + 2; - ExecuteDelayslot(); //WARN : pr and r[n] can change here + executeDelaySlot(); //WARN : pr and r[n] can change here pr = newpr; next_pc = newpc; @@ -829,7 +834,7 @@ sh4op(i0000_nnnn_0000_0011) sh4op(i0000_0000_0010_1011) { u32 newpc = spc; - ExecuteDelayslot_RTE(); + static_cast(emu.getSh4Executor())->ExecuteDelayslot_RTE(); next_pc = newpc; if (UpdateSR()) UpdateINTC(); @@ -841,7 +846,7 @@ sh4op(i0000_0000_0010_1011) sh4op(i0000_0000_0000_1011) { u32 newpc=pr; - ExecuteDelayslot(); //WARN : pr can change here + executeDelaySlot(); //WARN : pr can change here next_pc=newpc; debugger::subroutineReturn(); } @@ -868,7 +873,7 @@ sh4op(i1000_1111_iiii_iiii) { //delay 1 instruction u32 newpc=branch_target_s8(op); - ExecuteDelayslot(); + executeDelaySlot(); next_pc = newpc; } } @@ -892,7 +897,7 @@ sh4op(i1000_1101_iiii_iiii) { //delay 1 instruction u32 newpc=branch_target_s8(op); - ExecuteDelayslot(); + executeDelaySlot(); next_pc = newpc; } } @@ -906,7 +911,7 @@ u32 branch_target_s12(u32 op) sh4op(i1010_iiii_iiii_iiii) { u32 newpc = branch_target_s12(op); - ExecuteDelayslot(); + executeDelaySlot(); next_pc=newpc; } @@ -915,7 +920,7 @@ sh4op(i1011_iiii_iiii_iiii) { u32 newpr = next_pc + 2; //return after delayslot u32 newpc = branch_target_s12(op); - ExecuteDelayslot(); + executeDelaySlot(); pr = newpr; next_pc = newpc; @@ -937,7 +942,7 @@ sh4op(i0100_nnnn_0010_1011) u32 n = GetN(op); u32 newpc=r[n]; - ExecuteDelayslot(); //r[n] can change here + executeDelaySlot(); //r[n] can change here next_pc=newpc; } @@ -948,7 +953,7 @@ sh4op(i0100_nnnn_0000_1011) u32 newpr = next_pc + 2; //return after delayslot u32 newpc= r[n]; - ExecuteDelayslot(); //r[n]/pr can change here + executeDelaySlot(); //r[n]/pr can change here pr = newpr; next_pc = newpc; diff --git a/core/hw/sh4/modules/ccn.cpp b/core/hw/sh4/modules/ccn.cpp index 708f33110..19bbb1916 100644 --- a/core/hw/sh4/modules/ccn.cpp +++ b/core/hw/sh4/modules/ccn.cpp @@ -8,6 +8,7 @@ #include "hw/sh4/sh4_core.h" #include "hw/sh4/sh4_cache.h" #include "cfg/option.h" +#include "emulator.h" CCNRegisters ccn; @@ -54,7 +55,7 @@ static void CCN_MMUCR_write(u32 addr, u32 value) { //printf("<*******>MMU Enabled , ONLY SQ remaps work<*******>\n"); mmu_set_state(); - sh4_cpu.ResetCache(); + emu.getSh4Executor()->ResetCache(); } } diff --git a/core/hw/sh4/sh4_cache.h b/core/hw/sh4/sh4_cache.h index 5f9ba26e8..2784866c5 100644 --- a/core/hw/sh4/sh4_cache.h +++ b/core/hw/sh4/sh4_cache.h @@ -221,6 +221,7 @@ class Sh4ICache } std::array lines; + Sh4Cycles sh4cycles; }; extern Sh4ICache icache; @@ -589,6 +590,7 @@ class Sh4OCache // TODO serialize u64 writeBackBufferCycles = 0; u64 writeThroughBufferCycles = 0; + Sh4Cycles sh4cycles; }; extern Sh4OCache ocache; diff --git a/core/hw/sh4/sh4_core.h b/core/hw/sh4/sh4_core.h index cf8daf234..eb28ce224 100644 --- a/core/hw/sh4/sh4_core.h +++ b/core/hw/sh4/sh4_core.h @@ -25,7 +25,6 @@ #define xf_hex ((u32*)xf) #define dr_hex ((u64*)fr) #define xd_hex ((u64*)xf) -#define sh4_int_bCpuRun Sh4cntx.CpuRunning void UpdateFPSCR(); bool UpdateSR(); diff --git a/core/hw/sh4/sh4_core_regs.cpp b/core/hw/sh4/sh4_core_regs.cpp index 2fbd1584b..8338ef89b 100644 --- a/core/hw/sh4/sh4_core_regs.cpp +++ b/core/hw/sh4/sh4_core_regs.cpp @@ -10,7 +10,6 @@ #endif Sh4RCB* p_sh4rcb; -sh4_if sh4_cpu; static void ChangeGPR() { diff --git a/core/hw/sh4/sh4_cycles.cpp b/core/hw/sh4/sh4_cycles.cpp index 5588c9d6a..e55ca0760 100644 --- a/core/hw/sh4/sh4_cycles.cpp +++ b/core/hw/sh4/sh4_cycles.cpp @@ -17,6 +17,74 @@ along with Flycast. If not, see . */ #include "sh4_cycles.h" +#include "modules/mmu.h" + +int Sh4Cycles::countCycles(u16 op) +{ + sh4_opcodelistentry *opcode = OpDesc[op]; + int cycles = 0; +#ifndef STRICT_MODE + static const bool isMemOp[45] { + false, + false, + true, // all mem moves, ldtlb, sts.l FPUL/FPSCR, @-Rn, lds.l @Rn+,FPUL + true, // gbr-based load/store + false, + true, // tst.b #, @(R0,GBR) + true, // and/or/xor.b #, @(R0,GBR) + true, // tas.b @Rn + false, + false, + false, + false, + true, // movca.l R0, @Rn + false, + false, + false, + false, + true, // ldc.l @Rn+, VBR/SPC/SSR/Rn_Bank/DBR + true, // ldc.l @Rn+, GBR/SGR + true, // ldc.l @Rn+, SR + false, + false, + true, // stc.l DBR/SR/GBR/VBR/SSR/SPC/Rn_Bank, @-Rn + true, // stc.l SGR, @-Rn + false, + true, // lds.l @Rn+, PR + false, + true, // sts.l PR, @-Rn + false, + true, // lds.l @Rn+, MACH/MACL + false, + true, // sts.l MACH/MACL, @-Rn + false, + true, // lds.l @Rn+,FPSCR + false, + true, // mac.wl @Rm+,@Rn+ + }; + if (isMemOp[opcode->ex_type]) + { + if (++memOps < 4) + cycles = mmu_enabled() ? 5 : 2; + } + // TODO only for mem read? +#endif + + if (lastUnit == CO + || opcode->unit == CO + || (lastUnit == opcode->unit && lastUnit != MT)) + { + // cannot run in parallel + lastUnit = opcode->unit; + cycles += opcode->IssueCycles; + } + else + { + // can run in parallel + lastUnit = CO; + } + return cycles * cpuRatio; +} // TODO additional wait cycles depending on area?: // Area Wait cycles (not including external wait) diff --git a/core/hw/sh4/sh4_cycles.h b/core/hw/sh4/sh4_cycles.h index 1d517c675..ef203b907 100644 --- a/core/hw/sh4/sh4_cycles.h +++ b/core/hw/sh4/sh4_cycles.h @@ -21,7 +21,6 @@ #include "sh4_opcode_list.h" #include "sh4_if.h" #include "sh4_sched.h" -#include "modules/mmu.h" class Sh4Cycles { @@ -48,72 +47,7 @@ class Sh4Cycles Sh4cntx.cycle_counter -= writeAccessCycles(addr, size); } - int countCycles(u16 op) - { - sh4_opcodelistentry *opcode = OpDesc[op]; - int cycles = 0; -#ifndef STRICT_MODE - static const bool isMemOp[45] { - false, - false, - true, // all mem moves, ldtlb, sts.l FPUL/FPSCR, @-Rn, lds.l @Rn+,FPUL - true, // gbr-based load/store - false, - true, // tst.b #, @(R0,GBR) - true, // and/or/xor.b #, @(R0,GBR) - true, // tas.b @Rn - false, - false, - false, - false, - true, // movca.l R0, @Rn - false, - false, - false, - false, - true, // ldc.l @Rn+, VBR/SPC/SSR/Rn_Bank/DBR - true, // ldc.l @Rn+, GBR/SGR - true, // ldc.l @Rn+, SR - false, - false, - true, // stc.l DBR/SR/GBR/VBR/SSR/SPC/Rn_Bank, @-Rn - true, // stc.l SGR, @-Rn - false, - true, // lds.l @Rn+, PR - false, - true, // sts.l PR, @-Rn - false, - true, // lds.l @Rn+, MACH/MACL - false, - true, // sts.l MACH/MACL, @-Rn - false, - true, // lds.l @Rn+,FPSCR - false, - true, // mac.wl @Rm+,@Rn+ - }; - if (isMemOp[opcode->ex_type]) - { - if (++memOps < 4) - cycles = mmu_enabled() ? 5 : 2; - } - // TODO only for mem read? -#endif - - if (lastUnit == CO - || opcode->unit == CO - || (lastUnit == opcode->unit && lastUnit != MT)) - { - // cannot run in parallel - lastUnit = opcode->unit; - cycles += opcode->IssueCycles; - } - else - { - // can run in parallel - lastUnit = CO; - } - return cycles * cpuRatio; - } + int countCycles(u16 op); void reset() { @@ -143,5 +77,3 @@ class Sh4Cycles const int cpuRatio; int memOps = 0; }; - -extern Sh4Cycles sh4cycles; diff --git a/core/hw/sh4/sh4_if.h b/core/hw/sh4/sh4_if.h index a564c1543..491f4e666 100644 --- a/core/hw/sh4/sh4_if.h +++ b/core/hw/sh4/sh4_if.h @@ -100,21 +100,21 @@ struct fpscr_t }; //sh4 interface -struct sh4_if +class Sh4Executor { - void (*Start)(); - void (*Run)(); - void (*Stop)(); - void (*Step)(); - void (*Reset)(bool hard); - void (*Init)(); - void (*Term)(); - void (*ResetCache)(); - bool (*IsCpuRunning)(); +public: + virtual ~Sh4Executor() {} + virtual void Run() = 0; + virtual void Start() = 0; + virtual void Stop() = 0; + virtual void Step() = 0; + virtual void Reset(bool hard) = 0; + virtual void Init() = 0; + virtual void Term() = 0; + virtual void ResetCache() = 0; + virtual bool IsCpuRunning() = 0; }; -extern sh4_if sh4_cpu; - struct alignas(32) SQBuffer { u8 data[32]; }; @@ -235,6 +235,7 @@ struct alignas(PAGE_SIZE) Sh4RCB SQWriteFunc *do_sqw_nommu; Sh4Context cntx; }; +static_assert((sizeof(Sh4RCB) % PAGE_SIZE) == 0, "sizeof(Sh4RCB) not multiple of PAGE_SIZE"); extern Sh4RCB* p_sh4rcb; @@ -244,8 +245,8 @@ extern Sh4RCB* p_sh4rcb; #define Sh4cntx (sh4rcb.cntx) //Get an interface to sh4 interpreter -void Get_Sh4Interpreter(sh4_if* cpu); -void Get_Sh4Recompiler(sh4_if* cpu); +Sh4Executor *Get_Sh4Interpreter(); +Sh4Executor *Get_Sh4Recompiler(); enum Sh4ExceptionCode : u16 { diff --git a/core/hw/sh4/sh4_interpreter.h b/core/hw/sh4/sh4_interpreter.h index 4c5edabb3..70e3c7fb8 100644 --- a/core/hw/sh4/sh4_interpreter.h +++ b/core/hw/sh4/sh4_interpreter.h @@ -1,41 +1,37 @@ #pragma once #include "types.h" +#include "sh4_cycles.h" -#undef sh4op -#define sh4op(str) void DYNACALL str (u32 op) -typedef void (DYNACALL OpCallFP) (u32 op); - -enum OpcodeType +class Sh4Interpreter : public Sh4Executor { - //basic - Normal = 0, // Heh , nothing special :P - ReadsPC = 1, // PC must be set upon calling it - WritesPC = 2, // It will write PC (branch) - Delayslot = 4, // Has a delayslot opcode , valid only when WritesPC is set - - WritesSR = 8, // Writes to SR , and UpdateSR needs to be called - WritesFPSCR = 16, // Writes to FPSCR , and UpdateSR needs to be called - Invalid = 128, // Invalid - - UsesFPU = 2048, // Floating point op - FWritesFPSCR = UsesFPU | WritesFPSCR, - - // Heh, not basic :P - ReadWritePC = ReadsPC|WritesPC, // Read and writes pc :P - WritesSRRWPC = WritesSR|ReadsPC|WritesPC, - - // Branches (not delay slot): - Branch_dir = ReadWritePC, // Direct (eg , pc=r[xx]) -- this one is ReadWritePC b/c the delayslot may use pc ;) - Branch_rel = ReadWritePC, // Relative (rg pc+=10); - - // Delay slot - Branch_dir_d = Delayslot|Branch_dir, // Direct (eg , pc=r[xx]) - Branch_rel_d = Delayslot|Branch_rel, // Relative (rg pc+=10); +public: + void Run() override; + void ResetCache() override {} + void Start() override; + void Stop() override; + void Step() override; + void Reset(bool hard) override; + void Init() override; + void Term() override; + bool IsCpuRunning() override; + void ExecuteDelayslot(); + void ExecuteDelayslot_RTE(); + Sh4Context *getContext() { return ctx; } + +protected: + Sh4Context *ctx = nullptr; + +private: + void ExecuteOpcode(u16 op); + u16 ReadNexOp(); + + Sh4Cycles sh4cycles{CPU_RATIO}; + // SH4 underclock factor when using the interpreter so that it's somewhat usable +#ifdef STRICT_MODE + static constexpr int CPU_RATIO = 1; +#else + static constexpr int CPU_RATIO = 8; +#endif }; -void ExecuteDelayslot(); -void ExecuteDelayslot_RTE(); - -#define SH4_TIMESLICE 448 // at 112 Bangai-O doesn't start. 224 is ok - int UpdateSystem_INTC(); diff --git a/core/hw/sh4/sh4_opcode_list.h b/core/hw/sh4/sh4_opcode_list.h index a87be546e..cfdc25f48 100644 --- a/core/hw/sh4/sh4_opcode_list.h +++ b/core/hw/sh4/sh4_opcode_list.h @@ -1,9 +1,10 @@ #pragma once #include "types.h" -#include "sh4_interpreter.h" - #include +#define sh4op(str) void DYNACALL str (u32 op) + +typedef void (DYNACALL OpCallFP) (u32 op); extern OpCallFP* OpPtr[0x10000]; typedef void OpDissasmFP(char* out,const char* const FormatString,u32 pc,u16 opcode); @@ -18,6 +19,34 @@ enum sh4_eu CO, }; +enum OpcodeType +{ + //basic + Normal = 0, // Heh , nothing special :P + ReadsPC = 1, // PC must be set upon calling it + WritesPC = 2, // It will write PC (branch) + Delayslot = 4, // Has a delayslot opcode , valid only when WritesPC is set + + WritesSR = 8, // Writes to SR , and UpdateSR needs to be called + WritesFPSCR = 16, // Writes to FPSCR , and UpdateSR needs to be called + Invalid = 128, // Invalid + + UsesFPU = 2048, // Floating point op + FWritesFPSCR = UsesFPU | WritesFPSCR, + + // Heh, not basic :P + ReadWritePC = ReadsPC|WritesPC, // Read and writes pc :P + WritesSRRWPC = WritesSR|ReadsPC|WritesPC, + + // Branches (not delay slot): + Branch_dir = ReadWritePC, // Direct (eg , pc=r[xx]) -- this one is ReadWritePC b/c the delayslot may use pc ;) + Branch_rel = ReadWritePC, // Relative (rg pc+=10); + + // Delay slot + Branch_dir_d = Delayslot|Branch_dir, // Direct (eg , pc=r[xx]) + Branch_rel_d = Delayslot|Branch_rel, // Relative (rg pc+=10); +}; + std::string disassemble_op(const char* tx1, u32 pc, u16 opcode); typedef void ( RecOpCallFP) (u32 op); diff --git a/core/hw/sh4/sh4_sched.h b/core/hw/sh4/sh4_sched.h index 029f9ba8b..b40368972 100644 --- a/core/hw/sh4/sh4_sched.h +++ b/core/hw/sh4/sh4_sched.h @@ -1,8 +1,8 @@ -#ifndef SH4_SCHED_H -#define SH4_SCHED_H - +#pragma once #include "types.h" +#define SH4_TIMESLICE 448 // at 112 Bangai-O doesn't start. 224 is ok + /* tag, as passed on sh4_sched_register sch_cycles, the cycle duration that the callback requested (sh4_sched_request) @@ -53,5 +53,3 @@ void sh4_sched_serialize(Serializer& ser); void sh4_sched_deserialize(Deserializer& deser); void sh4_sched_serialize(Serializer& ser, int id); void sh4_sched_deserialize(Deserializer& deser, int id); - -#endif //SH4_SCHED_H diff --git a/core/network/ggpo.cpp b/core/network/ggpo.cpp index 0672af39b..6d4bb5fce 100644 --- a/core/network/ggpo.cpp +++ b/core/network/ggpo.cpp @@ -330,7 +330,7 @@ static bool load_game_state(unsigned char *buffer, int len) */ static bool save_game_state(unsigned char **buffer, int *len, int *checksum, int frame) { - verify(!sh4_cpu.IsCpuRunning()); + verify(!emu.getSh4Executor()->IsCpuRunning()); lastSavedFrame = frame; // TODO this is way too much memory size_t allocSize = settings.platform.isNaomi() ? 20_MB : 10_MB; @@ -914,7 +914,7 @@ void endOfFrame() if (active()) { _endOfFrame = true; - sh4_cpu.Stop(); + emu.getSh4Executor()->Stop(); } } diff --git a/core/nullDC.cpp b/core/nullDC.cpp index abe6669f7..8a50a042e 100644 --- a/core/nullDC.cpp +++ b/core/nullDC.cpp @@ -270,7 +270,7 @@ void dc_loadstate(int index) try { Deserializer deser(data, total_size); - dc_loadstate(deser); + emu.loadstate(deser); NOTICE_LOG(SAVESTATE, "Loaded state ver %d from %s size %d", deser.version(), filename.c_str(), total_size); if (deser.size() != total_size) // Note: this isn't true for RA savestates diff --git a/shell/libretro/libretro.cpp b/shell/libretro/libretro.cpp index 9b4122f7e..3c2c503ee 100644 --- a/shell/libretro/libretro.cpp +++ b/shell/libretro/libretro.cpp @@ -2362,7 +2362,7 @@ bool retro_unserialize(const void * data, size_t size) try { Deserializer deser(data, size); - dc_loadstate(deser); + emu.loadstate(deser); retro_audio_flush_buffer(); if (!first_run) emu.start(); diff --git a/tests/src/AicaArmTest.cpp b/tests/src/AicaArmTest.cpp index ad6ca7e66..994d03b6f 100644 --- a/tests/src/AicaArmTest.cpp +++ b/tests/src/AicaArmTest.cpp @@ -27,7 +27,7 @@ class AicaArmTest : public ::testing::Test { if (!addrspace::reserve()) die("addrspace::reserve failed"); emu.init(); - dc_reset(true); + emu.dc_reset(true); Arm7Enabled = true; } diff --git a/tests/src/CheatManagerTest.cpp b/tests/src/CheatManagerTest.cpp index 7300c57d6..813b63213 100644 --- a/tests/src/CheatManagerTest.cpp +++ b/tests/src/CheatManagerTest.cpp @@ -287,7 +287,7 @@ cheats = "2" mgr.reset("TESTSUB8"); mgr.loadCheatFile("test.cht"); mem_map_default(); - dc_reset(true); + emu.dc_reset(true); mgr.enableCheat(0, true); WriteMem8_nommu(0x8c010000, 0xFA); diff --git a/tests/src/MmuTest.cpp b/tests/src/MmuTest.cpp index 74933979e..3956fd4b0 100644 --- a/tests/src/MmuTest.cpp +++ b/tests/src/MmuTest.cpp @@ -30,7 +30,7 @@ class MmuTest : public ::testing::Test { if (!addrspace::reserve()) die("addrspace::reserve failed"); emu.init(); - dc_reset(true); + emu.dc_reset(true); CCN_MMUCR.AT = 1; MMU_reset(); } diff --git a/tests/src/Sh4InterpreterTest.cpp b/tests/src/Sh4InterpreterTest.cpp index adb573dcd..bd7642216 100644 --- a/tests/src/Sh4InterpreterTest.cpp +++ b/tests/src/Sh4InterpreterTest.cpp @@ -28,9 +28,9 @@ class Sh4InterpreterTest : public Sh4OpTest { die("addrspace::reserve failed"); emu.init(); mem_map_default(); - dc_reset(true); + emu.dc_reset(true); ctx = &p_sh4rcb->cntx; - Get_Sh4Interpreter(&sh4); + sh4 = Get_Sh4Interpreter(); } void PrepareOp(u16 op, u16 op2 = 0, u16 op3 = 0) override { @@ -45,7 +45,7 @@ class Sh4InterpreterTest : public Sh4OpTest { { ctx->pc = START_PC; for (int i = 0; i < numOp; i++) - sh4.Step(); + sh4->Step(); } }; diff --git a/tests/src/div32_test.cpp b/tests/src/div32_test.cpp index 3805abec2..36a5bd1fc 100644 --- a/tests/src/div32_test.cpp +++ b/tests/src/div32_test.cpp @@ -111,7 +111,7 @@ class Div32Test : public ::testing::Test { if (!addrspace::reserve()) die("addrspace::reserve failed"); emu.init(); - dc_reset(true); + emu.dc_reset(true); } void div32s(u32 n1, u32 n2, u32 n3) diff --git a/tests/src/serialize_test.cpp b/tests/src/serialize_test.cpp index 75a769128..b02151cdb 100644 --- a/tests/src/serialize_test.cpp +++ b/tests/src/serialize_test.cpp @@ -13,7 +13,7 @@ class SerializeTest : public ::testing::Test { if (!addrspace::reserve()) die("addrspace::reserve failed"); emu.init(); - dc_reset(true); + emu.dc_reset(true); } }; diff --git a/tests/src/sh4_ops.h b/tests/src/sh4_ops.h index af5ab2921..4d5189532 100644 --- a/tests/src/sh4_ops.h +++ b/tests/src/sh4_ops.h @@ -91,7 +91,7 @@ class Sh4OpTest : public ::testing::Test { } Sh4Context *ctx; - sh4_if sh4; + Sh4Executor *sh4; std::set checkedRegs; static constexpr u32 START_PC = 0xAC000000; From 9cb7d9e5d58e8401cdafe99d968d3736b3120b23 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 8 Nov 2024 18:27:52 +0100 Subject: [PATCH 14/81] rec-arm64: fix Store Queue write handlers --- core/rec-ARM64/rec_arm64.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/rec-ARM64/rec_arm64.cpp b/core/rec-ARM64/rec_arm64.cpp index 4602137cc..a242e3456 100644 --- a/core/rec-ARM64/rec_arm64.cpp +++ b/core/rec-ARM64/rec_arm64.cpp @@ -1557,8 +1557,7 @@ class Arm64Assembler : public MacroAssembler Cmp(x7, 0x38); GenBranchRuntime(addrspace::write32, Condition::ne); And(x0, x0, 0x3f); - Sub(x7, x0, sizeof(Sh4Context::sq_buffer), LeaveFlags); - Str(w1, MemOperand(x28, x7)); + Str(w1, MemOperand(x28, x0)); Ret(); Label writeStoreQueue64Label; @@ -1567,8 +1566,7 @@ class Arm64Assembler : public MacroAssembler Cmp(x7, 0x38); GenBranchRuntime(addrspace::write64, Condition::ne); And(x0, x0, 0x3f); - Sub(x7, x0, sizeof(Sh4Context::sq_buffer), LeaveFlags); - Str(x1, MemOperand(x28, x7)); + Str(x1, MemOperand(x28, x0)); Ret(); FinalizeCode(); From db846ca933513dc757ee4321deb4f3fbc91764aa Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 8 Nov 2024 18:36:08 +0100 Subject: [PATCH 15/81] sh4: pass context to interpreter funcs. get rid of sh4 regs #define --- core/hw/sh4/dyna/blockmanager.cpp | 16 +- core/hw/sh4/dyna/decoder.cpp | 4 +- core/hw/sh4/dyna/driver.cpp | 36 +- core/hw/sh4/dyna/shil_canonical.h | 1 + core/hw/sh4/interpr/sh4_fpu.cpp | 300 +++++---- core/hw/sh4/interpr/sh4_interpreter.cpp | 27 +- core/hw/sh4/interpr/sh4_opcodes.cpp | 814 ++++++++++++------------ core/hw/sh4/modules/ccn.cpp | 4 +- core/hw/sh4/modules/mmu.cpp | 22 +- core/hw/sh4/modules/wince.h | 2 +- core/hw/sh4/sh4_cache.h | 6 +- core/hw/sh4/sh4_core.h | 24 - core/hw/sh4/sh4_core_regs.cpp | 50 +- core/hw/sh4/sh4_interpreter.h | 2 + core/hw/sh4/sh4_interrupts.cpp | 34 +- core/hw/sh4/sh4_opcode_list.h | 5 +- core/rec-ARM/rec_arm.cpp | 11 +- core/rec-ARM64/rec_arm64.cpp | 31 +- core/rec-x64/rec_x64.cpp | 23 +- core/rec-x86/rec_x86.cpp | 18 +- core/rec-x86/x86_ops.cpp | 11 +- core/reios/reios.cpp | 120 ++-- core/reios/reios.h | 3 +- tests/src/div32_test.cpp | 5 + 24 files changed, 770 insertions(+), 799 deletions(-) diff --git a/core/hw/sh4/dyna/blockmanager.cpp b/core/hw/sh4/dyna/blockmanager.cpp index 905871b4c..d53700a49 100644 --- a/core/hw/sh4/dyna/blockmanager.cpp +++ b/core/hw/sh4/dyna/blockmanager.cpp @@ -65,8 +65,8 @@ DynarecCodeEntryPtr DYNACALL bm_GetCodeByVAddr(u32 addr) #ifdef USE_WINCE_HACK case 0xfffffde7: // GetTickCount // This should make this syscall faster - r[0] = sh4_sched_now64() * 1000 / SH4_MAIN_CLOCK; - next_pc = pr; + Sh4cntx.r[0] = sh4_sched_now64() * 1000 / SH4_MAIN_CLOCK; + Sh4cntx.pc = Sh4cntx.pr; Sh4cntx.cycle_counter -= 100; break; @@ -75,11 +75,11 @@ DynarecCodeEntryPtr DYNACALL bm_GetCodeByVAddr(u32 addr) bool isRam; u64 *ptr; u32 paddr; - if (rdv_writeMemImmediate(r[4], sizeof(u64), (void*&)ptr, isRam, paddr) && isRam) + if (rdv_writeMemImmediate(Sh4cntx.r[4], sizeof(u64), (void*&)ptr, isRam, paddr) && isRam) { *ptr = sh4_sched_now64() >> 4; - r[0] = 1; - next_pc = pr; + Sh4cntx.r[0] = 1; + Sh4cntx.pc = Sh4cntx.pr; Sh4cntx.cycle_counter -= 100; } else @@ -94,7 +94,7 @@ DynarecCodeEntryPtr DYNACALL bm_GetCodeByVAddr(u32 addr) Do_Exception(addr, Sh4Ex_AddressErrorRead); break; } - addr = next_pc; + addr = Sh4cntx.pc; } u32 paddr; @@ -102,7 +102,7 @@ DynarecCodeEntryPtr DYNACALL bm_GetCodeByVAddr(u32 addr) if (rv != MmuError::NONE) { DoMMUException(addr, rv, MMU_TT_IREAD); - mmu_instruction_translation(next_pc, paddr); + mmu_instruction_translation(Sh4cntx.pc, paddr); } return bm_GetCode(paddr); @@ -472,7 +472,7 @@ void bm_RamWriteAccess(u32 addr) std::vector list_copy; list_copy.insert(list_copy.begin(), block_list.begin(), block_list.end()); if (!list_copy.empty()) - DEBUG_LOG(DYNAREC, "bm_RamWriteAccess write access to %08x pc %08x", addr, next_pc); + DEBUG_LOG(DYNAREC, "bm_RamWriteAccess write access to %08x pc %08x", addr, Sh4cntx.pc); for (auto& block : list_copy) bm_DiscardBlock(block); verify(block_list.empty()); diff --git a/core/hw/sh4/dyna/decoder.cpp b/core/hw/sh4/dyna/decoder.cpp index 6503e3fd2..cc4129ca7 100644 --- a/core/hw/sh4/dyna/decoder.cpp +++ b/core/hw/sh4/dyna/decoder.cpp @@ -984,11 +984,11 @@ bool dec_DecodeBlock(RuntimeBlockInfo* rbi,u32 max_cycles) if (!blk->has_fpu_op && OpDesc[op]->IsFloatingPoint()) { - if (sr.FD == 1) + if (Sh4cntx.sr.FD == 1) { // We need to know FPSCR to compile the block, so let the exception handler run first // as it may change the fp registers - Do_Exception(next_pc, Sh4Ex_FpuDisabled); + Do_Exception(Sh4cntx.pc, Sh4Ex_FpuDisabled); return false; } blk->has_fpu_op = true; diff --git a/core/hw/sh4/dyna/driver.cpp b/core/hw/sh4/dyna/driver.cpp index 32b8230b2..d3595897b 100644 --- a/core/hw/sh4/dyna/driver.cpp +++ b/core/hw/sh4/dyna/driver.cpp @@ -92,7 +92,7 @@ void Sh4Recompiler::clear_temp_cache(bool full) void Sh4Recompiler::ResetCache() { - INFO_LOG(DYNAREC, "recSh4:Dynarec Cache clear at %08X free space %d", next_pc, codeBuffer.getFreeSpace()); + INFO_LOG(DYNAREC, "recSh4:Dynarec Cache clear at %08X free space %d", Sh4cntx.pc, codeBuffer.getFreeSpace()); codeBuffer.reset(false); bm_ResetCache(); smc_hotspots.clear(); @@ -168,14 +168,14 @@ bool RuntimeBlockInfo::Setup(u32 rpc,fpscr_t rfpu_cfg) DynarecCodeEntryPtr rdv_CompilePC(u32 blockcheck_failures) { - const u32 pc = next_pc; + const u32 pc = Sh4cntx.pc; if (codeBuffer.getFreeSpace() < 32_KB || pc == 0x8c0000e0 || pc == 0xac010000 || pc == 0xac008300) Sh4Recompiler::Instance->ResetCache(); RuntimeBlockInfo* rbi = sh4Dynarec->allocateBlock(); - if (!rbi->Setup(pc, fpscr)) + if (!rbi->Setup(pc, Sh4cntx.fpscr)) { delete rbi; return nullptr; @@ -204,16 +204,16 @@ DynarecCodeEntryPtr rdv_CompilePC(u32 blockcheck_failures) DynarecCodeEntryPtr DYNACALL rdv_FailedToFindBlock_pc() { - return rdv_FailedToFindBlock(next_pc); + return rdv_FailedToFindBlock(Sh4cntx.pc); } DynarecCodeEntryPtr DYNACALL rdv_FailedToFindBlock(u32 pc) { //DEBUG_LOG(DYNAREC, "rdv_FailedToFindBlock %08x", pc); - next_pc=pc; + Sh4cntx.pc=pc; DynarecCodeEntryPtr code = rdv_CompilePC(0); if (code == NULL) - code = bm_GetCodeByVAddr(next_pc); + code = bm_GetCodeByVAddr(Sh4cntx.pc); else code = (DynarecCodeEntryPtr)CC_RW2RX(code); return code; @@ -247,7 +247,7 @@ DynarecCodeEntryPtr DYNACALL rdv_BlockCheckFail(u32 addr) } else { - next_pc = addr; + Sh4cntx.pc = addr; Sh4Recompiler::Instance->ResetCache(); } return (DynarecCodeEntryPtr)CC_RW2RX(rdv_CompilePC(blockcheck_failures)); @@ -255,7 +255,7 @@ DynarecCodeEntryPtr DYNACALL rdv_BlockCheckFail(u32 addr) DynarecCodeEntryPtr rdv_FindOrCompile() { - DynarecCodeEntryPtr rv = bm_GetCodeByVAddr(next_pc); // Returns exec addr + DynarecCodeEntryPtr rv = bm_GetCodeByVAddr(Sh4cntx.pc); // Returns exec addr if (rv == ngen_FailedToFindBlock) rv = (DynarecCodeEntryPtr)CC_RW2RX(rdv_CompilePC(0)); // Returns rw addr @@ -281,20 +281,20 @@ void* DYNACALL rdv_LinkBlock(u8* code,u32 dpc) if (bcls == BET_CLS_Static) { if (rbi->BlockType == BET_StaticIntr) - next_pc = rbi->NextBlock; + Sh4cntx.pc = rbi->NextBlock; else - next_pc = rbi->BranchBlock; + Sh4cntx.pc = rbi->BranchBlock; } else if (bcls == BET_CLS_Dynamic) { - next_pc = dpc; + Sh4cntx.pc = dpc; } else if (bcls == BET_CLS_COND) { if (dpc) - next_pc = rbi->BranchBlock; + Sh4cntx.pc = rbi->BranchBlock; else - next_pc = rbi->NextBlock; + Sh4cntx.pc = rbi->NextBlock; } DynarecCodeEntryPtr rv = rdv_FindOrCompile(); // Returns rx ptr @@ -313,17 +313,17 @@ void* DYNACALL rdv_LinkBlock(u8* code,u32 dpc) } else if (rbi->relink_data == 0) { - rbi->pBranchBlock = bm_GetBlock(next_pc).get(); + rbi->pBranchBlock = bm_GetBlock(Sh4cntx.pc).get(); rbi->pBranchBlock->AddRef(rbi); } } else { - RuntimeBlockInfo* nxt = bm_GetBlock(next_pc).get(); + RuntimeBlockInfo* nxt = bm_GetBlock(Sh4cntx.pc).get(); - if (rbi->BranchBlock == next_pc) + if (rbi->BranchBlock == Sh4cntx.pc) rbi->pBranchBlock = nxt; - if (rbi->NextBlock == next_pc) + if (rbi->NextBlock == Sh4cntx.pc) rbi->pNextBlock = nxt; nxt->AddRef(rbi); @@ -334,7 +334,7 @@ void* DYNACALL rdv_LinkBlock(u8* code,u32 dpc) } else { - INFO_LOG(DYNAREC, "null RBI: from %08X to %08X -- unlinked stale block -- code %p next %p", rbi->vaddr, next_pc, code, rv); + INFO_LOG(DYNAREC, "null RBI: from %08X to %08X -- unlinked stale block -- code %p next %p", rbi->vaddr, Sh4cntx.pc, code, rv); } return (void*)rv; diff --git a/core/hw/sh4/dyna/shil_canonical.h b/core/hw/sh4/dyna/shil_canonical.h index c42397727..b4230e34a 100644 --- a/core/hw/sh4/dyna/shil_canonical.h +++ b/core/hw/sh4/dyna/shil_canonical.h @@ -661,6 +661,7 @@ shil_opc(div1) shil_canonical ( u64,f1,(u32 a, s32 b, u32 T), + sr_t& sr = Sh4cntx.sr; bool qxm = sr.Q ^ sr.M; sr.Q = (int)a < 0; a = (a << 1) | T; diff --git a/core/hw/sh4/interpr/sh4_fpu.cpp b/core/hw/sh4/interpr/sh4_fpu.cpp index bc5fee8ad..e4048e8df 100644 --- a/core/hw/sh4/interpr/sh4_fpu.cpp +++ b/core/hw/sh4/interpr/sh4_fpu.cpp @@ -6,8 +6,6 @@ #include "hw/sh4/sh4_rom.h" #include "hw/sh4/sh4_mem.h" -#define sh4op(str) void DYNACALL str (u32 op) - static u32 GetN(u32 op) { return (op >> 8) & 0xf; } @@ -15,14 +13,14 @@ static u32 GetM(u32 op) { return (op >> 4) & 0xf; } -static double getDRn(u32 op) { - return p_sh4rcb->cntx.getDR((op >> 9) & 7); +static double getDRn(Sh4Context *ctx, u32 op) { + return ctx->getDR((op >> 9) & 7); } -static double getDRm(u32 op) { - return p_sh4rcb->cntx.getDR((op >> 5) & 7); +static double getDRm(Sh4Context *ctx, u32 op) { + return ctx->getDR((op >> 5) & 7); } -static void setDRn(u32 op, double d) { - p_sh4rcb->cntx.setDR((op >> 9) & 7, d); +static void setDRn(Sh4Context *ctx, u32 op, double d) { + ctx->setDR((op >> 9) & 7, d); } static void iNimp(const char *str); @@ -32,127 +30,127 @@ static void iNimp(const char *str); //fadd , sh4op(i1111_nnnn_mmmm_0000) { - if (fpscr.PR == 0) + if (ctx->fpscr.PR == 0) { u32 n = GetN(op); u32 m = GetM(op); - fr[n] += fr[m]; - CHECK_FPU_32(fr[n]); + ctx->fr(n) += ctx->fr(m); + CHECK_FPU_32(ctx->fr(n)); } else { - double d = getDRn(op) + getDRm(op); + double d = getDRn(ctx, op) + getDRm(ctx, op); d = fixNaN64(d); - setDRn(op, d); + setDRn(ctx, op, d); } } //fsub , sh4op(i1111_nnnn_mmmm_0001) { - if (fpscr.PR == 0) + if (ctx->fpscr.PR == 0) { u32 n = GetN(op); u32 m = GetM(op); - fr[n] -= fr[m]; - CHECK_FPU_32(fr[n]); + ctx->fr(n) -= ctx->fr(m); + CHECK_FPU_32(ctx->fr(n)); } else { - double d = getDRn(op) - getDRm(op); + double d = getDRn(ctx, op) - getDRm(ctx, op); d = fixNaN64(d); - setDRn(op, d); + setDRn(ctx, op, d); } } //fmul , sh4op(i1111_nnnn_mmmm_0010) { - if (fpscr.PR == 0) + if (ctx->fpscr.PR == 0) { u32 n = GetN(op); u32 m = GetM(op); - fr[n] *= fr[m]; - CHECK_FPU_32(fr[n]); + ctx->fr(n) *= ctx->fr(m); + CHECK_FPU_32(ctx->fr(n)); } else { - double d = getDRn(op) * getDRm(op); + double d = getDRn(ctx, op) * getDRm(ctx, op); d = fixNaN64(d); - setDRn(op, d); + setDRn(ctx, op, d); } } //fdiv , sh4op(i1111_nnnn_mmmm_0011) { - if (fpscr.PR == 0) + if (ctx->fpscr.PR == 0) { u32 n = GetN(op); u32 m = GetM(op); - fr[n] /= fr[m]; + ctx->fr(n) /= ctx->fr(m); - CHECK_FPU_32(fr[n]); + CHECK_FPU_32(ctx->fr(n)); } else { - double d = getDRn(op) / getDRm(op); + double d = getDRn(ctx, op) / getDRm(ctx, op); d = fixNaN64(d); - setDRn(op, d); + setDRn(ctx, op, d); } } //fcmp/eq , sh4op(i1111_nnnn_mmmm_0100) { - if (fpscr.PR == 0) + if (ctx->fpscr.PR == 0) { u32 n = GetN(op); u32 m = GetM(op); - sr.T = fr[m] == fr[n]; + ctx->sr.T = ctx->fr(m) == ctx->fr(n); } else { - sr.T = getDRn(op) == getDRm(op); + ctx->sr.T = getDRn(ctx, op) == getDRm(ctx, op); } } //fcmp/gt , sh4op(i1111_nnnn_mmmm_0101) { - if (fpscr.PR == 0) + if (ctx->fpscr.PR == 0) { u32 n = GetN(op); u32 m = GetM(op); - if (fr[n] > fr[m]) - sr.T = 1; + if (ctx->fr(n) > ctx->fr(m)) + ctx->sr.T = 1; else - sr.T = 0; + ctx->sr.T = 0; } else { - sr.T = getDRn(op) > getDRm(op); + ctx->sr.T = getDRn(ctx, op) > getDRm(ctx, op); } } //All memory opcodes are here //fmov.s @(R0,), sh4op(i1111_nnnn_mmmm_0110) { - if (fpscr.SZ == 0) + if (ctx->fpscr.SZ == 0) { u32 n = GetN(op); u32 m = GetM(op); - fr_hex[n] = ReadMem32(r[m] + r[0]); + ctx->fr_hex(n) = ReadMem32(ctx->r[m] + ctx->r[0]); } else { u32 n = GetN(op)>>1; u32 m = GetM(op); if (((op >> 8) & 1) == 0) - dr_hex[n] = ReadMem64(r[m] + r[0]); + ctx->dr_hex(n) = ReadMem64(ctx->r[m] + ctx->r[0]); else - xd_hex[n] = ReadMem64(r[m] + r[0]); + ctx->xd_hex(n) = ReadMem64(ctx->r[m] + ctx->r[0]); } } @@ -160,21 +158,21 @@ sh4op(i1111_nnnn_mmmm_0110) //fmov.s ,@(R0,) sh4op(i1111_nnnn_mmmm_0111) { - if (fpscr.SZ == 0) + if (ctx->fpscr.SZ == 0) { u32 n = GetN(op); u32 m = GetM(op); - WriteMem32(r[0] + r[n], fr_hex[m]); + WriteMem32(ctx->r[0] + ctx->r[n], ctx->fr_hex(m)); } else { u32 n = GetN(op); u32 m = GetM(op)>>1; if (((op >> 4) & 0x1) == 0) - WriteMem64(r[n] + r[0], dr_hex[m]); + WriteMem64(ctx->r[n] + ctx->r[0], ctx->dr_hex(m)); else - WriteMem64(r[n] + r[0], xd_hex[m]); + WriteMem64(ctx->r[n] + ctx->r[0], ctx->xd_hex(m)); } } @@ -182,20 +180,20 @@ sh4op(i1111_nnnn_mmmm_0111) //fmov.s @, sh4op(i1111_nnnn_mmmm_1000) { - if (fpscr.SZ == 0) + if (ctx->fpscr.SZ == 0) { u32 n = GetN(op); u32 m = GetM(op); - fr_hex[n] = ReadMem32(r[m]); + ctx->fr_hex(n) = ReadMem32(ctx->r[m]); } else { u32 n = GetN(op)>>1; u32 m = GetM(op); if (((op >> 8) & 1) == 0) - dr_hex[n] = ReadMem64(r[m]); + ctx->dr_hex(n) = ReadMem64(ctx->r[m]); else - xd_hex[n] = ReadMem64(r[m]); + ctx->xd_hex(n) = ReadMem64(ctx->r[m]); } } @@ -203,23 +201,23 @@ sh4op(i1111_nnnn_mmmm_1000) //fmov.s @+, sh4op(i1111_nnnn_mmmm_1001) { - if (fpscr.SZ == 0) + if (ctx->fpscr.SZ == 0) { u32 n = GetN(op); u32 m = GetM(op); - fr_hex[n] = ReadMem32(r[m]); - r[m] += 4; + ctx->fr_hex(n) = ReadMem32(ctx->r[m]); + ctx->r[m] += 4; } else { u32 n = GetN(op)>>1; u32 m = GetM(op); if (((op >> 8) & 1) == 0) - dr_hex[n] = ReadMem64(r[m]); + ctx->dr_hex(n) = ReadMem64(ctx->r[m]); else - xd_hex[n] = ReadMem64(r[m]); - r[m] += 8; + ctx->xd_hex(n) = ReadMem64(ctx->r[m]); + ctx->r[m] += 8; } } @@ -227,11 +225,11 @@ sh4op(i1111_nnnn_mmmm_1001) //fmov.s ,@ sh4op(i1111_nnnn_mmmm_1010) { - if (fpscr.SZ == 0) + if (ctx->fpscr.SZ == 0) { u32 n = GetN(op); u32 m = GetM(op); - WriteMem32(r[n], fr_hex[m]); + WriteMem32(ctx->r[n], ctx->fr_hex(m)); } else { @@ -239,38 +237,38 @@ sh4op(i1111_nnnn_mmmm_1010) u32 m = GetM(op)>>1; if (((op >> 4) & 0x1) == 0) - WriteMem64(r[n], dr_hex[m]); + WriteMem64(ctx->r[n], ctx->dr_hex(m)); else - WriteMem64(r[n], xd_hex[m]); + WriteMem64(ctx->r[n], ctx->xd_hex(m)); } } //fmov.s ,@- sh4op(i1111_nnnn_mmmm_1011) { - if (fpscr.SZ == 0) + if (ctx->fpscr.SZ == 0) { u32 n = GetN(op); u32 m = GetM(op); - u32 addr = r[n] - 4; + u32 addr = ctx->r[n] - 4; - WriteMem32(addr, fr_hex[m]); + WriteMem32(addr, ctx->fr_hex(m)); - r[n] = addr; + ctx->r[n] = addr; } else { u32 n = GetN(op); u32 m = GetM(op)>>1; - u32 addr = r[n] - 8; + u32 addr = ctx->r[n] - 8; if (((op >> 4) & 0x1) == 0) - WriteMem64(addr, dr_hex[m]); + WriteMem64(addr, ctx->dr_hex(m)); else - WriteMem64(addr, xd_hex[m]); + WriteMem64(addr, ctx->xd_hex(m)); - r[n] = addr; + ctx->r[n] = addr; } } @@ -279,11 +277,11 @@ sh4op(i1111_nnnn_mmmm_1011) //fmov , sh4op(i1111_nnnn_mmmm_1100) { - if (fpscr.SZ == 0) + if (ctx->fpscr.SZ == 0) { u32 n = GetN(op); u32 m = GetM(op); - fr[n] = fr[m]; + ctx->fr(n) = ctx->fr(m); } else { @@ -293,22 +291,22 @@ sh4op(i1111_nnnn_mmmm_1100) { case 0x00: //dr[n] = dr[m]; - dr_hex[n] = dr_hex[m]; + ctx->dr_hex(n) = ctx->dr_hex(m); break; case 0x01: //dr[n] = xd[m]; - dr_hex[n] = xd_hex[m]; + ctx->dr_hex(n) = ctx->xd_hex(m); break; case 0x10: //xd[n] = dr[m]; - xd_hex[n] = dr_hex[m]; + ctx->xd_hex(n) = ctx->dr_hex(m); break; case 0x11: //xd[n] = xd[m]; - xd_hex[n] = xd_hex[m]; + ctx->xd_hex(n) = ctx->xd_hex(m); break; } } @@ -320,10 +318,10 @@ sh4op(i1111_nnnn_0101_1101) { int n=GetN(op); - if (fpscr.PR ==0) - fr_hex[n]&=0x7FFFFFFF; + if (ctx->fpscr.PR == 0) + ctx->fr_hex(n) &= 0x7FFFFFFF; else - fr_hex[(n&0xE)]&=0x7FFFFFFF; + ctx->fr_hex(n & 0xE) &= 0x7FFFFFFF; } @@ -334,21 +332,21 @@ sh4op(i1111_nnn0_1111_1101) //cosine(x) = sine(pi/2 + x). - if (fpscr.PR==0) + if (ctx->fpscr.PR==0) { - u32 pi_index=fpul&0xFFFF; + u32 pi_index = ctx->fpul & 0xFFFF; #ifdef NATIVE_FSCA float rads = pi_index / (65536.0f / 2) * float(M_PI); - fr[n + 0] = sinf(rads); - fr[n + 1] = cosf(rads); + ctx->fr(n + 0) = sinf(rads); + ctx->fr(n + 1) = cosf(rads); - CHECK_FPU_32(fr[n]); - CHECK_FPU_32(fr[n+1]); + CHECK_FPU_32(ctx->fr(n)); + CHECK_FPU_32(ctx->fr(n + 1)); #else - fr[n + 0] = sin_table[pi_index].u[0]; - fr[n + 1] = sin_table[pi_index].u[1]; + ctx->fr(n + 0) = sin_table[pi_index].u[0]; + ctx->fr(n + 1) = sin_table[pi_index].u[1]; #endif } @@ -360,10 +358,10 @@ sh4op(i1111_nnn0_1111_1101) sh4op(i1111_nnnn_0111_1101) { u32 n = GetN(op); - if (fpscr.PR==0) + if (ctx->fpscr.PR==0) { - fr[n] = 1.f / sqrtf(fr[n]); - CHECK_FPU_32(fr[n]); + ctx->fr(n) = 1.f / sqrtf(ctx->fr(n)); + CHECK_FPU_32(ctx->fr(n)); } else iNimp("FSRRA : Double precision mode"); @@ -373,10 +371,10 @@ sh4op(i1111_nnnn_0111_1101) sh4op(i1111_nnnn_1011_1101) { - if (fpscr.PR == 1) + if (ctx->fpscr.PR == 1) { - u32 *p = &fpul; - *((float *)p) = (float)getDRn(op); + u32 *p = &ctx->fpul; + *((float *)p) = (float)getDRn(ctx, op); } else { @@ -388,10 +386,10 @@ sh4op(i1111_nnnn_1011_1101) //fcnvsd FPUL, sh4op(i1111_nnnn_1010_1101) { - if (fpscr.PR == 1) + if (ctx->fpscr.PR == 1) { - u32 *p = &fpul; - setDRn(op, (double)*((float *)p)); + u32 *p = &ctx->fpul; + setDRn(ctx, op, (double)*((float *)p)); } else { @@ -404,14 +402,14 @@ sh4op(i1111_nnmm_1110_1101) { int n=GetN(op)&0xC; int m=(GetN(op)&0x3)<<2; - if (fpscr.PR == 0) + if (ctx->fpscr.PR == 0) { - double idp = (double)fr[n + 0] * fr[m + 0]; - idp += (double)fr[n + 1] * fr[m + 1]; - idp += (double)fr[n + 2] * fr[m + 2]; - idp += (double)fr[n + 3] * fr[m + 3]; + double idp = (double)ctx->fr(n + 0) * ctx->fr(m + 0); + idp += (double)ctx->fr(n + 1) * ctx->fr(m + 1); + idp += (double)ctx->fr(n + 2) * ctx->fr(m + 2); + idp += (double)ctx->fr(n + 3) * ctx->fr(m + 3); - fr[n + 3] = fixNaN((float)idp); + ctx->fr(n + 3) = fixNaN((float)idp); } else { @@ -422,24 +420,24 @@ sh4op(i1111_nnmm_1110_1101) //fldi0 sh4op(i1111_nnnn_1000_1101) { - if (fpscr.PR!=0) + if (ctx->fpscr.PR!=0) return; u32 n = GetN(op); - fr[n] = 0.0f; + ctx->fr(n) = 0.0f; } //fldi1 sh4op(i1111_nnnn_1001_1101) { - if (fpscr.PR!=0) + if (ctx->fpscr.PR!=0) return; u32 n = GetN(op); - fr[n] = 1.0f; + ctx->fr(n) = 1.0f; } //flds ,FPUL @@ -447,27 +445,27 @@ sh4op(i1111_nnnn_0001_1101) { u32 n = GetN(op); - fpul = fr_hex[n]; + ctx->fpul = ctx->fr_hex(n); } //fsts FPUL, sh4op(i1111_nnnn_0000_1101) { u32 n = GetN(op); - fr_hex[n] = fpul; + ctx->fr_hex(n) = ctx->fpul; } //float FPUL, sh4op(i1111_nnnn_0010_1101) { - if (fpscr.PR == 0) + if (ctx->fpscr.PR == 0) { u32 n = GetN(op); - fr[n] = (float)(int)fpul; + ctx->fr(n) = (float)(int)ctx->fpul; } else { - setDRn(op, (double)(int)fpul); + setDRn(ctx, op, (double)(int)ctx->fpul); } } @@ -477,17 +475,17 @@ sh4op(i1111_nnnn_0100_1101) { u32 n = GetN(op); - if (fpscr.PR ==0) - fr_hex[n]^=0x80000000; + if (ctx->fpscr.PR == 0) + ctx->fr_hex(n) ^= 0x80000000; else - fr_hex[(n&0xE)]^=0x80000000; + ctx->fr_hex(n & 0xE) ^= 0x80000000; } //frchg sh4op(i1111_1011_1111_1101) { - fpscr.FR = 1 - fpscr.FR; + ctx->fpscr.FR = 1 - ctx->fpscr.FR; UpdateFPSCR(); } @@ -495,22 +493,22 @@ sh4op(i1111_1011_1111_1101) //fschg sh4op(i1111_0011_1111_1101) { - fpscr.SZ = 1 - fpscr.SZ; + ctx->fpscr.SZ = 1 - ctx->fpscr.SZ; } //fsqrt sh4op(i1111_nnnn_0110_1101) { - if (fpscr.PR == 0) + if (ctx->fpscr.PR == 0) { u32 n = GetN(op); - fr[n] = sqrtf(fr[n]); - CHECK_FPU_32(fr[n]); + ctx->fr(n) = sqrtf(ctx->fr(n)); + CHECK_FPU_32(ctx->fr(n)); } else { - setDRn(op, fixNaN64(std::sqrt(getDRn(op)))); + setDRn(ctx, op, fixNaN64(sqrt(getDRn(ctx, op)))); } } @@ -518,31 +516,31 @@ sh4op(i1111_nnnn_0110_1101) //ftrc , FPUL sh4op(i1111_nnnn_0011_1101) { - if (fpscr.PR == 0) + if (ctx->fpscr.PR == 0) { u32 n = GetN(op); - fpul = (u32)(s32)fr[n]; + ctx->fpul = (u32)(s32)ctx->fr(n); - if ((s32)fpul > 0x7fffff80) - fpul = 0x7fffffff; + if ((s32)ctx->fpul > 0x7fffff80) + ctx->fpul = 0x7fffffff; // Intel CPUs convert out of range float numbers to 0x80000000. Manually set the correct sign - else if (fpul == 0x80000000 && fr[n] == fr[n]) + else if (ctx->fpul == 0x80000000 && ctx->fr(n) == ctx->fr(n)) { - if (*(int *)&fr[n] > 0) // Using integer math to avoid issues with Inf and NaN - fpul--; + if (*(int *)&ctx->fr(n) > 0) // Using integer math to avoid issues with Inf and NaN + ctx->fpul--; } } else { - f64 f = getDRn(op); - fpul = (u32)(s32)f; + f64 f = getDRn(ctx, op); + ctx->fpul = (u32)(s32)f; // TODO saturate // Intel CPUs convert out of range float numbers to 0x80000000. Manually set the correct sign - if (fpul == 0x80000000 && f == f) + if (ctx->fpul == 0x80000000 && f == f) { if (*(s64 *)&f > 0) // Using integer math to avoid issues with Inf and NaN - fpul--; + ctx->fpul--; } } } @@ -551,13 +549,13 @@ sh4op(i1111_nnnn_0011_1101) //fmac ,, sh4op(i1111_nnnn_mmmm_1110) { - if (fpscr.PR==0) + if (ctx->fpscr.PR==0) { u32 n = GetN(op); u32 m = GetM(op); - fr[n] = std::fma(fr[0], fr[m], fr[n]); - CHECK_FPU_32(fr[n]); + ctx->fr(n) = std::fma(ctx->fr(0), ctx->fr(m), ctx->fr(n)); + CHECK_FPU_32(ctx->fr(n)); } else { @@ -578,32 +576,32 @@ sh4op(i1111_nn01_1111_1101) u32 n=GetN(op)&0xC; - if (fpscr.PR==0) + if (ctx->fpscr.PR==0) { - double v1 = (double)xf[0] * fr[n + 0] + - (double)xf[4] * fr[n + 1] + - (double)xf[8] * fr[n + 2] + - (double)xf[12] * fr[n + 3]; + double v1 = (double)ctx->xf(0) * ctx->fr(n + 0) + + (double)ctx->xf(4) * ctx->fr(n + 1) + + (double)ctx->xf(8) * ctx->fr(n + 2) + + (double)ctx->xf(12) * ctx->fr(n + 3); - double v2 = (double)xf[1] * fr[n + 0] + - (double)xf[5] * fr[n + 1] + - (double)xf[9] * fr[n + 2] + - (double)xf[13] * fr[n + 3]; + double v2 = (double)ctx->xf(1) * ctx->fr(n + 0) + + (double)ctx->xf(5) * ctx->fr(n + 1) + + (double)ctx->xf(9) * ctx->fr(n + 2) + + (double)ctx->xf(13) * ctx->fr(n + 3); - double v3 = (double)xf[2] * fr[n + 0] + - (double)xf[6] * fr[n + 1] + - (double)xf[10] * fr[n + 2] + - (double)xf[14] * fr[n + 3]; + double v3 = (double)ctx->xf(2) * ctx->fr(n + 0) + + (double)ctx->xf(6) * ctx->fr(n + 1) + + (double)ctx->xf(10) * ctx->fr(n + 2) + + (double)ctx->xf(14) * ctx->fr(n + 3); - double v4 = (double)xf[3] * fr[n + 0] + - (double)xf[7] * fr[n + 1] + - (double)xf[11] * fr[n + 2] + - (double)xf[15] * fr[n + 3]; + double v4 = (double)ctx->xf(3) * ctx->fr(n + 0) + + (double)ctx->xf(7) * ctx->fr(n + 1) + + (double)ctx->xf(11) * ctx->fr(n + 2) + + (double)ctx->xf(15) * ctx->fr(n + 3); - fr[n + 0] = fixNaN((float)v1); - fr[n + 1] = fixNaN((float)v2); - fr[n + 2] = fixNaN((float)v3); - fr[n + 3] = fixNaN((float)v4); + ctx->fr(n + 0) = fixNaN((float)v1); + ctx->fr(n + 1) = fixNaN((float)v2); + ctx->fr(n + 2) = fixNaN((float)v3); + ctx->fr(n + 3) = fixNaN((float)v4); } else { diff --git a/core/hw/sh4/interpr/sh4_interpreter.cpp b/core/hw/sh4/interpr/sh4_interpreter.cpp index c5c5df60e..543e43c63 100644 --- a/core/hw/sh4/interpr/sh4_interpreter.cpp +++ b/core/hw/sh4/interpr/sh4_interpreter.cpp @@ -16,12 +16,13 @@ Sh4ICache icache; Sh4OCache ocache; +Sh4Interpreter *Sh4Interpreter::Instance; void Sh4Interpreter::ExecuteOpcode(u16 op) { - if (sr.FD == 1 && OpDesc[op]->IsFloatingPoint()) + if (ctx->sr.FD == 1 && OpDesc[op]->IsFloatingPoint()) throw SH4ThrownException(ctx->pc - 2, Sh4Ex_FpuDisabled); - OpPtr[op](op); + OpPtr[op](ctx, op); sh4cycles.executeCycles(op); } @@ -39,6 +40,7 @@ u16 Sh4Interpreter::ReadNexOp() void Sh4Interpreter::Run() { + Instance = this; RestoreHostRoundingMode(); try { @@ -63,6 +65,7 @@ void Sh4Interpreter::Run() } ctx->CpuRunning = false; + Instance = nullptr; } void Sh4Interpreter::Start() @@ -78,6 +81,7 @@ void Sh4Interpreter::Stop() void Sh4Interpreter::Step() { verify(!ctx->CpuRunning); + Instance = this; RestoreHostRoundingMode(); try { @@ -89,6 +93,7 @@ void Sh4Interpreter::Step() sh4cycles.addCycles(5 * CPU_RATIO); } catch (const debugger::Stop&) { } + Instance = nullptr; } void Sh4Interpreter::Reset(bool hard) @@ -103,18 +108,18 @@ void Sh4Interpreter::Reset(bool hard) } ctx->pc = 0xA0000000; - memset(r, 0, sizeof(r)); - memset(r_bank, 0, sizeof(r_bank)); + memset(ctx->r, 0, sizeof(ctx->r)); + memset(ctx->r_bank, 0, sizeof(ctx->r_bank)); - gbr = ssr = spc = sgr = dbr = vbr = 0; - mac.full = pr = fpul = 0; + ctx->gbr = ctx->ssr = ctx->spc = ctx->sgr = ctx->dbr = ctx->vbr = 0; + ctx->mac.full = ctx->pr = ctx->fpul = 0; - sr.setFull(0x700000F0); - old_sr.status = sr.status; + ctx->sr.setFull(0x700000F0); + ctx->old_sr.status = ctx->sr.status; UpdateSR(); - fpscr.full = 0x00040001; - old_fpscr = fpscr; + ctx->fpscr.full = 0x00040001; + ctx->old_fpscr = ctx->fpscr; icache.Reset(hard); ocache.Reset(hard); @@ -155,7 +160,7 @@ void Sh4Interpreter::ExecuteDelayslot_RTE() // instruction execution. The STC and STC.L SR instructions access all SR bits after modification. u32 op = ReadNexOp(); // Now restore all SR bits - sr.setFull(ssr); + ctx->sr.setFull(ctx->ssr); // And execute ExecuteOpcode(op); } catch (const SH4ThrownException&) { diff --git a/core/hw/sh4/interpr/sh4_opcodes.cpp b/core/hw/sh4/interpr/sh4_opcodes.cpp index 60713c51e..70cdb5595 100644 --- a/core/hw/sh4/interpr/sh4_opcodes.cpp +++ b/core/hw/sh4/interpr/sh4_opcodes.cpp @@ -45,7 +45,7 @@ sh4op(i0000_nnnn_0001_0010) { u32 n = GetN(op); - r[n] = gbr; + ctx->r[n] = ctx->gbr; } @@ -53,7 +53,7 @@ sh4op(i0000_nnnn_0001_0010) sh4op(i0000_nnnn_0010_0010) { u32 n = GetN(op); - r[n] = vbr; + ctx->r[n] = ctx->vbr; } @@ -61,21 +61,21 @@ sh4op(i0000_nnnn_0010_0010) sh4op(i0000_nnnn_0011_0010) { u32 n = GetN(op); - r[n] = ssr; + ctx->r[n] = ctx->ssr; } //stc SGR, sh4op(i0000_nnnn_0011_1010) { u32 n = GetN(op); - r[n] = sgr; + ctx->r[n] = ctx->sgr; } //stc SPC, sh4op(i0000_nnnn_0100_0010) { u32 n = GetN(op); - r[n] = spc; + ctx->r[n] = ctx->spc; } @@ -84,21 +84,21 @@ sh4op(i0000_nnnn_1mmm_0010) { u32 n = GetN(op); u32 m = GetM(op) & 0x7; - r[n] = r_bank[m]; + ctx->r[n] = ctx->r_bank[m]; } //sts FPUL, sh4op(i0000_nnnn_0101_1010) { u32 n = GetN(op); - r[n] = fpul; + ctx->r[n] = ctx->fpul; } //stc DBR, sh4op(i0000_nnnn_1111_1010) { u32 n = GetN(op); - r[n] = dbr; + ctx->r[n] = ctx->dbr; } @@ -106,7 +106,7 @@ sh4op(i0000_nnnn_1111_1010) sh4op(i0000_nnnn_0000_1010) { u32 n = GetN(op); - r[n] = mac.h; + ctx->r[n] = ctx->mac.h; } @@ -114,7 +114,7 @@ sh4op(i0000_nnnn_0000_1010) sh4op(i0000_nnnn_0001_1010) { u32 n = GetN(op); - r[n]=mac.l; + ctx->r[n] = ctx->mac.l; } @@ -122,7 +122,7 @@ sh4op(i0000_nnnn_0001_1010) sh4op(i0000_nnnn_0010_1010) { u32 n = GetN(op); - r[n] = pr; + ctx->r[n] = ctx->pr; } @@ -131,7 +131,7 @@ sh4op(i0000_nnnn_mmmm_1100) { u32 n = GetN(op); u32 m = GetM(op); - ReadMemBOS8(r[n],r[0],r[m]); + ReadMemBOS8(ctx->r[n], ctx->r[0], ctx->r[m]); } @@ -140,7 +140,7 @@ sh4op(i0000_nnnn_mmmm_1101) { u32 n = GetN(op); u32 m = GetM(op); - ReadMemBOS16(r[n],r[0],r[m]); + ReadMemBOS16(ctx->r[n], ctx->r[0], ctx->r[m]); } @@ -150,7 +150,7 @@ sh4op(i0000_nnnn_mmmm_1110) u32 n = GetN(op); u32 m = GetM(op); - ReadMemBOU32(r[n],r[0],r[m]); + ReadMemBOU32(ctx->r[n], ctx->r[0], ctx->r[m]); } //mov.b ,@(R0,) @@ -159,7 +159,7 @@ sh4op(i0000_nnnn_mmmm_0100) u32 n = GetN(op); u32 m = GetM(op); - WriteMemBOU8(r[0],r[n], r[m]); + WriteMemBOU8(ctx->r[0], ctx->r[n], ctx->r[m]); } @@ -168,7 +168,7 @@ sh4op(i0000_nnnn_mmmm_0101) { u32 n = GetN(op); u32 m = GetM(op); - WriteMemBOU16(r[0] , r[n], r[m]); + WriteMemBOU16(ctx->r[0] , ctx->r[n], ctx->r[m]); } @@ -177,11 +177,10 @@ sh4op(i0000_nnnn_mmmm_0110) { u32 n = GetN(op); u32 m = GetM(op); - WriteMemBOU32(r[0], r[n], r[m]); + WriteMemBOU32(ctx->r[0], ctx->r[n], ctx->r[m]); } - // // 1xxx @@ -191,7 +190,7 @@ sh4op(i0001_nnnn_mmmm_iiii) u32 n = GetN(op); u32 m = GetM(op); u32 disp = GetImm4(op); - WriteMemBOU32(r[n] , (disp <<2), r[m]); + WriteMemBOU32(ctx->r[n] , (disp <<2), ctx->r[m]); } // @@ -202,7 +201,7 @@ sh4op(i0010_nnnn_mmmm_0000) { u32 n = GetN(op); u32 m = GetM(op); - WriteMemU8(r[n],r[m] ); + WriteMemU8(ctx->r[n], ctx->r[m]); } // mov.w ,@ @@ -210,7 +209,7 @@ sh4op(i0010_nnnn_mmmm_0001) { u32 n = GetN(op); u32 m = GetM(op); - WriteMemU16(r[n],r[m]); + WriteMemU16(ctx->r[n], ctx->r[m]); } // mov.l ,@ @@ -218,7 +217,7 @@ sh4op(i0010_nnnn_mmmm_0010) { u32 n = GetN(op); u32 m = GetM(op); - WriteMemU32(r[n],r[m]); + WriteMemU32(ctx->r[n], ctx->r[m]); } // mov.b ,@- @@ -227,9 +226,9 @@ sh4op(i0010_nnnn_mmmm_0100) u32 n = GetN(op); u32 m = GetM(op); - u32 addr = r[n] - 1; - WriteMemBOU8(r[n], (u32)-1, r[m]); - r[n] = addr; + u32 addr = ctx->r[n] - 1; + WriteMemBOU8(ctx->r[n], (u32)-1, ctx->r[m]); + ctx->r[n] = addr; } //mov.w ,@- @@ -238,9 +237,9 @@ sh4op(i0010_nnnn_mmmm_0101) u32 n = GetN(op); u32 m = GetM(op); - u32 addr = r[n] - 2; - WriteMemU16(addr, r[m]); - r[n] = addr; + u32 addr = ctx->r[n] - 2; + WriteMemU16(addr, ctx->r[m]); + ctx->r[n] = addr; } //mov.l ,@- @@ -249,21 +248,21 @@ sh4op(i0010_nnnn_mmmm_0110) u32 n = GetN(op); u32 m = GetM(op); - u32 addr = r[n] - 4; - WriteMemU32(addr, r[m]); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->r[m]); + ctx->r[n] = addr; } - // +// // 4xxx //sts.l FPUL,@- sh4op(i0100_nnnn_0101_0010) { u32 n = GetN(op); - u32 addr = r[n] - 4; - WriteMemU32(addr, fpul); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->fpul); + ctx->r[n] = addr; } //sts.l MACH,@- @@ -271,9 +270,9 @@ sh4op(i0100_nnnn_0000_0010) { u32 n = GetN(op); - u32 addr = r[n] - 4; - WriteMemU32(addr, mac.h); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->mac.h); + ctx->r[n] = addr; } @@ -282,9 +281,9 @@ sh4op(i0100_nnnn_0001_0010) { u32 n = GetN(op); - u32 addr = r[n] - 4; - WriteMemU32(addr, mac.l); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->mac.l); + ctx->r[n] = addr; } @@ -293,9 +292,9 @@ sh4op(i0100_nnnn_0010_0010) { u32 n = GetN(op); - u32 addr = r[n] - 4; - WriteMemU32(addr,pr); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->pr); + ctx->r[n] = addr; } //sts.l DBR,@- @@ -303,9 +302,9 @@ sh4op(i0100_nnnn_1111_0010) { u32 n = GetN(op); - u32 addr = r[n] - 4; - WriteMemU32(addr,dbr); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->dbr); + ctx->r[n] = addr; } //stc.l GBR,@- @@ -313,9 +312,9 @@ sh4op(i0100_nnnn_0001_0011) { u32 n = GetN(op); - u32 addr = r[n] - 4; - WriteMemU32(addr, gbr); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->gbr); + ctx->r[n] = addr; } @@ -324,9 +323,9 @@ sh4op(i0100_nnnn_0010_0011) { u32 n = GetN(op); - u32 addr = r[n] - 4; - WriteMemU32(addr, vbr); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->vbr); + ctx->r[n] = addr; } @@ -335,18 +334,18 @@ sh4op(i0100_nnnn_0011_0011) { u32 n = GetN(op); - u32 addr = r[n] - 4; - WriteMemU32(addr, ssr); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->ssr); + ctx->r[n] = addr; } //stc.l SGR,@- sh4op(i0100_nnnn_0011_0010) { u32 n = GetN(op); - u32 addr = r[n] - 4; - WriteMemU32(addr, sgr); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->sgr); + ctx->r[n] = addr; } @@ -355,9 +354,9 @@ sh4op(i0100_nnnn_0100_0011) { u32 n = GetN(op); - u32 addr = r[n] - 4; - WriteMemU32(addr, spc); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->spc); + ctx->r[n] = addr; } //stc RM_BANK,@- @@ -366,20 +365,19 @@ sh4op(i0100_nnnn_1mmm_0011) u32 n = GetN(op); u32 m = GetM(op) & 0x07; - u32 addr = r[n] - 4; - WriteMemU32(addr, r_bank[m]); - r[n] = addr; + u32 addr = ctx->r[n] - 4; + WriteMemU32(addr, ctx->r_bank[m]); + ctx->r[n] = addr; } - //lds.l @+,MACH sh4op(i0100_nnnn_0000_0110) { u32 n = GetN(op); - ReadMemU32(mac.h,r[n]); + ReadMemU32(ctx->mac.h, ctx->r[n]); - r[n] += 4; + ctx->r[n] += 4; } @@ -387,9 +385,9 @@ sh4op(i0100_nnnn_0000_0110) sh4op(i0100_nnnn_0001_0110) { u32 n = GetN(op); - ReadMemU32(mac.l,r[n]); + ReadMemU32(ctx->mac.l, ctx->r[n]); - r[n] += 4; + ctx->r[n] += 4; } @@ -397,9 +395,9 @@ sh4op(i0100_nnnn_0001_0110) sh4op(i0100_nnnn_0010_0110) { u32 n = GetN(op); - ReadMemU32(pr,r[n]); + ReadMemU32(ctx->pr, ctx->r[n]); - r[n] += 4; + ctx->r[n] += 4; } @@ -408,8 +406,8 @@ sh4op(i0100_nnnn_0101_0110) { u32 n = GetN(op); - ReadMemU32(fpul,r[n]); - r[n] += 4; + ReadMemU32(ctx->fpul, ctx->r[n]); + ctx->r[n] += 4; } //lds.l @+,DBR @@ -417,8 +415,8 @@ sh4op(i0100_nnnn_1111_0110) { u32 n = GetN(op); - ReadMemU32(dbr,r[n]); - r[n] += 4; + ReadMemU32(ctx->dbr, ctx->r[n]); + ctx->r[n] += 4; } @@ -427,8 +425,8 @@ sh4op(i0100_nnnn_0001_0111) { u32 n = GetN(op); - ReadMemU32(gbr,r[n]); - r[n] += 4; + ReadMemU32(ctx->gbr, ctx->r[n]); + ctx->r[n] += 4; } @@ -437,8 +435,8 @@ sh4op(i0100_nnnn_0010_0111) { u32 n = GetN(op); - ReadMemU32(vbr,r[n]); - r[n] += 4; + ReadMemU32(ctx->vbr, ctx->r[n]); + ctx->r[n] += 4; } @@ -447,8 +445,8 @@ sh4op(i0100_nnnn_0011_0111) { u32 n = GetN(op); - ReadMemU32(ssr,r[n]); - r[n] += 4; + ReadMemU32(ctx->ssr, ctx->r[n]); + ctx->r[n] += 4; } //ldc.l @+,SGR @@ -456,8 +454,8 @@ sh4op(i0100_nnnn_0011_0110) { u32 n = GetN(op); - ReadMemU32(sgr,r[n]); - r[n] += 4; + ReadMemU32(ctx->sgr, ctx->r[n]); + ctx->r[n] += 4; } //ldc.l @+,SPC @@ -465,8 +463,8 @@ sh4op(i0100_nnnn_0100_0111) { u32 n = GetN(op); - ReadMemU32(spc,r[n]); - r[n] += 4; + ReadMemU32(ctx->spc, ctx->r[n]); + ctx->r[n] += 4; } @@ -476,15 +474,15 @@ sh4op(i0100_nnnn_1mmm_0111) u32 n = GetN(op); u32 m = GetM(op) & 7; - ReadMemU32(r_bank[m],r[n]); - r[n] += 4; + ReadMemU32(ctx->r_bank[m], ctx->r[n]); + ctx->r[n] += 4; } //lds ,MACH sh4op(i0100_nnnn_0000_1010) { u32 n = GetN(op); - mac.h = r[n]; + ctx->mac.h = ctx->r[n]; } @@ -492,7 +490,7 @@ sh4op(i0100_nnnn_0000_1010) sh4op(i0100_nnnn_0001_1010) { u32 n = GetN(op); - mac.l = r[n]; + ctx->mac.l = ctx->r[n]; } @@ -500,7 +498,7 @@ sh4op(i0100_nnnn_0001_1010) sh4op(i0100_nnnn_0010_1010) { u32 n = GetN(op); - pr = r[n]; + ctx->pr = ctx->r[n]; } @@ -508,27 +506,23 @@ sh4op(i0100_nnnn_0010_1010) sh4op(i0100_nnnn_0101_1010) { u32 n = GetN(op); - fpul =r[n]; + ctx->fpul = ctx->r[n]; } - - //ldc ,DBR sh4op(i0100_nnnn_1111_1010) { u32 n = GetN(op); - dbr = r[n]; + ctx->dbr = ctx->r[n]; } - - //ldc ,GBR sh4op(i0100_nnnn_0001_1110) { u32 n = GetN(op); - gbr = r[n]; + ctx->gbr = ctx->r[n]; } @@ -537,7 +531,7 @@ sh4op(i0100_nnnn_0010_1110) { u32 n = GetN(op); - vbr = r[n]; + ctx->vbr = ctx->r[n]; } @@ -546,7 +540,7 @@ sh4op(i0100_nnnn_0011_1110) { u32 n = GetN(op); - ssr = r[n]; + ctx->ssr = ctx->r[n]; } //ldc ,SGR @@ -554,7 +548,7 @@ sh4op(i0100_nnnn_0011_1010) { u32 n = GetN(op); - sgr = r[n]; + ctx->sgr = ctx->r[n]; } //ldc ,SPC @@ -562,7 +556,7 @@ sh4op(i0100_nnnn_0100_1110) { u32 n = GetN(op); - spc = r[n]; + ctx->spc = ctx->r[n]; } @@ -572,10 +566,10 @@ sh4op(i0100_nnnn_1mmm_1110) u32 n = GetN(op); u32 m = GetM(op) & 7; - r_bank[m] = r[n]; + ctx->r_bank[m] = ctx->r[n]; } - // +// // 5xxx //mov.l @(,), @@ -585,7 +579,7 @@ sh4op(i0101_nnnn_mmmm_iiii) u32 m = GetM(op); u32 disp = GetImm4(op) << 2; - ReadMemBOU32(r[n],r[m],disp); + ReadMemBOU32(ctx->r[n], ctx->r[m], disp); } // @@ -596,7 +590,7 @@ sh4op(i0110_nnnn_mmmm_0000) u32 n = GetN(op); u32 m = GetM(op); - ReadMemS8(r[n],r[m]); + ReadMemS8(ctx->r[n], ctx->r[m]); } @@ -605,7 +599,7 @@ sh4op(i0110_nnnn_mmmm_0001) { u32 n = GetN(op); u32 m = GetM(op); - ReadMemS16(r[n] ,r[m]); + ReadMemS16(ctx->r[n], ctx->r[m]); } @@ -615,7 +609,7 @@ sh4op(i0110_nnnn_mmmm_0010) u32 n = GetN(op); u32 m = GetM(op); - ReadMemU32(r[n],r[m]); + ReadMemU32(ctx->r[n], ctx->r[m]); } @@ -624,7 +618,7 @@ sh4op(i0110_nnnn_mmmm_0011) { u32 n = GetN(op); u32 m = GetM(op); - r[n] = r[m]; + ctx->r[n] = ctx->r[m]; } @@ -633,9 +627,9 @@ sh4op(i0110_nnnn_mmmm_0100) { u32 n = GetN(op); u32 m = GetM(op); - ReadMemS8(r[n],r[m]); + ReadMemS8(ctx->r[n], ctx->r[m]); if (n != m) - r[m] += 1; + ctx->r[m] += 1; } @@ -644,9 +638,9 @@ sh4op(i0110_nnnn_mmmm_0101) { u32 n = GetN(op); u32 m = GetM(op); - ReadMemS16(r[n],r[m]); + ReadMemS16(ctx->r[n], ctx->r[m]); if (n != m) - r[m] += 2; + ctx->r[m] += 2; } @@ -657,19 +651,19 @@ sh4op(i0110_nnnn_mmmm_0110) u32 m = GetM(op); - ReadMemU32(r[n],r[m]); + ReadMemU32(ctx->r[n], ctx->r[m]); if (n != m) - r[m] += 4; + ctx->r[m] += 4; } // //8xxx - // mov.b R0,@(,) +// mov.b R0,@(,) sh4op(i1000_0000_mmmm_iiii) { u32 n = GetM(op); u32 disp = GetImm4(op); - WriteMemBOU8(r[n],disp,r[0]); + WriteMemBOU8(ctx->r[n], disp, ctx->r[0]); } @@ -678,7 +672,7 @@ sh4op(i1000_0001_mmmm_iiii) { u32 disp = GetImm4(op); u32 m = GetM(op); - WriteMemBOU16(r[m] , (disp << 1),r[0]); + WriteMemBOU16(ctx->r[m], (disp << 1), ctx->r[0]); } @@ -687,7 +681,7 @@ sh4op(i1000_0100_mmmm_iiii) { u32 disp = GetImm4(op); u32 m = GetM(op); - ReadMemBOS8(r[0] ,r[m] , disp); + ReadMemBOS8(ctx->r[0], ctx->r[m] , disp); } @@ -696,10 +690,10 @@ sh4op(i1000_0101_mmmm_iiii) { u32 disp = GetImm4(op); u32 m = GetM(op); - ReadMemBOS16(r[0],r[m] , (disp << 1)); + ReadMemBOS16(ctx->r[0], ctx->r[m], (disp << 1)); } - // +// // 9xxx //mov.w @(,PC), @@ -707,17 +701,17 @@ sh4op(i1001_nnnn_iiii_iiii) { u32 n = GetN(op); u32 disp = GetImm8(op); - ReadMemS16(r[n],(disp<<1) + next_pc + 2); + ReadMemS16(ctx->r[n], (disp << 1) + ctx->pc + 2); } - // +// // Cxxx // mov.b R0,@(,GBR) sh4op(i1100_0000_iiii_iiii) { u32 disp = GetImm8(op); - WriteMemBOU8(gbr, disp, r[0]); + WriteMemBOU8(ctx->gbr, disp, ctx->r[0]); } @@ -725,7 +719,7 @@ sh4op(i1100_0000_iiii_iiii) sh4op(i1100_0001_iiii_iiii) { u32 disp = GetImm8(op); - WriteMemBOU16(gbr, (disp << 1), r[0]); + WriteMemBOU16(ctx->gbr, (disp << 1), ctx->r[0]); } @@ -733,14 +727,14 @@ sh4op(i1100_0001_iiii_iiii) sh4op(i1100_0010_iiii_iiii) { u32 disp = GetImm8(op); - WriteMemBOU32(gbr, (disp << 2), r[0]); + WriteMemBOU32(ctx->gbr, (disp << 2), ctx->r[0]); } // mov.b @(,GBR),R0 sh4op(i1100_0100_iiii_iiii) { u32 disp = GetImm8(op); - ReadMemBOS8(r[0],gbr,disp); + ReadMemBOS8(ctx->r[0], ctx->gbr, disp); } @@ -748,7 +742,7 @@ sh4op(i1100_0100_iiii_iiii) sh4op(i1100_0101_iiii_iiii) { u32 disp = GetImm8(op); - ReadMemBOS16(r[0],gbr,(disp<<1)); + ReadMemBOS16(ctx->r[0], ctx->gbr, (disp << 1)); } @@ -757,14 +751,14 @@ sh4op(i1100_0110_iiii_iiii) { u32 disp = GetImm8(op); - ReadMemBOU32(r[0],gbr,(disp<<2)); + ReadMemBOU32(ctx->r[0], ctx->gbr, (disp << 2)); } // mova @(,PC),R0 sh4op(i1100_0111_iiii_iiii) { - r[0] = ((next_pc+2)&0xFFFFFFFC)+(GetImm8(op)<<2); + ctx->r[0] = ((ctx->pc + 2) & 0xFFFFFFFC) + (GetImm8(op) << 2); } // @@ -774,10 +768,11 @@ sh4op(i1100_0111_iiii_iiii) sh4op(i1101_nnnn_iiii_iiii) { u32 n = GetN(op); - u32 disp = (GetImm8(op)); + u32 disp = GetImm8(op); - ReadMemU32(r[n],(disp<<2) + ((next_pc+2) & 0xFFFFFFFC)); + ReadMemU32(ctx->r[n], (disp << 2) + ((ctx->pc + 2) & 0xFFFFFFFC)); } + // // Exxx @@ -785,7 +780,7 @@ sh4op(i1101_nnnn_iiii_iiii) sh4op(i1110_nnnn_iiii_iiii) { u32 n = GetN(op); - r[n] = (u32)(s32)(s8)(GetSImm8(op)); + ctx->r[n] = (u32)(s32)(s8)GetSImm8(op); } @@ -793,39 +788,40 @@ sh4op(i1110_nnnn_iiii_iiii) sh4op(i0000_nnnn_1100_0011) { u32 n = GetN(op); - WriteMemU32(r[n],r[0]);//at r[n],r[0] + WriteMemU32(ctx->r[n], ctx->r[0]); // TODO ocache } //clrmac sh4op(i0000_0000_0010_1000) { - mac.full=0; + ctx->mac.full = 0; } static void executeDelaySlot() { - static_cast(emu.getSh4Executor())->ExecuteDelayslot(); + Sh4Interpreter::Instance->ExecuteDelayslot(); } //braf sh4op(i0000_nnnn_0010_0011) { u32 n = GetN(op); - u32 newpc = r[n] + next_pc + 2;// + u32 newpc = ctx->r[n] + ctx->pc + 2; executeDelaySlot(); //WARN : r[n] can change here - next_pc = newpc; + ctx->pc = newpc; } + //bsrf sh4op(i0000_nnnn_0000_0011) { u32 n = GetN(op); - u32 newpc = r[n] + next_pc +2; - u32 newpr = next_pc + 2; + u32 newpc = ctx->r[n] + ctx->pc +2; + u32 newpr = ctx->pc + 2; executeDelaySlot(); //WARN : pr and r[n] can change here - pr = newpr; - next_pc = newpc; + ctx->pr = newpr; + ctx->pc = newpc; debugger::subroutineCall(); } @@ -833,9 +829,9 @@ sh4op(i0000_nnnn_0000_0011) //rte sh4op(i0000_0000_0010_1011) { - u32 newpc = spc; - static_cast(emu.getSh4Executor())->ExecuteDelayslot_RTE(); - next_pc = newpc; + u32 newpc = ctx->spc; + Sh4Interpreter::Instance->ExecuteDelayslot_RTE(); + ctx->pc = newpc; if (UpdateSR()) UpdateINTC(); debugger::subroutineReturn(); @@ -845,23 +841,24 @@ sh4op(i0000_0000_0010_1011) //rts sh4op(i0000_0000_0000_1011) { - u32 newpc=pr; + u32 newpc = ctx->pr; executeDelaySlot(); //WARN : pr can change here - next_pc=newpc; + ctx->pc = newpc; debugger::subroutineReturn(); } -u32 branch_target_s8(u32 op) +u32 branch_target_s8(Sh4Context *ctx, u32 op) { - return GetSImm8(op)*2 + 2 + next_pc; + return GetSImm8(op) * 2 + 2 + ctx->pc; } + // bf sh4op(i1000_1011_iiii_iiii) { - if (sr.T==0) + if (ctx->sr.T == 0) { //direct jump - next_pc = branch_target_s8(op); + ctx->pc = branch_target_s8(ctx, op); } } @@ -869,12 +866,12 @@ sh4op(i1000_1011_iiii_iiii) // bf.s sh4op(i1000_1111_iiii_iiii) { - if (sr.T==0) + if (ctx->sr.T == 0) { //delay 1 instruction - u32 newpc=branch_target_s8(op); + u32 newpc = branch_target_s8(ctx, op); executeDelaySlot(); - next_pc = newpc; + ctx->pc = newpc; } } @@ -882,10 +879,10 @@ sh4op(i1000_1111_iiii_iiii) // bt sh4op(i1000_1001_iiii_iiii) { - if (sr.T != 0) + if (ctx->sr.T != 0) { //direct jump - next_pc = branch_target_s8(op); + ctx->pc = branch_target_s8(ctx, op); } } @@ -893,37 +890,37 @@ sh4op(i1000_1001_iiii_iiii) // bt.s sh4op(i1000_1101_iiii_iiii) { - if (sr.T != 0) + if (ctx->sr.T != 0) { //delay 1 instruction - u32 newpc=branch_target_s8(op); + u32 newpc = branch_target_s8(ctx, op); executeDelaySlot(); - next_pc = newpc; + ctx->pc = newpc; } } -u32 branch_target_s12(u32 op) +static u32 branch_target_s12(Sh4Context *ctx, u32 op) { - return GetSImm12(op)*2 + 2 + next_pc; + return GetSImm12(op) * 2 + 2 + ctx->pc; } // bra sh4op(i1010_iiii_iiii_iiii) { - u32 newpc = branch_target_s12(op); + u32 newpc = branch_target_s12(ctx, op); executeDelaySlot(); - next_pc=newpc; + ctx->pc = newpc; } // bsr sh4op(i1011_iiii_iiii_iiii) { - u32 newpr = next_pc + 2; //return after delayslot - u32 newpc = branch_target_s12(op); + u32 newpr = ctx->pc + 2; //return after delayslot + u32 newpc = branch_target_s12(ctx, op); executeDelaySlot(); - pr = newpr; - next_pc = newpc; + ctx->pr = newpr; + ctx->pc = newpc; debugger::subroutineCall(); } @@ -933,7 +930,7 @@ sh4op(i1100_0011_iiii_iiii) WARN_LOG(INTERPRETER, "TRAP #%X", GetImm8(op)); debugger::debugTrap(Sh4Ex_Trap); CCN_TRA = (GetImm8(op) << 2); - Do_Exception(next_pc, Sh4Ex_Trap); + Do_Exception(ctx->pc, Sh4Ex_Trap); } //jmp @ @@ -941,9 +938,9 @@ sh4op(i0100_nnnn_0010_1011) { u32 n = GetN(op); - u32 newpc=r[n]; + u32 newpc = ctx->r[n]; executeDelaySlot(); //r[n] can change here - next_pc=newpc; + ctx->pc = newpc; } //jsr @ @@ -951,12 +948,12 @@ sh4op(i0100_nnnn_0000_1011) { u32 n = GetN(op); - u32 newpr = next_pc + 2; //return after delayslot - u32 newpc= r[n]; + u32 newpr = ctx->pc + 2; //return after delayslot + u32 newpc = ctx->r[n]; executeDelaySlot(); //r[n]/pr can change here - pr = newpr; - next_pc = newpc; + ctx->pr = newpr; + ctx->pc = newpc; debugger::subroutineCall(); } @@ -964,8 +961,7 @@ sh4op(i0100_nnnn_0000_1011) sh4op(i0000_0000_0001_1011) { //just wait for an Interrupt - - int i=0,s=1; + int i = 0, s = 1; while (!UpdateSystem_INTC())//448 { @@ -976,9 +972,8 @@ sh4op(i0000_0000_0001_1011) } } //if not Interrupted , we must rexecute the sleep - if (s==0) - next_pc-=2;// re execute sleep - + if (s == 0) + ctx->pc -= 2;// re execute sleep } @@ -987,7 +982,7 @@ sh4op(i0011_nnnn_mmmm_1000) { u32 n = GetN(op); u32 m = GetM(op); - r[n] -=r[m]; + ctx->r[n] -= ctx->r[m]; } //add , @@ -995,10 +990,10 @@ sh4op(i0011_nnnn_mmmm_1100) { u32 n = GetN(op); u32 m = GetM(op); - r[n] +=r[m]; + ctx->r[n] += ctx->r[m]; } - // +// // 7xxx //add #, @@ -1006,7 +1001,7 @@ sh4op(i0111_nnnn_iiii_iiii) { u32 n = GetN(op); s32 stmp1 = GetSImm8(op); - r[n] +=stmp1; + ctx->r[n] += stmp1; } //Bitwise logical operations @@ -1017,7 +1012,7 @@ sh4op(i0010_nnnn_mmmm_1001) { u32 n = GetN(op); u32 m = GetM(op); - r[n] &= r[m]; + ctx->r[n] &= ctx->r[m]; } //xor , @@ -1025,7 +1020,7 @@ sh4op(i0010_nnnn_mmmm_1010) { u32 n = GetN(op); u32 m = GetM(op); - r[n] ^= r[m]; + ctx->r[n] ^= ctx->r[m]; } //or , @@ -1033,7 +1028,7 @@ sh4op(i0010_nnnn_mmmm_1011) { u32 n = GetN(op); u32 m = GetM(op); - r[n] |= r[m]; + ctx->r[n] |= ctx->r[m]; } @@ -1041,7 +1036,7 @@ sh4op(i0010_nnnn_mmmm_1011) sh4op(i0100_nnnn_0000_1000) { u32 n = GetN(op); - r[n] <<= 2; + ctx->r[n] <<= 2; } @@ -1049,7 +1044,7 @@ sh4op(i0100_nnnn_0000_1000) sh4op(i0100_nnnn_0001_1000) { u32 n = GetN(op); - r[n] <<= 8; + ctx->r[n] <<= 8; } @@ -1057,7 +1052,7 @@ sh4op(i0100_nnnn_0001_1000) sh4op(i0100_nnnn_0010_1000) { u32 n = GetN(op); - r[n] <<= 16; + ctx->r[n] <<= 16; } @@ -1065,7 +1060,7 @@ sh4op(i0100_nnnn_0010_1000) sh4op(i0100_nnnn_0000_1001) { u32 n = GetN(op); - r[n] >>= 2; + ctx->r[n] >>= 2; } @@ -1073,7 +1068,7 @@ sh4op(i0100_nnnn_0000_1001) sh4op(i0100_nnnn_0001_1001) { u32 n = GetN(op); - r[n] >>= 8; + ctx->r[n] >>= 8; } @@ -1081,22 +1076,22 @@ sh4op(i0100_nnnn_0001_1001) sh4op(i0100_nnnn_0010_1001) { u32 n = GetN(op); - r[n] >>= 16; + ctx->r[n] >>= 16; } // and #,R0 sh4op(i1100_1001_iiii_iiii) { u32 imm = GetImm8(op); - r[0] &= imm; + ctx->r[0] &= imm; } // xor #,R0 sh4op(i1100_1010_iiii_iiii) { - u32 imm = GetImm8(op); - r[0] ^= imm; + u32 imm = GetImm8(op); + ctx->r[0] ^= imm; } @@ -1104,7 +1099,7 @@ sh4op(i1100_1010_iiii_iiii) sh4op(i1100_1011_iiii_iiii) { u32 imm = GetImm8(op); - r[0] |= imm; + ctx->r[0] |= imm; } @@ -1152,14 +1147,14 @@ sh4op(i0000_nnnn_1011_0011) sh4op(i0000_nnnn_1000_0011) { u32 n = GetN(op); - u32 Dest = r[n]; + u32 Dest = ctx->r[n]; if ((Dest >> 26) == 0x38) // Store Queue { if (CCN_MMUCR.AT) do_sqw_mmu(Dest); else - do_sqw_nommu(Dest, &p_sh4rcb->cntx); + do_sqw_nommu(Dest, ctx); } else { @@ -1174,32 +1169,32 @@ sh4op(i0000_nnnn_1000_0011) //sets sh4op(i0000_0000_0101_1000) { - sr.S = 1; + ctx->sr.S = 1; } //clrs sh4op(i0000_0000_0100_1000) { - sr.S = 0; + ctx->sr.S = 0; } //sett sh4op(i0000_0000_0001_1000) { - sr.T = 1; + ctx->sr.T = 1; } //clrt sh4op(i0000_0000_0000_1000) { - sr.T = 0; + ctx->sr.T = 0; } //movt sh4op(i0000_nnnn_0010_1001) { u32 n = GetN(op); - r[n] = sr.T; + ctx->r[n] = ctx->sr.T; } //************************ Reg Compares ************************ @@ -1208,30 +1203,30 @@ sh4op(i0100_nnnn_0001_0001) { u32 n = GetN(op); - if (((s32)r[n]) >= 0) - sr.T = 1; + if (((s32)ctx->r[n]) >= 0) + ctx->sr.T = 1; else - sr.T = 0; + ctx->sr.T = 0; } //cmp/pl sh4op(i0100_nnnn_0001_0101) { u32 n = GetN(op); - if ((s32)r[n] > 0) - sr.T = 1; + if ((s32)ctx->r[n] > 0) + ctx->sr.T = 1; else - sr.T = 0; + ctx->sr.T = 0; } //cmp/eq #,R0 sh4op(i1000_1000_iiii_iiii) { u32 imm = (u32)(s32)(GetSImm8(op)); - if (r[0] == imm) - sr.T =1; + if (ctx->r[0] == imm) + ctx->sr.T = 1; else - sr.T =0; + ctx->sr.T = 0; } //cmp/eq , @@ -1240,10 +1235,10 @@ sh4op(i0011_nnnn_mmmm_0000) u32 n = GetN(op); u32 m = GetM(op); - if (r[m] == r[n]) - sr.T = 1; + if (ctx->r[m] == ctx->r[n]) + ctx->sr.T = 1; else - sr.T = 0; + ctx->sr.T = 0; } //cmp/hs , @@ -1251,10 +1246,10 @@ sh4op(i0011_nnnn_mmmm_0010) { u32 n = GetN(op); u32 m = GetM(op); - if (r[n] >= r[m]) - sr.T=1; + if (ctx->r[n] >= ctx->r[m]) + ctx->sr.T = 1; else - sr.T=0; + ctx->sr.T = 0; } //cmp/ge , @@ -1262,10 +1257,10 @@ sh4op(i0011_nnnn_mmmm_0011) { u32 n = GetN(op); u32 m = GetM(op); - if ((s32)r[n] >= (s32)r[m]) - sr.T = 1; + if ((s32)ctx->r[n] >= (s32)ctx->r[m]) + ctx->sr.T = 1; else - sr.T = 0; + ctx->sr.T = 0; } //cmp/hi , @@ -1274,10 +1269,10 @@ sh4op(i0011_nnnn_mmmm_0110) u32 n = GetN(op); u32 m = GetM(op); - if (r[n] > r[m]) - sr.T=1; + if (ctx->r[n] > ctx->r[m]) + ctx->sr.T = 1; else - sr.T=0; + ctx->sr.T = 0; } //cmp/gt , @@ -1286,10 +1281,10 @@ sh4op(i0011_nnnn_mmmm_0111) u32 n = GetN(op); u32 m = GetM(op); - if (((s32)r[n]) > ((s32)r[m])) - sr.T = 1; + if ((s32)ctx->r[n] > (s32)ctx->r[m]) + ctx->sr.T = 1; else - sr.T = 0; + ctx->sr.T = 0; } //cmp/str , @@ -1302,27 +1297,27 @@ sh4op(i0010_nnnn_mmmm_1100) u32 temp; u32 HH, HL, LH, LL; - temp = r[n] ^ r[m]; + temp = ctx->r[n] ^ ctx->r[m]; - HH=(temp&0xFF000000)>>24; - HL=(temp&0x00FF0000)>>16; - LH=(temp&0x0000FF00)>>8; - LL=temp&0x000000FF; - HH=HH&&HL&&LH&&LL; - if (HH==0) - sr.T=1; + HH = (temp & 0xFF000000) >> 24; + HL = (temp & 0x00FF0000) >> 16; + LH = (temp & 0x0000FF00) >> 8; + LL = temp & 0x000000FF; + HH = HH && HL && LH && LL; + if (HH == 0) + ctx->sr.T = 1; else - sr.T=0; + ctx->sr.T = 0; } //tst #,R0 sh4op(i1100_1000_iiii_iiii) { - u32 utmp1 = r[0] & GetImm8(op); + u32 utmp1 = ctx->r[0] & GetImm8(op); if (utmp1 == 0) - sr.T = 1; + ctx->sr.T = 1; else - sr.T = 0; + ctx->sr.T = 0; } //tst , sh4op(i0010_nnnn_mmmm_1000) @@ -1330,10 +1325,10 @@ sh4op(i0010_nnnn_mmmm_1000) u32 n = GetN(op); u32 m = GetM(op); - if ((r[n] & r[m])!=0) - sr.T=0; + if ((ctx->r[n] & ctx->r[m]) != 0) + ctx->sr.T = 0; else - sr.T=1; + ctx->sr.T = 1; } //************************ mulls! ************************ @@ -1342,7 +1337,7 @@ sh4op(i0010_nnnn_mmmm_1110) { u32 n = GetN(op); u32 m = GetM(op); - mac.l = (u16)r[n] * (u16)r[m]; + ctx->mac.l = (u16)ctx->r[n] * (u16)ctx->r[m]; } //muls.w , @@ -1351,7 +1346,7 @@ sh4op(i0010_nnnn_mmmm_1111) u32 n = GetN(op); u32 m = GetM(op); - mac.l = (u32)((s16)r[n] * (s16)r[m]); + ctx->mac.l = (u32)((s16)ctx->r[n] * (s16)ctx->r[m]); } //dmulu.l , sh4op(i0011_nnnn_mmmm_0101) @@ -1359,7 +1354,7 @@ sh4op(i0011_nnnn_mmmm_0101) u32 n = GetN(op); u32 m = GetM(op); - mac.full = (u64)r[n] * (u64)r[m]; + ctx->mac.full = (u64)ctx->r[n] * (u64)ctx->r[m]; } //dmuls.l , @@ -1368,7 +1363,7 @@ sh4op(i0011_nnnn_mmmm_1101) u32 n = GetN(op); u32 m = GetM(op); - mac.full = (s64)(s32)r[n] * (s64)(s32)r[m]; + ctx->mac.full = (s64)(s32)ctx->r[n] * (s64)(s32)ctx->r[m]; } @@ -1377,7 +1372,7 @@ sh4op(i0100_nnnn_mmmm_1111) { u32 n = GetN(op); u32 m = GetM(op); - if (sr.S!=0) + if (ctx->sr.S != 0) { die("mac.w @+,@+ : S=1"); } @@ -1385,14 +1380,14 @@ sh4op(i0100_nnnn_mmmm_1111) { s32 rm,rn; - rn = (s32)(s16)ReadMem16(r[n]); - rm = (s32)(s16)ReadMem16(r[m] + (n == m ? 2 : 0)); + rn = (s32)(s16)ReadMem16(ctx->r[n]); + rm = (s32)(s16)ReadMem16(ctx->r[m] + (n == m ? 2 : 0)); - r[n]+=2; - r[m]+=2; + ctx->r[n] += 2; + ctx->r[m] += 2; - s32 mul=rm * rn; - mac.full+=(s64)mul; + s32 mul = rm * rn; + ctx->mac.full += (s64)mul; } } //mac.l @+,@+ @@ -1402,14 +1397,14 @@ sh4op(i0000_nnnn_mmmm_1111) u32 m = GetM(op); s32 rm, rn; - verify(sr.S==0); + verify(ctx->sr.S == 0); - ReadMemS32(rm,r[m]); - ReadMemS32(rn,r[n] + (n == m ? 4 : 0)); - r[m] += 4; - r[n] += 4; + ReadMemS32(rm, ctx->r[m]); + ReadMemS32(rn, ctx->r[n] + (n == m ? 4 : 0)); + ctx->r[m] += 4; + ctx->r[n] += 4; - mac.full += (s64)rm * (s64)rn; + ctx->mac.full += (s64)rm * (s64)rn; } //mul.l , @@ -1417,15 +1412,15 @@ sh4op(i0000_nnnn_mmmm_0111) { u32 n = GetN(op); u32 m = GetM(op); - mac.l = (u32)((((s32)r[n]) * ((s32)r[m]))); + ctx->mac.l = (u32)((((s32)ctx->r[n]) * ((s32)ctx->r[m]))); } //************************ Div ! ************************ //div0u sh4op(i0000_0000_0001_1001) { - sr.Q = 0; - sr.M = 0; - sr.T = 0; + ctx->sr.Q = 0; + ctx->sr.M = 0; + ctx->sr.T = 0; } //div0s , sh4op(i0010_nnnn_mmmm_0111) @@ -1433,9 +1428,9 @@ sh4op(i0010_nnnn_mmmm_0111) u32 n = GetN(op); u32 m = GetM(op); - sr.Q = r[n] >> 31; - sr.M = r[m] >> 31; - sr.T = sr.M ^ sr.Q; + ctx->sr.Q = ctx->r[n] >> 31; + ctx->sr.M = ctx->r[m] >> 31; + ctx->sr.T = ctx->sr.M ^ ctx->sr.Q; } //div1 , @@ -1444,46 +1439,46 @@ sh4op(i0011_nnnn_mmmm_0100) u32 n = GetN(op); u32 m = GetM(op); - const u8 old_q = sr.Q; - sr.Q = (u8)((0x80000000 & r[n]) != 0); + const u8 old_q = ctx->sr.Q; + ctx->sr.Q = (u8)((0x80000000 & ctx->r[n]) != 0); - const u32 old_rm = r[m]; - r[n] <<= 1; - r[n] |= sr.T; + const u32 old_rm = ctx->r[m]; + ctx->r[n] <<= 1; + ctx->r[n] |= ctx->sr.T; - const u32 old_rn = r[n]; + const u32 old_rn = ctx->r[n]; if (old_q == 0) { - if (sr.M == 0) + if (ctx->sr.M == 0) { - r[n] -= old_rm; - bool tmp1 = r[n] > old_rn; - sr.Q = sr.Q ^ tmp1; + ctx->r[n] -= old_rm; + bool tmp1 = ctx->r[n] > old_rn; + ctx->sr.Q = ctx->sr.Q ^ tmp1; } else { - r[n] += old_rm; - bool tmp1 = r[n] < old_rn; - sr.Q = !sr.Q ^ tmp1; + ctx->r[n] += old_rm; + bool tmp1 = ctx->r[n] < old_rn; + ctx->sr.Q = !ctx->sr.Q ^ tmp1; } } else { - if (sr.M == 0) + if (ctx->sr.M == 0) { - r[n] += old_rm; - bool tmp1 = r[n] < old_rn; - sr.Q = sr.Q ^ tmp1; + ctx->r[n] += old_rm; + bool tmp1 = ctx->r[n] < old_rn; + ctx->sr.Q = ctx->sr.Q ^ tmp1; } else { - r[n] -= old_rm; - bool tmp1 = r[n] > old_rn; - sr.Q = !sr.Q ^ tmp1; + ctx->r[n] -= old_rm; + bool tmp1 = ctx->r[n] > old_rn; + ctx->sr.Q = !ctx->sr.Q ^ tmp1; } } - sr.T = (sr.Q == sr.M); + ctx->sr.T = (ctx->sr.Q == ctx->sr.M); } //************************ Simple maths ************************ @@ -1492,18 +1487,18 @@ sh4op(i0011_nnnn_mmmm_1110) { u32 n = GetN(op); u32 m = GetM(op); - u32 tmp1 = r[n] + r[m]; - u32 tmp0 = r[n]; + u32 tmp1 = ctx->r[n] + ctx->r[m]; + u32 tmp0 = ctx->r[n]; - r[n] = tmp1 + sr.T; + ctx->r[n] = tmp1 + ctx->sr.T; if (tmp0 > tmp1) - sr.T = 1; + ctx->sr.T = 1; else - sr.T = 0; + ctx->sr.T = 0; - if (tmp1 > r[n]) - sr.T = 1; + if (tmp1 > ctx->r[n]) + ctx->sr.T = 1; } // addv , @@ -1512,16 +1507,16 @@ sh4op(i0011_nnnn_mmmm_1111) //Retail game "Twinkle Star Sprites" "uses" this opcode. u32 n = GetN(op); u32 m = GetM(op); - s64 br=(s64)(s32)r[n]+(s64)(s32)r[m]; + s64 br = (s64)(s32)ctx->r[n] + (s64)(s32)ctx->r[m]; if (br >=0x80000000) - sr.T=1; - else if (br < (s64) (0xFFFFFFFF80000000u)) - sr.T=1; + ctx->sr.T = 1; + else if (br < (s64)0xFFFFFFFF80000000u) + ctx->sr.T = 1; else - sr.T=0; + ctx->sr.T = 0; - r[n]+=r[m]; + ctx->r[n] += ctx->r[m]; } //subc , @@ -1530,17 +1525,17 @@ sh4op(i0011_nnnn_mmmm_1010) u32 n = GetN(op); u32 m = GetM(op); - u32 tmp1 = r[n] - r[m]; - u32 tmp0 = r[n]; - r[n] = tmp1 - sr.T; + u32 tmp1 = ctx->r[n] - ctx->r[m]; + u32 tmp0 = ctx->r[n]; + ctx->r[n] = tmp1 - ctx->sr.T; if (tmp0 < tmp1) - sr.T=1; + ctx->sr.T = 1; else - sr.T=0; + ctx->sr.T = 0; - if (tmp1 < r[n]) - sr.T=1; + if (tmp1 < ctx->r[n]) + ctx->sr.T = 1; } //subv , @@ -1549,26 +1544,27 @@ sh4op(i0011_nnnn_mmmm_1011) //Retail game "Twinkle Star Sprites" "uses" this opcode. u32 n = GetN(op); u32 m = GetM(op); - s64 br=(s64)(s32)r[n]-(s64)(s32)r[m]; + s64 br = (s64)(s32)ctx->r[n] - (s64)(s32)ctx->r[m]; - if (br >=0x80000000) - sr.T=1; + if (br >= 0x80000000) + ctx->sr.T = 1; else if (br < (s64) (0xFFFFFFFF80000000u)) - sr.T=1; + ctx->sr.T = 1; else - sr.T=0; + ctx->sr.T = 0; - r[n]-=r[m]; + ctx->r[n] -= ctx->r[m]; } + //dt sh4op(i0100_nnnn_0001_0000) { u32 n = GetN(op); - r[n]-=1; - if (r[n] == 0) - sr.T=1; + ctx->r[n] -= 1; + if (ctx->r[n] == 0) + ctx->sr.T = 1; else - sr.T=0; + ctx->sr.T = 0; } //negc , @@ -1578,16 +1574,16 @@ sh4op(i0110_nnnn_mmmm_1010) u32 m = GetM(op); //r[n]=-r[m]-sr.T; - u32 tmp=0 - r[m]; - r[n]=tmp - sr.T; + u32 tmp = 0 - ctx->r[m]; + ctx->r[n] = tmp - ctx->sr.T; - if (0sr.T = 1; else - sr.T=0; + ctx->sr.T = 0; - if (tmpr[n]) + ctx->sr.T = 1; } @@ -1596,7 +1592,7 @@ sh4op(i0110_nnnn_mmmm_1011) { u32 n = GetN(op); u32 m = GetM(op); - r[n] = -r[m]; + ctx->r[n] = -ctx->r[m]; } //not , @@ -1605,7 +1601,7 @@ sh4op(i0110_nnnn_mmmm_0111) u32 n = GetN(op); u32 m = GetM(op); - r[n] = ~r[m]; + ctx->r[n] = ~ctx->r[m]; } @@ -1615,15 +1611,15 @@ sh4op(i0100_nnnn_0000_0000) { u32 n = GetN(op); - sr.T = r[n] >> 31; - r[n] <<= 1; + ctx->sr.T = ctx->r[n] >> 31; + ctx->r[n] <<= 1; } //shal sh4op(i0100_nnnn_0010_0000) { - u32 n=GetN(op); - sr.T=r[n]>>31; - r[n]=((s32)r[n])<<1; + u32 n = GetN(op); + ctx->sr.T = ctx->r[n] >> 31; + ctx->r[n] = ((s32)ctx->r[n]) << 1; } @@ -1631,8 +1627,8 @@ sh4op(i0100_nnnn_0010_0000) sh4op(i0100_nnnn_0000_0001) { u32 n = GetN(op); - sr.T = r[n] & 0x1; - r[n] >>= 1; + ctx->sr.T = ctx->r[n] & 0x1; + ctx->r[n] >>= 1; } //shar @@ -1640,8 +1636,8 @@ sh4op(i0100_nnnn_0010_0001) { u32 n = GetN(op); - sr.T=r[n] & 1; - r[n]=((s32)r[n])>>1; + ctx->sr.T = ctx->r[n] & 1; + ctx->r[n] = ((s32)ctx->r[n]) >> 1; } //shad , @@ -1649,15 +1645,13 @@ sh4op(i0100_nnnn_mmmm_1100) { u32 n = GetN(op); u32 m = GetM(op); - u32 sgn = r[m] & 0x80000000; + u32 sgn = ctx->r[m] & 0x80000000; if (sgn == 0) - r[n] <<= (r[m] & 0x1F); - else if ((r[m] & 0x1F) == 0) - { - r[n]=((s32)r[n])>>31; - } + ctx->r[n] <<= ctx->r[m] & 0x1F; + else if ((ctx->r[m] & 0x1F) == 0) + ctx->r[n] = (s32)ctx->r[n] >> 31; else - r[n] = ((s32)r[n]) >> ((~r[m] & 0x1F) + 1); + ctx->r[n] = (s32)ctx->r[n] >> ((~ctx->r[m] & 0x1F) + 1); } @@ -1666,32 +1660,24 @@ sh4op(i0100_nnnn_mmmm_1101) { u32 n = GetN(op); u32 m = GetM(op); - u32 sgn = r[m] & 0x80000000; + u32 sgn = ctx->r[m] & 0x80000000; if (sgn == 0) - r[n] <<= (r[m] & 0x1F); - else if ((r[m] & 0x1F) == 0) - { - r[n] = 0; - } + ctx->r[n] <<= (ctx->r[m] & 0x1F); + else if ((ctx->r[m] & 0x1F) == 0) + ctx->r[n] = 0; else - r[n] = ((u32)r[n]) >> ((~r[m] & 0x1F) + 1); //isn't this the same as -r[m] ? + ctx->r[n] = ((u32)ctx->r[n]) >> ((~ctx->r[m] & 0x1F) + 1); //isn't this the same as -r[m] ? } - //rotcl sh4op(i0100_nnnn_0010_0100) { u32 n = GetN(op); - u32 t; - - t = sr.T; - - sr.T = r[n] >> 31; - - r[n] <<= 1; - - r[n]|=t; + u32 t = ctx->sr.T; + ctx->sr.T = ctx->r[n] >> 31; + ctx->r[n] <<= 1; + ctx->r[n] |= t; } @@ -1700,27 +1686,19 @@ sh4op(i0100_nnnn_0000_0100) { u32 n = GetN(op); - sr.T=r[n]>>31; - - r[n] <<= 1; - - r[n]|=sr.T; + ctx->sr.T = ctx->r[n] >> 31; + ctx->r[n] <<= 1; + ctx->r[n] |= ctx->sr.T; } //rotcr sh4op(i0100_nnnn_0010_0101) { u32 n = GetN(op); - u32 t; - - - t = r[n] & 0x1; - - r[n] >>= 1; - - r[n] |=(sr.T)<<31; - - sr.T = t; + u32 t = ctx->r[n] & 0x1; + ctx->r[n] >>= 1; + ctx->r[n] |= ctx->sr.T << 31; + ctx->sr.T = t; } @@ -1728,10 +1706,12 @@ sh4op(i0100_nnnn_0010_0101) sh4op(i0100_nnnn_0000_0101) { u32 n = GetN(op); - sr.T = r[n] & 0x1; - r[n] >>= 1; - r[n] |= ((sr.T) << 31); + + ctx->sr.T = ctx->r[n] & 0x1; + ctx->r[n] >>= 1; + ctx->r[n] |= ctx->sr.T << 31; } + //************************ byte reorder/sign ************************ //swap.b , sh4op(i0110_nnnn_mmmm_1000) @@ -1739,8 +1719,8 @@ sh4op(i0110_nnnn_mmmm_1000) u32 m = GetM(op); u32 n = GetN(op); - u32 rg=r[m]; - r[n] = (rg & 0xFFFF0000) | ((rg&0xFF)<<8) | ((rg>>8)&0xFF); + u32 rg = ctx->r[m]; + ctx->r[n] = (rg & 0xFFFF0000) | ((rg & 0xFF) << 8) | ((rg >> 8) & 0xFF); } @@ -1750,18 +1730,17 @@ sh4op(i0110_nnnn_mmmm_1001) u32 n = GetN(op); u32 m = GetM(op); - u16 t = (u16)(r[m]>>16); - r[n] = (r[m] << 16) | t; + u16 t = (u16)(ctx->r[m] >> 16); + ctx->r[n] = (ctx->r[m] << 16) | t; } - //extu.b , sh4op(i0110_nnnn_mmmm_1100) { u32 n = GetN(op); u32 m = GetM(op); - r[n] = (u32)(u8)r[m]; + ctx->r[n] = (u32)(u8)ctx->r[m]; } @@ -1770,7 +1749,7 @@ sh4op(i0110_nnnn_mmmm_1101) { u32 n = GetN(op); u32 m = GetM(op); - r[n] =(u32)(u16) r[m]; + ctx->r[n] = (u32)(u16)ctx->r[m]; } @@ -1780,8 +1759,7 @@ sh4op(i0110_nnnn_mmmm_1110) u32 n = GetN(op); u32 m = GetM(op); - r[n] = (u32)(s32)(s8)(u8)(r[m]); - + ctx->r[n] = (u32)(s32)(s8)(u8)ctx->r[m]; } @@ -1791,7 +1769,7 @@ sh4op(i0110_nnnn_mmmm_1111) u32 n = GetN(op); u32 m = GetM(op); - r[n] = (u32)(s32)(s16)(u16)(r[m]); + ctx->r[n] = (u32)(s32)(s16)(u16)ctx->r[m]; } @@ -1801,7 +1779,7 @@ sh4op(i0010_nnnn_mmmm_1101) u32 n = GetN(op); u32 m = GetM(op); - r[n] = ((r[n] >> 16) & 0xFFFF) | ((r[m] << 16) & 0xFFFF0000); + ctx->r[n] = ((ctx->r[n] >> 16) & 0xFFFF) | ((ctx->r[m] << 16) & 0xFFFF0000); } @@ -1810,57 +1788,56 @@ sh4op(i0010_nnnn_mmmm_1101) sh4op(i1100_1100_iiii_iiii) { //Retail game "Twinkle Star Sprites" "uses" this opcode. - u32 imm=GetImm8(op); + u32 imm = GetImm8(op); - u32 temp = (u8)ReadMem8(gbr+r[0]); + u32 temp = (u8)ReadMem8(ctx->gbr + ctx->r[0]); temp &= imm; - if (temp==0) - sr.T=1; + if (temp == 0) + ctx->sr.T = 1; else - sr.T=0; + ctx->sr.T = 0; } //and.b #,@(R0,GBR) sh4op(i1100_1101_iiii_iiii) { - u8 temp = (u8)ReadMem8(gbr+r[0]); + u8 temp = (u8)ReadMem8(ctx->gbr + ctx->r[0]); temp &= GetImm8(op); - WriteMem8(gbr +r[0], temp); + WriteMem8(ctx->gbr + ctx->r[0], temp); } //xor.b #,@(R0,GBR) sh4op(i1100_1110_iiii_iiii) { - u8 temp = (u8)ReadMem8(gbr+r[0]); + u8 temp = (u8)ReadMem8(ctx->gbr + ctx->r[0]); temp ^= GetImm8(op); - WriteMem8(gbr +r[0], temp); + WriteMem8(ctx->gbr + ctx->r[0], temp); } //or.b #,@(R0,GBR) sh4op(i1100_1111_iiii_iiii) { - u8 temp = (u8)ReadMem8(gbr+r[0]); + u8 temp = (u8)ReadMem8(ctx->gbr + ctx->r[0]); temp |= GetImm8(op); - WriteMem8(gbr+r[0], temp); + WriteMem8(ctx->gbr + ctx->r[0], temp); } + //tas.b @ sh4op(i0100_nnnn_0001_1011) { u32 n = GetN(op); - u8 val; - - val=(u8)ReadMem8(r[n]); + u8 val = (u8)ReadMem8(ctx->r[n]); u32 srT; if (val == 0) @@ -1870,9 +1847,9 @@ sh4op(i0100_nnnn_0001_1011) val |= 0x80; - WriteMem8(r[n], val); + WriteMem8(ctx->r[n], val); - sr.T=srT; + ctx->sr.T = srT; } //************************ Opcodes that read/write the status registers ************************ @@ -1880,30 +1857,30 @@ sh4op(i0100_nnnn_0001_1011) sh4op(i0000_nnnn_0000_0010)//0002 { u32 n = GetN(op); - r[n] = sr.getFull(); + ctx->r[n] = ctx->sr.getFull(); } //sts FPSCR, sh4op(i0000_nnnn_0110_1010) { u32 n = GetN(op); - r[n] = fpscr.full; + ctx->r[n] = ctx->fpscr.full; } //sts.l FPSCR,@- sh4op(i0100_nnnn_0110_0010) { u32 n = GetN(op); - WriteMemU32(r[n] - 4, fpscr.full); - r[n] -= 4; + WriteMemU32(ctx->r[n] - 4, ctx->fpscr.full); + ctx->r[n] -= 4; } //stc.l SR,@- sh4op(i0100_nnnn_0000_0011) { u32 n = GetN(op); - WriteMemU32(r[n] - 4, sr.getFull()); - r[n] -= 4; + WriteMemU32(ctx->r[n] - 4, ctx->sr.getFull()); + ctx->r[n] -= 4; } //lds.l @+,FPSCR @@ -1911,10 +1888,9 @@ sh4op(i0100_nnnn_0110_0110) { u32 n = GetN(op); - ReadMemU32(fpscr.full,r[n]); - + ReadMemU32(ctx->fpscr.full, ctx->r[n]); UpdateFPSCR(); - r[n] += 4; + ctx->r[n] += 4; } //ldc.l @+,SR @@ -1923,10 +1899,10 @@ sh4op(i0100_nnnn_0000_0111) u32 n = GetN(op); u32 sr_t; - ReadMemU32(sr_t,r[n]); + ReadMemU32(sr_t, ctx->r[n]); - sr.setFull(sr_t); - r[n] += 4; + ctx->sr.setFull(sr_t); + ctx->r[n] += 4; if (UpdateSR()) UpdateINTC(); } @@ -1935,7 +1911,7 @@ sh4op(i0100_nnnn_0000_0111) sh4op(i0100_nnnn_0110_1010) { u32 n = GetN(op); - fpscr.full = r[n]; + ctx->fpscr.full = ctx->r[n]; UpdateFPSCR(); } @@ -1943,7 +1919,7 @@ sh4op(i0100_nnnn_0110_1010) sh4op(i0100_nnnn_0000_1110) { u32 n = GetN(op); - sr.setFull(r[n]); + ctx->sr.setFull(ctx->r[n]); if (UpdateSR()) UpdateINTC(); } @@ -1953,6 +1929,6 @@ sh4op(iNotImplemented) INFO_LOG(INTERPRETER, "iNimp %04X", op); debugger::debugTrap(Sh4Ex_IllegalInstr); - throw SH4ThrownException(next_pc - 2, Sh4Ex_IllegalInstr); + throw SH4ThrownException(ctx->pc - 2, Sh4Ex_IllegalInstr); } diff --git a/core/hw/sh4/modules/ccn.cpp b/core/hw/sh4/modules/ccn.cpp index 19bbb1916..ae5003b5a 100644 --- a/core/hw/sh4/modules/ccn.cpp +++ b/core/hw/sh4/modules/ccn.cpp @@ -65,14 +65,14 @@ static void CCN_CCR_write(u32 addr, u32 value) temp.reg_data = value & 0x89AF; if (temp.ICI) { - DEBUG_LOG(SH4, "Sh4: i-cache invalidation %08X", curr_pc); + DEBUG_LOG(SH4, "Sh4: i-cache invalidation %08X", Sh4cntx.pc); //Shikigami No Shiro II uses ICI frequently if (!config::DynarecEnabled) icache.Invalidate(); temp.ICI = 0; } if (temp.OCI) { - DEBUG_LOG(SH4, "Sh4: o-cache invalidation %08X", curr_pc); + DEBUG_LOG(SH4, "Sh4: o-cache invalidation %08X", Sh4cntx.pc); if (!config::DynarecEnabled) ocache.Invalidate(); temp.OCI = 0; diff --git a/core/hw/sh4/modules/mmu.cpp b/core/hw/sh4/modules/mmu.cpp index 483125fbb..af9de30e0 100644 --- a/core/hw/sh4/modules/mmu.cpp +++ b/core/hw/sh4/modules/mmu.cpp @@ -85,7 +85,7 @@ void ITLB_Sync(u32 entry) template static void mmuException(MmuError mmu_error, u32 address, u32 am, F raise) { - printf_mmu("MMU exception -> pc = 0x%X : ", next_pc); + printf_mmu("MMU exception -> pc = 0x%X : ", Sh4cntx.pc); CCN_TEA = address; CCN_PTEH.VPN = address >> 10; @@ -156,7 +156,7 @@ static void mmuException(MmuError mmu_error, u32 address, u32 am, F raise) mmuException(mmu_error, address, am, [](Sh4ExceptionCode event) { debugger::debugTrap(event); // FIXME CCN_TEA and CCN_PTEH have been updated already - throw SH4ThrownException(next_pc - 2, event); + throw SH4ThrownException(Sh4cntx.pc - 2, event); }); die("Unknown mmu_error"); } @@ -165,7 +165,7 @@ static void mmuException(MmuError mmu_error, u32 address, u32 am, F raise) void DoMMUException(u32 address, MmuError mmu_error, u32 access_type) { mmuException(mmu_error, address, access_type, [](Sh4ExceptionCode event) { - Do_Exception(next_pc, event); + Do_Exception(Sh4cntx.pc, event); }); } @@ -179,7 +179,7 @@ bool mmu_match(u32 va, CCN_PTEH_type Address, CCN_PTEL_type Data) if ((((Address.VPN << 10) & mask) == (va & mask))) { - bool needAsidMatch = Data.SH == 0 && (sr.MD == 0 || CCN_MMUCR.SV == 0); + bool needAsidMatch = Data.SH == 0 && (Sh4cntx.sr.MD == 0 || CCN_MMUCR.SV == 0); if (!needAsidMatch || Address.ASID == CCN_PTEH.ASID) return true; @@ -231,7 +231,7 @@ template MmuError mmu_full_SQ(u32 va, u32& rv) { - if ((va & 3) || (CCN_MMUCR.SQMD == 1 && sr.MD == 0)) + if ((va & 3) || (CCN_MMUCR.SQMD == 1 && Sh4cntx.sr.MD == 0)) //here, or after ? return MmuError::BADADDR; @@ -250,7 +250,7 @@ MmuError mmu_full_SQ(u32 va, u32& rv) u32 md = entry->Data.PR >> 1; //Priv mode protection - if (md == 0 && sr.MD == 0) + if (md == 0 && Sh4cntx.sr.MD == 0) return MmuError::PROTECTED; //Write Protection (Lock or FW) @@ -287,7 +287,7 @@ MmuError mmu_data_translation(u32 va, u32& rv) } } - if (sr.MD == 0 && (va & 0x80000000) != 0) + if (Sh4cntx.sr.MD == 0 && (va & 0x80000000) != 0) //if on kernel, and not SQ addr -> error return MmuError::BADADDR; @@ -326,7 +326,7 @@ MmuError mmu_data_translation(u32 va, u32& rv) //0X & User mode-> protection violation //Priv mode protection - if (md == 0 && sr.MD == 0) + if (md == 0 && Sh4cntx.sr.MD == 0) return MmuError::PROTECTED; //X0 -> read olny @@ -351,7 +351,7 @@ template MmuError mmu_data_translation(u32 va, u32& rv); MmuError mmu_instruction_translation(u32 va, u32& rv) { - if (sr.MD == 0 && (va & 0x80000000) != 0) + if (Sh4cntx.sr.MD == 0 && (va & 0x80000000) != 0) // User mode on kernel address return MmuError::BADADDR; @@ -375,7 +375,7 @@ MmuError mmu_instruction_translation(u32 va, u32& rv) //0X & User mode-> protection violation //Priv mode protection - if (md == 0 && sr.MD == 0) + if (md == 0 && Sh4cntx.sr.MD == 0) return MmuError::PROTECTED; return MmuError::NONE; @@ -396,7 +396,7 @@ MmuError mmu_instruction_lookup(u32 va, const TLB_Entry** tlb_entry_ret, u32& rv if ((((entry.Address.VPN << 10) & mask) == (va & mask))) { - bool needAsidMatch = entry.Data.SH == 0 && (sr.MD == 0 || CCN_MMUCR.SV == 0); + bool needAsidMatch = entry.Data.SH == 0 && (Sh4cntx.sr.MD == 0 || CCN_MMUCR.SV == 0); if (!needAsidMatch || entry.Address.ASID == CCN_PTEH.ASID) { diff --git a/core/hw/sh4/modules/wince.h b/core/hw/sh4/modules/wince.h index 3967488a9..dd2e8e662 100644 --- a/core/hw/sh4/modules/wince.h +++ b/core/hw/sh4/modules/wince.h @@ -363,7 +363,7 @@ static bool wince_resolve_address(u32 va, TLB_Entry &entry) u32 paddr = *(u32 *)&mem_b[(page_group + page) & ram_mask]; if (paddr & 0x80000000) { - u32 whatever = *(u32 *)&mem_b[(r_bank[4] + 0x14) & ram_mask]; + u32 whatever = *(u32 *)&mem_b[(p_sh4rcb->cntx.r_bank[4] + 0x14) & ram_mask]; if (whatever != *(u32 *)&mem_b[paddr & ram_mask]) { paddr += 12; diff --git a/core/hw/sh4/sh4_cache.h b/core/hw/sh4/sh4_cache.h index 2784866c5..f753de753 100644 --- a/core/hw/sh4/sh4_cache.h +++ b/core/hw/sh4/sh4_cache.h @@ -21,7 +21,7 @@ #include "types.h" #include "sh4_mem.h" #include "modules/mmu.h" -#include "hw/sh4/sh4_core.h" +#include "hw/sh4/sh4_if.h" #include "serialize.h" #include "sh4_cycles.h" @@ -177,7 +177,7 @@ class Sh4ICache return MmuError::BADADDR; const u32 area = address >> 29; - const bool userMode = sr.MD == 0; + const bool userMode = p_sh4rcb->cntx.sr.MD == 0; if (userMode) { @@ -515,7 +515,7 @@ class Sh4OCache return lookup; } const u32 area = address >> 29; - const bool userMode = sr.MD == 0; + const bool userMode = p_sh4rcb->cntx.sr.MD == 0; // kernel mem protected in user mode if (userMode && (address & 0x80000000)) diff --git a/core/hw/sh4/sh4_core.h b/core/hw/sh4/sh4_core.h index eb28ce224..cf50036b1 100644 --- a/core/hw/sh4/sh4_core.h +++ b/core/hw/sh4/sh4_core.h @@ -2,30 +2,6 @@ #include "types.h" #include "sh4_if.h" -#define r Sh4cntx.r -#define r_bank Sh4cntx.r_bank -#define gbr Sh4cntx.gbr -#define ssr Sh4cntx.ssr -#define spc Sh4cntx.spc -#define sgr Sh4cntx.sgr -#define dbr Sh4cntx.dbr -#define vbr Sh4cntx.vbr -#define mac Sh4cntx.mac -#define pr Sh4cntx.pr -#define fpul Sh4cntx.fpul -#define next_pc Sh4cntx.pc -#define curr_pc (next_pc-2) -#define sr Sh4cntx.sr -#define fpscr Sh4cntx.fpscr -#define old_sr Sh4cntx.old_sr -#define old_fpscr Sh4cntx.old_fpscr -#define fr (&Sh4cntx.xffr[16]) -#define xf Sh4cntx.xffr -#define fr_hex ((u32*)fr) -#define xf_hex ((u32*)xf) -#define dr_hex ((u64*)fr) -#define xd_hex ((u64*)xf) - void UpdateFPSCR(); bool UpdateSR(); void RestoreHostRoundingMode(); diff --git a/core/hw/sh4/sh4_core_regs.cpp b/core/hw/sh4/sh4_core_regs.cpp index 8338ef89b..4102c60d5 100644 --- a/core/hw/sh4/sh4_core_regs.cpp +++ b/core/hw/sh4/sh4_core_regs.cpp @@ -13,7 +13,7 @@ Sh4RCB* p_sh4rcb; static void ChangeGPR() { - std::swap((u32 (&)[8])r, r_bank); + std::swap((u32 (&)[8])Sh4cntx.r, Sh4cntx.r_bank); } static void ChangeFP() @@ -25,19 +25,19 @@ static void ChangeFP() //returns true if interrupt pending bool UpdateSR() { - if (sr.MD) + if (Sh4cntx.sr.MD) { - if (old_sr.RB != sr.RB) + if (Sh4cntx.old_sr.RB != Sh4cntx.sr.RB) ChangeGPR();//bank change } else { - if (old_sr.RB) + if (Sh4cntx.old_sr.RB) ChangeGPR();//switch } - old_sr.status = sr.status; - old_sr.RB &= sr.MD; + Sh4cntx.old_sr.status = Sh4cntx.sr.status; + Sh4cntx.old_sr.RB &= Sh4cntx.sr.MD; return SRdecode(); } @@ -48,19 +48,19 @@ static u32 old_dn = 0xFF; static void setHostRoundingMode() { - if (old_rm != fpscr.RM || old_dn != fpscr.DN) + if (old_rm != Sh4cntx.fpscr.RM || old_dn != Sh4cntx.fpscr.DN) { - old_rm = fpscr.RM; - old_dn = fpscr.DN; + old_rm = Sh4cntx.fpscr.RM; + old_dn = Sh4cntx.fpscr.DN; //Correct rounding is required by some games (SOTB, etc) #ifdef _MSC_VER - if (fpscr.RM == 1) //if round to 0 , set the flag + if (Sh4cntx.fpscr.RM == 1) //if round to 0 , set the flag _controlfp(_RC_CHOP, _MCW_RC); else _controlfp(_RC_NEAR, _MCW_RC); - if (fpscr.DN) //denormals are considered 0 + if (Sh4cntx.fpscr.DN) //denormals are considered 0 _controlfp(_DN_FLUSH, _MCW_DN); else _controlfp(_DN_SAVE, _MCW_DN); @@ -70,20 +70,20 @@ static void setHostRoundingMode() u32 temp=0x1f80; //no flush to zero && round to nearest - if (fpscr.RM==1) //if round to 0 , set the flag + if (Sh4cntx.fpscr.RM==1) //if round to 0 , set the flag temp|=(3<<13); - if (fpscr.DN) //denormals are considered 0 + if (Sh4cntx.fpscr.DN) //denormals are considered 0 temp|=(1<<15); asm("ldmxcsr %0" : : "m"(temp)); #elif HOST_CPU==CPU_ARM static const unsigned int offMask = 0x04086060; unsigned int onMask = 0x02000000; - if (fpscr.RM == 1) //if round to 0 , set the flag + if (Sh4cntx.fpscr.RM == 1) //if round to 0 , set the flag onMask |= 3 << 22; - if (fpscr.DN) + if (Sh4cntx.fpscr.DN) onMask |= 1 << 24; #ifdef __ANDROID__ @@ -109,10 +109,10 @@ static void setHostRoundingMode() static const unsigned long off_mask = 0x04080000; unsigned long on_mask = 0x02000000; // DN=1 Any operation involving one or more NaNs returns the Default NaN - if (fpscr.RM == 1) // if round to 0, set the flag + if (Sh4cntx.fpscr.RM == 1) // if round to 0, set the flag on_mask |= 3 << 22; - if (fpscr.DN) + if (Sh4cntx.fpscr.DN) on_mask |= 1 << 24; // flush denormalized numbers to zero asm volatile @@ -136,10 +136,10 @@ static void setHostRoundingMode() //called when fpscr is changed and we must check for reg banks etc.. void UpdateFPSCR() { - if (fpscr.FR !=old_fpscr.FR) + if (Sh4cntx.fpscr.FR != Sh4cntx.old_fpscr.FR) ChangeFP(); // FPU bank change - old_fpscr=fpscr; + Sh4cntx.old_fpscr = Sh4cntx.fpscr; setHostRoundingMode(); } @@ -152,11 +152,11 @@ void RestoreHostRoundingMode() void setDefaultRoundingMode() { - u32 savedRM = fpscr.RM; - u32 savedDN = fpscr.DN; - fpscr.RM = 0; - fpscr.DN = 0; + u32 savedRM = Sh4cntx.fpscr.RM; + u32 savedDN = Sh4cntx.fpscr.DN; + Sh4cntx.fpscr.RM = 0; + Sh4cntx.fpscr.DN = 0; setHostRoundingMode(); - fpscr.RM = savedRM; - fpscr.DN = savedDN; + Sh4cntx.fpscr.RM = savedRM; + Sh4cntx.fpscr.DN = savedDN; } diff --git a/core/hw/sh4/sh4_interpreter.h b/core/hw/sh4/sh4_interpreter.h index 70e3c7fb8..db58d327e 100644 --- a/core/hw/sh4/sh4_interpreter.h +++ b/core/hw/sh4/sh4_interpreter.h @@ -18,6 +18,8 @@ class Sh4Interpreter : public Sh4Executor void ExecuteDelayslot_RTE(); Sh4Context *getContext() { return ctx; } + static Sh4Interpreter *Instance; + protected: Sh4Context *ctx = nullptr; diff --git a/core/hw/sh4/sh4_interrupts.cpp b/core/hw/sh4/sh4_interrupts.cpp index 115493f01..49a3be455 100644 --- a/core/hw/sh4/sh4_interrupts.cpp +++ b/core/hw/sh4/sh4_interrupts.cpp @@ -141,10 +141,10 @@ void SIIDRebuild() //Decode SR.IMSK into a interrupt mask, update and return the interrupt state bool SRdecode() { - if (sr.BL) + if (Sh4cntx.sr.BL) decoded_srimask=~0xFFFFFFFF; else - decoded_srimask=~InterruptLevelBit[sr.IMASK]; + decoded_srimask=~InterruptLevelBit[Sh4cntx.sr.IMASK]; recalc_pending_itrs(); return Sh4cntx.interrupt_pend; @@ -185,14 +185,14 @@ static void Do_Interrupt(Sh4ExceptionCode intEvn) { CCN_INTEVT = intEvn; - ssr = sr.getFull(); - spc = next_pc; - sgr = r[15]; - sr.BL = 1; - sr.MD = 1; - sr.RB = 1; + Sh4cntx.ssr = Sh4cntx.sr.getFull(); + Sh4cntx.spc = Sh4cntx.pc; + Sh4cntx.sgr = Sh4cntx.r[15]; + Sh4cntx.sr.BL = 1; + Sh4cntx.sr.MD = 1; + Sh4cntx.sr.RB = 1; UpdateSR(); - next_pc = vbr + 0x600; + Sh4cntx.pc = Sh4cntx.vbr + 0x600; debugger::subroutineCall(); } @@ -200,19 +200,19 @@ void Do_Exception(u32 epc, Sh4ExceptionCode expEvn) { assert((expEvn >= Sh4Ex_TlbMissRead && expEvn <= Sh4Ex_SlotIllegalInstr) || expEvn == Sh4Ex_FpuDisabled || expEvn == Sh4Ex_SlotFpuDisabled || expEvn == Sh4Ex_UserBreak); - if (sr.BL != 0) + if (Sh4cntx.sr.BL != 0) throw FlycastException("Fatal: SH4 exception when blocked"); CCN_EXPEVT = expEvn; - ssr = sr.getFull(); - spc = epc; - sgr = r[15]; - sr.BL = 1; - sr.MD = 1; - sr.RB = 1; + Sh4cntx.ssr = Sh4cntx.sr.getFull(); + Sh4cntx.spc = epc; + Sh4cntx.sgr = Sh4cntx.r[15]; + Sh4cntx.sr.BL = 1; + Sh4cntx.sr.MD = 1; + Sh4cntx.sr.RB = 1; UpdateSR(); - next_pc = vbr + (expEvn == Sh4Ex_TlbMissRead || expEvn == Sh4Ex_TlbMissWrite ? 0x400 : 0x100); + Sh4cntx.pc = Sh4cntx.vbr + (expEvn == Sh4Ex_TlbMissRead || expEvn == Sh4Ex_TlbMissWrite ? 0x400 : 0x100); debugger::subroutineCall(); //printf("RaiseException: from pc %08x to %08x, event %x\n", epc, next_pc, expEvn); diff --git a/core/hw/sh4/sh4_opcode_list.h b/core/hw/sh4/sh4_opcode_list.h index cfdc25f48..d0b905aae 100644 --- a/core/hw/sh4/sh4_opcode_list.h +++ b/core/hw/sh4/sh4_opcode_list.h @@ -1,10 +1,11 @@ #pragma once #include "types.h" +#include "sh4_if.h" #include -#define sh4op(str) void DYNACALL str (u32 op) +#define sh4op(str) void DYNACALL str (Sh4Context *ctx, u32 op) -typedef void (DYNACALL OpCallFP) (u32 op); +typedef void (DYNACALL OpCallFP) (Sh4Context *ctx, u32 op); extern OpCallFP* OpPtr[0x10000]; typedef void OpDissasmFP(char* out,const char* const FormatString,u32 pc,u16 opcode); diff --git a/core/rec-ARM/rec_arm.cpp b/core/rec-ARM/rec_arm.cpp index dc6244fb9..2a65ec139 100644 --- a/core/rec-ARM/rec_arm.cpp +++ b/core/rec-ARM/rec_arm.cpp @@ -1176,10 +1176,10 @@ void Arm32Assembler::genMmuLookup(RuntimeBlockInfo* block, const shil_opcode& op } } -static void interpreter_fallback(u16 op, OpCallFP *oph, u32 pc) +static void interpreter_fallback(Sh4Context *ctx, u16 op, OpCallFP *oph, u32 pc) { try { - oph(op); + oph(ctx, op); } catch (SH4ThrownException& ex) { if (pc & 1) { @@ -1515,15 +1515,16 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op storeSh4Reg(r1, reg_nextpc); } - Mov(r0, op->rs3._imm); + Sub(r0, r8, sizeof(Sh4Context)); + Mov(r1, op->rs3._imm); if (!mmu_enabled()) { call((void *)OpPtr[op->rs3._imm]); } else { - Mov(r1, reinterpret_cast(*OpDesc[op->rs3._imm]->oph)); // op handler - Mov(r2, block->vaddr + op->guest_offs - (op->delay_slot ? 1 : 0)); // pc + Mov(r2, reinterpret_cast(*OpDesc[op->rs3._imm]->oph)); // op handler + Mov(r3, block->vaddr + op->guest_offs - (op->delay_slot ? 1 : 0)); // pc call((void *)interpreter_fallback); } break; diff --git a/core/rec-ARM64/rec_arm64.cpp b/core/rec-ARM64/rec_arm64.cpp index a242e3456..66d17c106 100644 --- a/core/rec-ARM64/rec_arm64.cpp +++ b/core/rec-ARM64/rec_arm64.cpp @@ -87,10 +87,10 @@ static void jitWriteProtect(Sh4CodeBuffer &codeBuffer, bool enable) #endif } -static void interpreter_fallback(u16 op, OpCallFP *oph, u32 pc) +static void interpreter_fallback(Sh4Context *ctx, u16 op, OpCallFP *oph, u32 pc) { try { - oph(op); + oph(ctx, op); } catch (SH4ThrownException& ex) { if (pc & 1) { @@ -287,18 +287,19 @@ class Arm64Assembler : public MacroAssembler if (op.rs1._imm) // if NeedPC() { Mov(w10, op.rs2._imm); - Str(w10, sh4_context_mem_operand(&next_pc)); + Str(w10, sh4_context_mem_operand(&Sh4cntx.pc)); } - Mov(w0, op.rs3._imm); + Mov(x0, x28); + Mov(w1, op.rs3._imm); if (!mmu_enabled()) { GenCallRuntime(OpDesc[op.rs3._imm]->oph); } else { - Mov(x1, reinterpret_cast(*OpDesc[op.rs3._imm]->oph)); // op handler - Mov(w2, block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc + Mov(x2, reinterpret_cast(*OpDesc[op.rs3._imm]->oph)); // op handler + Mov(w3, block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc GenCallRuntime(interpreter_fallback); } @@ -1162,7 +1163,7 @@ class Arm64Assembler : public MacroAssembler #endif { Mov(w29, block->BranchBlock); - Str(w29, sh4_context_mem_operand(&next_pc)); + Str(w29, sh4_context_mem_operand(&Sh4cntx.pc)); GenBranch(arm64_no_update); } } @@ -1178,7 +1179,7 @@ class Arm64Assembler : public MacroAssembler if (block->has_jcond) Ldr(w11, sh4_context_mem_operand(&Sh4cntx.jdyn)); else - Ldr(w11, sh4_context_mem_operand(&sr.T)); + Ldr(w11, sh4_context_mem_operand(&Sh4cntx.sr.T)); Cmp(w11, block->BlockType & 1); @@ -1206,7 +1207,7 @@ class Arm64Assembler : public MacroAssembler #endif { Mov(w29, block->BranchBlock); - Str(w29, sh4_context_mem_operand(&next_pc)); + Str(w29, sh4_context_mem_operand(&Sh4cntx.pc)); GenBranch(arm64_no_update); } } @@ -1234,7 +1235,7 @@ class Arm64Assembler : public MacroAssembler #endif { Mov(w29, block->NextBlock); - Str(w29, sh4_context_mem_operand(&next_pc)); + Str(w29, sh4_context_mem_operand(&Sh4cntx.pc)); GenBranch(arm64_no_update); } } @@ -1246,7 +1247,7 @@ class Arm64Assembler : public MacroAssembler case BET_DynamicRet: // next_pc = *jdyn; - Str(w29, sh4_context_mem_operand(&next_pc)); + Str(w29, sh4_context_mem_operand(&Sh4cntx.pc)); if (!mmu_enabled()) { // TODO Call no_update instead (and check CpuRunning less frequently?) @@ -1275,11 +1276,11 @@ class Arm64Assembler : public MacroAssembler Mov(w29, block->NextBlock); // else next_pc = *jdyn (already in w29) - Str(w29, sh4_context_mem_operand(&next_pc)); + Str(w29, sh4_context_mem_operand(&Sh4cntx.pc)); GenCallRuntime(UpdateINTC); - Ldr(w29, sh4_context_mem_operand(&next_pc)); + Ldr(w29, sh4_context_mem_operand(&Sh4cntx.pc)); GenBranch(arm64_no_update); break; @@ -1498,12 +1499,12 @@ class Arm64Assembler : public MacroAssembler // w0: vaddr, w1: addr checkBlockFpu = GetCursorAddress(); Label fpu_enabled; - Ldr(w10, sh4_context_mem_operand(&sr.status)); + Ldr(w10, sh4_context_mem_operand(&Sh4cntx.sr.status)); Tbz(w10, 15, &fpu_enabled); // test SR.FD bit Mov(w1, Sh4Ex_FpuDisabled); // exception code GenCallRuntime(Do_Exception); - Ldr(w29, sh4_context_mem_operand(&next_pc)); + Ldr(w29, sh4_context_mem_operand(&Sh4cntx.pc)); B(&no_update); Bind(&fpu_enabled); // fallthrough diff --git a/core/rec-x64/rec_x64.cpp b/core/rec-x64/rec_x64.cpp index fa7d4e8a6..96ad71b80 100644 --- a/core/rec-x64/rec_x64.cpp +++ b/core/rec-x64/rec_x64.cpp @@ -77,10 +77,10 @@ static void handle_sh4_exception(SH4ThrownException& ex, u32 pc) handleException(); } -static void interpreter_fallback(u16 op, OpCallFP *oph, u32 pc) +static void interpreter_fallback(Sh4Context *ctx, u16 op, OpCallFP *oph, u32 pc) { try { - oph(op); + oph(ctx, op); } catch (SH4ThrownException& ex) { handle_sh4_exception(ex, pc); } @@ -136,7 +136,7 @@ class BlockCompiler : public BaseXbyakRec if (mmu_enabled() && block->has_fpu_op) { Xbyak::Label fpu_enabled; - mov(rax, (uintptr_t)&sr.status); + mov(rax, (uintptr_t)&p_sh4rcb->cntx.sr.status); test(dword[rax], 0x8000); // test SR.FD bit jz(fpu_enabled); mov(call_regs[0], block->vaddr); // pc @@ -161,18 +161,19 @@ class BlockCompiler : public BaseXbyakRec case shop_ifb: if (mmu_enabled()) { - mov(call_regs64[1], reinterpret_cast(*OpDesc[op.rs3._imm]->oph)); // op handler - mov(call_regs[2], block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc + mov(call_regs64[2], reinterpret_cast(*OpDesc[op.rs3._imm]->oph)); // op handler + mov(call_regs[3], block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc } if (op.rs1._imm) { - mov(rax, (size_t)&next_pc); + mov(rax, (size_t)&p_sh4rcb->cntx.pc); mov(dword[rax], op.rs2._imm); } - mov(call_regs[0], op.rs3._imm); - + mov(call_regs[1], op.rs3._imm); + mov(call_regs64[0], (uintptr_t)&p_sh4rcb->cntx); + if (!mmu_enabled()) GenCall(OpDesc[op.rs3._imm]->oph); else @@ -471,7 +472,7 @@ class BlockCompiler : public BaseXbyakRec regalloc.Cleanup(); current_opid = -1; - mov(rax, (size_t)&next_pc); + mov(rax, (size_t)&p_sh4rcb->cntx.pc); switch (block->BlockType) { @@ -493,7 +494,7 @@ class BlockCompiler : public BaseXbyakRec if (block->has_jcond) mov(rdx, (size_t)&Sh4cntx.jdyn); else - mov(rdx, (size_t)&sr.T); + mov(rdx, (size_t)&Sh4cntx.sr.T); cmp(dword[rdx], block->BlockType & 1); Xbyak::Label branch_not_taken; @@ -1057,7 +1058,7 @@ class BlockCompiler : public BaseXbyakRec // same at compile and run times. if (mmu_enabled()) { - mov(rax, (uintptr_t)&next_pc); + mov(rax, (uintptr_t)&p_sh4rcb->cntx.pc); cmp(dword[rax], block->vaddr); jne(reinterpret_cast(&ngen_blockcheckfail)); } diff --git a/core/rec-x86/rec_x86.cpp b/core/rec-x86/rec_x86.cpp index a8bc5fc09..b67667c1c 100644 --- a/core/rec-x86/rec_x86.cpp +++ b/core/rec-x86/rec_x86.cpp @@ -109,14 +109,14 @@ void X86Compiler::compile(RuntimeBlockInfo* block, bool force_checks, bool optim if (mmu_enabled() && block->has_fpu_op) { Xbyak::Label fpu_enabled; - mov(eax, dword[&sr.status]); + mov(eax, dword[&Sh4cntx.sr.status]); test(eax, 0x8000); // test SR.FD bit jz(fpu_enabled); push(Sh4Ex_FpuDisabled); // exception code push(block->vaddr); // pc call((void (*)())Do_Exception); add(esp, 8); - mov(ecx, dword[&next_pc]); + mov(ecx, dword[&Sh4cntx.pc]); jmp((const void *)no_update); L(fpu_enabled); } @@ -298,16 +298,16 @@ u32 X86Compiler::relinkBlock(RuntimeBlockInfo* block) case BET_DynamicIntr: if (block->BlockType == BET_StaticIntr) { - mov(dword[&next_pc], block->NextBlock); + mov(dword[&Sh4cntx.pc], block->NextBlock); } else { mov(eax, dword[GetRegPtr(reg_pc_dyn)]); - mov(dword[&next_pc], eax); + mov(dword[&Sh4cntx.pc], eax); } call(UpdateINTC); - mov(ecx, dword[&next_pc]); + mov(ecx, dword[&Sh4cntx.pc]); jmp((const void *)no_update); break; @@ -459,14 +459,14 @@ void X86Compiler::genMainloop() } else { - mov(dword[&next_pc], ecx); + mov(dword[&Sh4cntx.pc], ecx); call((void *)bm_GetCodeByVAddr); jmp(eax); } //cleanup: L(cleanup); - mov(dword[&next_pc], ecx); + mov(dword[&Sh4cntx.pc], ecx); #ifndef _WIN32 // 16-byte alignment add(esp, 12); @@ -569,7 +569,7 @@ void X86Compiler::genMainloop() Xbyak::Label jumpblockLabel; cmp(eax, 0); jne(jumpblockLabel); - mov(ecx, dword[&next_pc]); + mov(ecx, dword[&Sh4cntx.pc]); jmp(no_updateLabel); L(jumpblockLabel); } @@ -810,7 +810,7 @@ void X86Compiler::checkBlock(bool smc_checks, RuntimeBlockInfo* block) if (mmu_enabled()) { - mov(eax, dword[&next_pc]); + mov(eax, dword[&Sh4cntx.pc]); cmp(eax, block->vaddr); jne(reinterpret_cast(ngen_blockcheckfail)); } diff --git a/core/rec-x86/x86_ops.cpp b/core/rec-x86/x86_ops.cpp index 26b7ed2f0..f423c39fb 100644 --- a/core/rec-x86/x86_ops.cpp +++ b/core/rec-x86/x86_ops.cpp @@ -298,10 +298,10 @@ static void DYNACALL handle_sh4_exception(SH4ThrownException& ex, u32 pc) std::abort(); } -static void DYNACALL interpreter_fallback(u16 op, OpCallFP *oph, u32 pc) +static void DYNACALL interpreter_fallback(Sh4Context *ctx, u16 op, OpCallFP *oph, u32 pc) { try { - oph(op); + oph(ctx, op); } catch (SH4ThrownException& ex) { handle_sh4_exception(ex, pc); } @@ -323,12 +323,13 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& case shop_ifb: if (mmu_enabled()) { - mov(edx, reinterpret_cast(*OpDesc[op.rs3._imm]->oph)); // op handler + push(reinterpret_cast(*OpDesc[op.rs3._imm]->oph)); // op handler push(block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc } if (op.rs1.is_imm() && op.rs1.imm_value()) - mov(dword[&next_pc], op.rs2.imm_value()); - mov(ecx, op.rs3.imm_value()); + mov(dword[&Sh4cntx.pc], op.rs2.imm_value()); + mov(ecx, (uintptr_t)&Sh4cntx); + mov(edx, op.rs3.imm_value()); if (!mmu_enabled()) genCall(OpDesc[op.rs3.imm_value()]->oph); else diff --git a/core/reios/reios.cpp b/core/reios/reios.cpp index 42c2127b3..e9a9a5dc9 100644 --- a/core/reios/reios.cpp +++ b/core/reios/reios.cpp @@ -15,8 +15,7 @@ #include "gdrom_hle.h" #include "descrambl.h" -#include "hw/sh4/sh4_core.h" -#undef r +#include "hw/sh4/sh4_if.h" #include "hw/sh4/sh4_mem.h" #include "hw/holly/sb.h" #include "hw/naomi/naomi_cart.h" @@ -475,25 +474,26 @@ static void reios_setup_state(u32 boot_addr) */ //Setup registers to imitate a normal boot - p_sh4rcb->cntx.r[15] = 0x8d000000; - - gbr = 0x8c000000; - ssr = 0x40000001; - spc = 0x8c000776; - sgr = 0x8d000000; - dbr = 0x8c000010; - vbr = 0x8c000000; - pr = 0xac00043c; - fpul = 0x00000000; - next_pc = boot_addr; - - sr.status = 0x400000f0; - sr.T = 1; - - old_sr.status = 0x400000f0; - - fpscr.full = 0x00040001; - old_fpscr.full = 0x00040001; + Sh4Context& ctx = p_sh4rcb->cntx; + ctx.r[15] = 0x8d000000; + + ctx.gbr = 0x8c000000; + ctx.ssr = 0x40000001; + ctx.spc = 0x8c000776; + ctx.sgr = 0x8d000000; + ctx.dbr = 0x8c000010; + ctx.vbr = 0x8c000000; + ctx.pr = 0xac00043c; + ctx.fpul = 0x00000000; + ctx.pc = boot_addr; + + ctx.sr.status = 0x400000f0; + ctx.sr.T = 1; + + ctx.old_sr.status = 0x400000f0; + + ctx.fpscr.full = 0x00040001; + ctx.old_fpscr.full = 0x00040001; } static void reios_setup_naomi(u32 boot_addr) { @@ -581,40 +581,41 @@ static void reios_setup_naomi(u32 boot_addr) { */ //Setup registers to imitate a normal boot - p_sh4rcb->cntx.r[0] = 0x0c021000; - p_sh4rcb->cntx.r[1] = 0x0c01f820; - p_sh4rcb->cntx.r[2] = 0xa0710004; - p_sh4rcb->cntx.r[3] = 0x0c01f130; - p_sh4rcb->cntx.r[4] = 0x5bfccd08; - p_sh4rcb->cntx.r[5] = 0xa05f7000; - p_sh4rcb->cntx.r[6] = 0xa05f7008; - p_sh4rcb->cntx.r[7] = 0x00000007; - p_sh4rcb->cntx.r[8] = 0x00000000; - p_sh4rcb->cntx.r[9] = 0x00002000; - p_sh4rcb->cntx.r[10] = 0xffffffff; - p_sh4rcb->cntx.r[11] = 0x0c0e0000; - p_sh4rcb->cntx.r[12] = 0x00000000; - p_sh4rcb->cntx.r[13] = 0x00000000; - p_sh4rcb->cntx.r[14] = 0x00000000; - p_sh4rcb->cntx.r[15] = 0x0cc00000; - - gbr = 0x0c2abcc0; - ssr = 0x60000000; - spc = 0x0c041738; - sgr = 0x0cbfffb0; - dbr = 0x00000fff; - vbr = 0x0c000000; - pr = 0xac0195ee; - fpul = 0x000001e0; - next_pc = boot_addr; - - sr.status = 0x60000000; - sr.T = 1; - - old_sr.status = 0x60000000; - - fpscr.full = 0x00040001; - old_fpscr.full = 0x00040001; + Sh4Context& ctx = p_sh4rcb->cntx; + ctx.r[0] = 0x0c021000; + ctx.r[1] = 0x0c01f820; + ctx.r[2] = 0xa0710004; + ctx.r[3] = 0x0c01f130; + ctx.r[4] = 0x5bfccd08; + ctx.r[5] = 0xa05f7000; + ctx.r[6] = 0xa05f7008; + ctx.r[7] = 0x00000007; + ctx.r[8] = 0x00000000; + ctx.r[9] = 0x00002000; + ctx.r[10] = 0xffffffff; + ctx.r[11] = 0x0c0e0000; + ctx.r[12] = 0x00000000; + ctx.r[13] = 0x00000000; + ctx.r[14] = 0x00000000; + ctx.r[15] = 0x0cc00000; + + ctx.gbr = 0x0c2abcc0; + ctx.ssr = 0x60000000; + ctx.spc = 0x0c041738; + ctx.sgr = 0x0cbfffb0; + ctx.dbr = 0x00000fff; + ctx.vbr = 0x0c000000; + ctx.pr = 0xac0195ee; + ctx.fpul = 0x000001e0; + ctx.pc = boot_addr; + + ctx.sr.status = 0x60000000; + ctx.sr.T = 1; + + ctx.old_sr.status = 0x60000000; + + ctx.fpscr.full = 0x00040001; + ctx.old_fpscr.full = 0x00040001; } static void reios_boot() @@ -694,9 +695,10 @@ static const std::map hooks = { { SYSCALL_ADDR(dc_bios_entrypoint_gd2), gdrom_hle_op }, }; -void DYNACALL reios_trap(u32 op) { +void DYNACALL reios_trap(Sh4Context *ctx, u32 op) +{ verify(op == REIOS_OPCODE); - u32 pc = next_pc - 2; + u32 pc = ctx->pc - 2; u32 mapd = SYSCALL_ADDR(pc); @@ -711,8 +713,8 @@ void DYNACALL reios_trap(u32 op) { it->second(); // Return from syscall, except if pc was modified - if (pc == next_pc - 2) - next_pc = pr; + if (pc == ctx->pc - 2) + ctx->pc = ctx->pr; } bool reios_init() diff --git a/core/reios/reios.h b/core/reios/reios.h index af1b616f5..23ce4a201 100644 --- a/core/reios/reios.h +++ b/core/reios/reios.h @@ -9,7 +9,8 @@ void reios_reset(u8* rom); void reios_term(); -void DYNACALL reios_trap(u32 op); +struct Sh4Context; +void DYNACALL reios_trap(Sh4Context *ctx, u32 op); void reios_disk_id(); diff --git a/tests/src/div32_test.cpp b/tests/src/div32_test.cpp index 36a5bd1fc..c6c68eebc 100644 --- a/tests/src/div32_test.cpp +++ b/tests/src/div32_test.cpp @@ -10,6 +10,7 @@ static void div1(u32& r1, u32 r2) { + sr_t& sr = Sh4cntx.sr; const u8 old_q = sr.Q; sr.Q = (u8)((0x80000000 & r1) != 0); @@ -53,6 +54,7 @@ static void div1(u32& r1, u32 r2) static void div32s_slow(u32& r1, u32 r2, u32& r3) { + sr_t& sr = Sh4cntx.sr; sr.Q = r3 >> 31; sr.M = r2 >> 31; sr.T = sr.Q ^ sr.M; @@ -68,6 +70,7 @@ static void div32s_slow(u32& r1, u32 r2, u32& r3) static void div32s_fast(u32& r1, u32 r2, u32& r3) { + sr_t& sr = Sh4cntx.sr; sr.T = (r3 ^ r2) & 0x80000000; u64 rv = shil_opcl_div32s::f1::impl(r1, r2, r3); r1 = (u32)rv; @@ -80,6 +83,7 @@ static void div32s_fast(u32& r1, u32 r2, u32& r3) static void div32u_fast(u32& r1, u32 r2, u32& r3) { + sr_t& sr = Sh4cntx.sr; u64 rv = shil_opcl_div32u::f1::impl(r1, r2, r3); r1 = (u32)rv; r3 = rv >> 32; @@ -90,6 +94,7 @@ static void div32u_fast(u32& r1, u32 r2, u32& r3) static void div32u_slow(u32& r1, u32 r2, u32& r3) { + sr_t& sr = Sh4cntx.sr; sr.Q = 0; sr.M = 0; sr.T = 0; From 2b28e819e5804fa1f0567a24b5ae273e779437b4 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 8 Nov 2024 21:34:24 +0100 Subject: [PATCH 16/81] sh4: build fix. rounding mode refactoring --- core/debug/debug_agent.h | 2 +- core/hw/gdrom/gdromv3.cpp | 1 - core/hw/sh4/dyna/shil.cpp | 24 ++++++++++++------- core/hw/sh4/interpr/sh4_opcodes.cpp | 7 +++--- core/hw/sh4/interpr/sh4_opcodes.h | 2 +- core/hw/sh4/sh4_core.h | 2 ++ core/hw/sh4/sh4_core_regs.cpp | 36 ++++++++++++----------------- core/hw/sh4/sh4_interpreter.h | 2 -- core/hw/sh4/sh4_mem.cpp | 1 + core/hw/sh4/sh4_mmr.cpp | 1 - core/rec-x86/rec_x86.cpp | 1 - 11 files changed, 39 insertions(+), 40 deletions(-) diff --git a/core/debug/debug_agent.h b/core/debug/debug_agent.h index 12af1ecd4..51b0ccf74 100644 --- a/core/debug/debug_agent.h +++ b/core/debug/debug_agent.h @@ -21,7 +21,7 @@ #include "emulator.h" #include "hw/sh4/sh4_if.h" #include "hw/sh4/sh4_mem.h" -#include "hw/sh4/sh4_interpreter.h" +#include "hw/sh4/dyna/shil.h" #include "cfg/option.h" #include #include diff --git a/core/hw/gdrom/gdromv3.cpp b/core/hw/gdrom/gdromv3.cpp index 1a29572ea..afe9d3aa3 100644 --- a/core/hw/gdrom/gdromv3.cpp +++ b/core/hw/gdrom/gdromv3.cpp @@ -9,7 +9,6 @@ #include "hw/holly/holly_intc.h" #include "hw/holly/sb.h" #include "hw/sh4/modules/dmac.h" -#include "hw/sh4/sh4_interpreter.h" #include "hw/sh4/sh4_mem.h" #include "hw/sh4/sh4_mmr.h" #include "hw/sh4/sh4_sched.h" diff --git a/core/hw/sh4/dyna/shil.cpp b/core/hw/sh4/dyna/shil.cpp index 635e3277c..c044173c0 100644 --- a/core/hw/sh4/dyna/shil.cpp +++ b/core/hw/sh4/dyna/shil.cpp @@ -15,14 +15,22 @@ void AnalyseBlock(RuntimeBlockInfo* blk) u32 getRegOffset(Sh4RegType reg) { - if (reg >= reg_r0 && reg <= reg_r15) - return offsetof(Sh4Context, r[reg - reg_r0]); - if (reg >= reg_r0_Bank && reg <= reg_r7_Bank) - return offsetof(Sh4Context, r_bank[reg - reg_r0_Bank]); - if (reg >= reg_fr_0 && reg <= reg_fr_15) - return offsetof(Sh4Context, xffr[reg - reg_fr_0 + 16]); - if (reg >= reg_xf_0 && reg <= reg_xf_15) - return offsetof(Sh4Context, xffr[reg - reg_xf_0]); + if (reg >= reg_r0 && reg <= reg_r15) { + const size_t regofs = (reg - reg_r0) * sizeof(u32); + return offsetof(Sh4Context, r[0]) + regofs; + } + if (reg >= reg_r0_Bank && reg <= reg_r7_Bank) { + const size_t regofs = (reg - reg_r0_Bank) * sizeof(u32); + return offsetof(Sh4Context, r_bank[0]) + regofs; + } + if (reg >= reg_fr_0 && reg <= reg_fr_15) { + const size_t regofs = (reg - reg_fr_0) * sizeof(float); + return offsetof(Sh4Context, xffr[16]) + regofs; + } + if (reg >= reg_xf_0 && reg <= reg_xf_15) { + const size_t regofs = (reg - reg_xf_0) * sizeof(float); + return offsetof(Sh4Context, xffr[0]) + regofs; + } switch (reg) { case reg_gbr: return offsetof(Sh4Context, gbr); diff --git a/core/hw/sh4/interpr/sh4_opcodes.cpp b/core/hw/sh4/interpr/sh4_opcodes.cpp index 70cdb5595..df43c32aa 100644 --- a/core/hw/sh4/interpr/sh4_opcodes.cpp +++ b/core/hw/sh4/interpr/sh4_opcodes.cpp @@ -3,7 +3,6 @@ */ #include "types.h" -#include "hw/sh4/sh4_interpreter.h" #include "hw/sh4/sh4_mem.h" #include "hw/sh4/sh4_mmr.h" #include "hw/sh4/sh4_core.h" @@ -1123,7 +1122,7 @@ sh4op(i0000_0000_0011_1000) sh4op(i0000_nnnn_1001_0011) { #ifdef STRICT_MODE - ocache.WriteBack(r[GetN(op)], false, true); + ocache.WriteBack(ctx->r[GetN(op)], false, true); #endif } @@ -1131,7 +1130,7 @@ sh4op(i0000_nnnn_1001_0011) sh4op(i0000_nnnn_1010_0011) { #ifdef STRICT_MODE - ocache.WriteBack(r[GetN(op)], true, true); + ocache.WriteBack(ctx->r[GetN(op)], true, true); #endif } @@ -1139,7 +1138,7 @@ sh4op(i0000_nnnn_1010_0011) sh4op(i0000_nnnn_1011_0011) { #ifdef STRICT_MODE - ocache.WriteBack(r[GetN(op)], true, false); + ocache.WriteBack(ctx->r[GetN(op)], true, false); #endif } diff --git a/core/hw/sh4/interpr/sh4_opcodes.h b/core/hw/sh4/interpr/sh4_opcodes.h index 3748685b4..f2a64275d 100644 --- a/core/hw/sh4/interpr/sh4_opcodes.h +++ b/core/hw/sh4/interpr/sh4_opcodes.h @@ -1,6 +1,6 @@ #pragma once #include "types.h" -#include "../sh4_interpreter.h" +#include "hw/sh4/sh4_opcode_list.h" /* Opcodes :) */ diff --git a/core/hw/sh4/sh4_core.h b/core/hw/sh4/sh4_core.h index cf50036b1..4a0aa6779 100644 --- a/core/hw/sh4/sh4_core.h +++ b/core/hw/sh4/sh4_core.h @@ -1,7 +1,9 @@ #pragma once #include "types.h" #include "sh4_if.h" +#include +int UpdateSystem_INTC(); void UpdateFPSCR(); bool UpdateSR(); void RestoreHostRoundingMode(); diff --git a/core/hw/sh4/sh4_core_regs.cpp b/core/hw/sh4/sh4_core_regs.cpp index 4102c60d5..4a23d2b1a 100644 --- a/core/hw/sh4/sh4_core_regs.cpp +++ b/core/hw/sh4/sh4_core_regs.cpp @@ -46,21 +46,21 @@ bool UpdateSR() static u32 old_rm = 0xFF; static u32 old_dn = 0xFF; -static void setHostRoundingMode() +static void setHostRoundingMode(u32 roundingMode, u32 denorm2zero) { - if (old_rm != Sh4cntx.fpscr.RM || old_dn != Sh4cntx.fpscr.DN) + if (old_rm != roundingMode || old_dn != denorm2zero) { - old_rm = Sh4cntx.fpscr.RM; - old_dn = Sh4cntx.fpscr.DN; + old_rm = roundingMode; + old_dn = denorm2zero; //Correct rounding is required by some games (SOTB, etc) #ifdef _MSC_VER - if (Sh4cntx.fpscr.RM == 1) //if round to 0 , set the flag + if (roundingMode == 1) // if round to 0 , set the flag _controlfp(_RC_CHOP, _MCW_RC); else _controlfp(_RC_NEAR, _MCW_RC); - if (Sh4cntx.fpscr.DN) //denormals are considered 0 + if (denorm2zero == 1) // denormals are considered 0 _controlfp(_DN_FLUSH, _MCW_DN); else _controlfp(_DN_SAVE, _MCW_DN); @@ -70,20 +70,20 @@ static void setHostRoundingMode() u32 temp=0x1f80; //no flush to zero && round to nearest - if (Sh4cntx.fpscr.RM==1) //if round to 0 , set the flag + if (roundingMode==1) // if round to 0 , set the flag temp|=(3<<13); - if (Sh4cntx.fpscr.DN) //denormals are considered 0 + if (denorm2zero == 1) // denormals are considered 0 temp|=(1<<15); asm("ldmxcsr %0" : : "m"(temp)); #elif HOST_CPU==CPU_ARM static const unsigned int offMask = 0x04086060; unsigned int onMask = 0x02000000; - if (Sh4cntx.fpscr.RM == 1) //if round to 0 , set the flag + if (roundingMode == 1) onMask |= 3 << 22; - if (Sh4cntx.fpscr.DN) + if (denorm2zero == 1) onMask |= 1 << 24; #ifdef __ANDROID__ @@ -109,10 +109,10 @@ static void setHostRoundingMode() static const unsigned long off_mask = 0x04080000; unsigned long on_mask = 0x02000000; // DN=1 Any operation involving one or more NaNs returns the Default NaN - if (Sh4cntx.fpscr.RM == 1) // if round to 0, set the flag + if (roundingMode == 1) on_mask |= 3 << 22; - if (Sh4cntx.fpscr.DN) + if (denorm2zero == 1) on_mask |= 1 << 24; // flush denormalized numbers to zero asm volatile @@ -140,23 +140,17 @@ void UpdateFPSCR() ChangeFP(); // FPU bank change Sh4cntx.old_fpscr = Sh4cntx.fpscr; - setHostRoundingMode(); + setHostRoundingMode(p_sh4rcb->cntx.fpscr.RM, p_sh4rcb->cntx.fpscr.DN); } void RestoreHostRoundingMode() { old_rm = 0xFF; old_dn = 0xFF; - setHostRoundingMode(); + setHostRoundingMode(p_sh4rcb->cntx.fpscr.RM, p_sh4rcb->cntx.fpscr.DN); } void setDefaultRoundingMode() { - u32 savedRM = Sh4cntx.fpscr.RM; - u32 savedDN = Sh4cntx.fpscr.DN; - Sh4cntx.fpscr.RM = 0; - Sh4cntx.fpscr.DN = 0; - setHostRoundingMode(); - Sh4cntx.fpscr.RM = savedRM; - Sh4cntx.fpscr.DN = savedDN; + setHostRoundingMode(0, 0); } diff --git a/core/hw/sh4/sh4_interpreter.h b/core/hw/sh4/sh4_interpreter.h index db58d327e..47dd2e444 100644 --- a/core/hw/sh4/sh4_interpreter.h +++ b/core/hw/sh4/sh4_interpreter.h @@ -35,5 +35,3 @@ class Sh4Interpreter : public Sh4Executor static constexpr int CPU_RATIO = 8; #endif }; - -int UpdateSystem_INTC(); diff --git a/core/hw/sh4/sh4_mem.cpp b/core/hw/sh4/sh4_mem.cpp index f7ca1f1c5..196cdf58f 100644 --- a/core/hw/sh4/sh4_mem.cpp +++ b/core/hw/sh4/sh4_mem.cpp @@ -11,6 +11,7 @@ #include "hw/pvr/pvr_mem.h" #include "hw/mem/addrspace.h" #include "hw/sh4/modules/mmu.h" +#include "cfg/option.h" #ifdef STRICT_MODE #include "sh4_cache.h" diff --git a/core/hw/sh4/sh4_mmr.cpp b/core/hw/sh4/sh4_mmr.cpp index 5d60d9406..314ff8f82 100644 --- a/core/hw/sh4/sh4_mmr.cpp +++ b/core/hw/sh4/sh4_mmr.cpp @@ -12,7 +12,6 @@ #include "serialize.h" #include "sh4_interrupts.h" #include "sh4_sched.h" -#include "sh4_interpreter.h" #include #include diff --git a/core/rec-x86/rec_x86.cpp b/core/rec-x86/rec_x86.cpp index b67667c1c..ac353df0f 100644 --- a/core/rec-x86/rec_x86.cpp +++ b/core/rec-x86/rec_x86.cpp @@ -22,7 +22,6 @@ #include "rec_x86.h" #include "hw/sh4/sh4_core.h" -#include "hw/sh4/sh4_interpreter.h" #include "hw/sh4/sh4_interrupts.h" #include "hw/sh4/sh4_mem.h" #include "hw/mem/addrspace.h" From 1cadeb276eb94232f33402f8e53d3028d4729a4c Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 8 Nov 2024 22:01:48 +0100 Subject: [PATCH 17/81] sh4: test fix --- core/reios/gdrom_hle.cpp | 1 - tests/src/Sh4InterpreterTest.cpp | 1 + tests/src/sh4_ops.h | 8 -------- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/core/reios/gdrom_hle.cpp b/core/reios/gdrom_hle.cpp index 1795ab471..1b16c3630 100644 --- a/core/reios/gdrom_hle.cpp +++ b/core/reios/gdrom_hle.cpp @@ -9,7 +9,6 @@ #include "hw/sh4/sh4_mem.h" #include "hw/sh4/sh4_sched.h" #include "hw/sh4/sh4_core.h" -#undef r #include "gdrom_hle.h" #include "hw/gdrom/gdromv3.h" diff --git a/tests/src/Sh4InterpreterTest.cpp b/tests/src/Sh4InterpreterTest.cpp index bd7642216..0cb4dc296 100644 --- a/tests/src/Sh4InterpreterTest.cpp +++ b/tests/src/Sh4InterpreterTest.cpp @@ -31,6 +31,7 @@ class Sh4InterpreterTest : public Sh4OpTest { emu.dc_reset(true); ctx = &p_sh4rcb->cntx; sh4 = Get_Sh4Interpreter(); + sh4->Init(); } void PrepareOp(u16 op, u16 op2 = 0, u16 op3 = 0) override { diff --git a/tests/src/sh4_ops.h b/tests/src/sh4_ops.h index 4d5189532..25b34fcff 100644 --- a/tests/src/sh4_ops.h +++ b/tests/src/sh4_ops.h @@ -23,14 +23,6 @@ #include "types.h" #include "hw/sh4/sh4_if.h" #include "hw/mem/addrspace.h" -#include "hw/sh4/sh4_core.h" -#undef r -#undef fr -#undef sr -#undef mac -#undef gbr -#undef fpscr -#undef fpul constexpr u32 REG_MAGIC = 0xbaadf00d; From e394840344cb7ab1212bac53bc6d8578507e5904 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 9 Nov 2024 13:25:55 +0100 Subject: [PATCH 18/81] sh4: pass sh4 context to dynarecs, sh4cycles and caches Reorder RuntimeBlockInfo members to save space --- core/hw/sh4/dyna/blockmanager.h | 12 ++--- core/hw/sh4/dyna/driver.cpp | 13 ++--- core/hw/sh4/dyna/ngen.h | 2 +- core/hw/sh4/interpr/sh4_interpreter.cpp | 3 ++ core/hw/sh4/sh4_cache.h | 16 +++++- core/hw/sh4/sh4_cycles.h | 17 +++--- core/rec-ARM/rec_arm.cpp | 31 ++++++----- core/rec-ARM64/rec_arm64.cpp | 70 ++++++++++++++----------- core/rec-x64/rec_x64.cpp | 44 ++++++++-------- core/rec-x64/xbyak_base.h | 6 ++- core/rec-x86/rec_x86.cpp | 58 ++++++++++---------- core/rec-x86/rec_x86.h | 4 +- core/rec-x86/x86_ops.cpp | 10 ++-- 13 files changed, 163 insertions(+), 123 deletions(-) diff --git a/core/hw/sh4/dyna/blockmanager.h b/core/hw/sh4/dyna/blockmanager.h index 333190489..255f42803 100644 --- a/core/hw/sh4/dyna/blockmanager.h +++ b/core/hw/sh4/dyna/blockmanager.h @@ -16,8 +16,8 @@ struct RuntimeBlockInfo bool Setup(u32 pc,fpscr_t fpu_cfg); u32 addr; - DynarecCodeEntryPtr code; u32 vaddr; + DynarecCodeEntryPtr code; u32 host_code_size; //in bytes u32 sh4_code_size; //in bytes @@ -27,8 +27,8 @@ struct RuntimeBlockInfo u32 guest_opcodes; u32 host_opcodes; // set by host code generator, optional bool has_fpu_op; - u32 blockcheck_failures; bool temp_block; + u32 blockcheck_failures; u32 BranchBlock; //if not 0xFFFFFFFF then jump target u32 NextBlock; //if not 0xFFFFFFFF then next block (by position) @@ -42,8 +42,11 @@ struct RuntimeBlockInfo BlockEndType BlockType; bool has_jcond; + bool read_only; std::vector oplist; + //predecessors references + std::vector pre_refs; bool containsCode(const void *ptr) { @@ -56,16 +59,11 @@ struct RuntimeBlockInfo return 0; } - //predecessors references - std::vector pre_refs; - void AddRef(const RuntimeBlockInfoPtr& other); void RemRef(const RuntimeBlockInfoPtr& other); void Discard(); void SetProtectedFlags(); - - bool read_only; }; void bm_WriteBlockMap(const std::string& file); diff --git a/core/hw/sh4/dyna/driver.cpp b/core/hw/sh4/dyna/driver.cpp index d3595897b..7981513ce 100644 --- a/core/hw/sh4/dyna/driver.cpp +++ b/core/hw/sh4/dyna/driver.cpp @@ -92,7 +92,7 @@ void Sh4Recompiler::clear_temp_cache(bool full) void Sh4Recompiler::ResetCache() { - INFO_LOG(DYNAREC, "recSh4:Dynarec Cache clear at %08X free space %d", Sh4cntx.pc, codeBuffer.getFreeSpace()); + INFO_LOG(DYNAREC, "recSh4:Dynarec Cache clear at %08X free space %d", getContext()->pc, codeBuffer.getFreeSpace()); codeBuffer.reset(false); bm_ResetCache(); smc_hotspots.clear(); @@ -103,12 +103,13 @@ void Sh4Recompiler::Run() { RestoreHostRoundingMode(); - u8 *sh4_dyna_rcb = (u8 *)&Sh4cntx + sizeof(Sh4cntx); - INFO_LOG(DYNAREC, "cntx // fpcb offset: %td // pc offset: %td // pc %08X", (u8*)&sh4rcb.fpcb - sh4_dyna_rcb, (u8*)&sh4rcb.cntx.pc - sh4_dyna_rcb, sh4rcb.cntx.pc); + u8 *sh4_dyna_rcb = (u8 *)getContext() + sizeof(Sh4cntx); + INFO_LOG(DYNAREC, "cntx // fpcb offset: %td // pc offset: %td // pc %08X", (u8*)&sh4rcb.fpcb - sh4_dyna_rcb, + (u8*)&getContext()->pc - sh4_dyna_rcb, getContext()->pc); sh4Dynarec->mainloop(sh4_dyna_rcb); - ctx->CpuRunning = false; + getContext()->CpuRunning = false; } void AnalyseBlock(RuntimeBlockInfo* blk); @@ -355,7 +356,7 @@ void Sh4Recompiler::Init() bm_Init(); if (addrspace::virtmemEnabled()) - verify(&mem_b[0] == ((u8*)p_sh4rcb->cntx.sq_buffer + 512 + 0x0C000000)); + verify(&mem_b[0] == ((u8*)getContext()->sq_buffer + sizeof(Sh4Context) + 0x0C000000)); // Call the platform-specific magic to make the pages RWX CodeCache = nullptr; @@ -369,7 +370,7 @@ void Sh4Recompiler::Init() verify(CodeCache != nullptr); TempCodeCache = CodeCache + CODE_SIZE; - sh4Dynarec->init(codeBuffer); + sh4Dynarec->init(*getContext(), codeBuffer); bm_ResetCache(); } diff --git a/core/hw/sh4/dyna/ngen.h b/core/hw/sh4/dyna/ngen.h index 88c8f1d25..110cf14ea 100644 --- a/core/hw/sh4/dyna/ngen.h +++ b/core/hw/sh4/dyna/ngen.h @@ -85,7 +85,7 @@ class Sh4Dynarec { public: // Initialize the dynarec, which should keep a reference to the passed code buffer to generate code later. - virtual void init(Sh4CodeBuffer& codeBuffer) = 0; + virtual void init(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) = 0; // Compile the given block. // If smc_checks is true, add self-modifying code detection. // If optimize is true, use fast memory accesses if possible, that will be rewritten if they fail. diff --git a/core/hw/sh4/interpr/sh4_interpreter.cpp b/core/hw/sh4/interpr/sh4_interpreter.cpp index 543e43c63..3e45e9d88 100644 --- a/core/hw/sh4/interpr/sh4_interpreter.cpp +++ b/core/hw/sh4/interpr/sh4_interpreter.cpp @@ -187,6 +187,9 @@ void Sh4Interpreter::Init() { ctx = &p_sh4rcb->cntx; memset(ctx, 0, sizeof(*ctx)); + sh4cycles.init(ctx); + icache.init(ctx); + ocache.init(ctx); } void Sh4Interpreter::Term() diff --git a/core/hw/sh4/sh4_cache.h b/core/hw/sh4/sh4_cache.h index f753de753..e99fe7f46 100644 --- a/core/hw/sh4/sh4_cache.h +++ b/core/hw/sh4/sh4_cache.h @@ -55,6 +55,11 @@ static bool translatedArea(u32 area) class Sh4ICache { public: + void init(Sh4Context *ctx) { + this->ctx = ctx; + sh4cycles.init(ctx); + } + u16 ReadMem(u32 address) { bool cacheOn = false; @@ -177,7 +182,7 @@ class Sh4ICache return MmuError::BADADDR; const u32 area = address >> 29; - const bool userMode = p_sh4rcb->cntx.sr.MD == 0; + const bool userMode = ctx->sr.MD == 0; if (userMode) { @@ -222,6 +227,7 @@ class Sh4ICache std::array lines; Sh4Cycles sh4cycles; + Sh4Context *ctx = nullptr; }; extern Sh4ICache icache; @@ -232,6 +238,11 @@ extern Sh4ICache icache; class Sh4OCache { public: + void init(Sh4Context *ctx) { + this->ctx = ctx; + sh4cycles.init(ctx); + } + template T ReadMem(u32 address) { @@ -515,7 +526,7 @@ class Sh4OCache return lookup; } const u32 area = address >> 29; - const bool userMode = p_sh4rcb->cntx.sr.MD == 0; + const bool userMode = ctx->sr.MD == 0; // kernel mem protected in user mode if (userMode && (address & 0x80000000)) @@ -591,6 +602,7 @@ class Sh4OCache u64 writeBackBufferCycles = 0; u64 writeThroughBufferCycles = 0; Sh4Cycles sh4cycles; + Sh4Context *ctx = nullptr; }; extern Sh4OCache ocache; diff --git a/core/hw/sh4/sh4_cycles.h b/core/hw/sh4/sh4_cycles.h index ef203b907..246a1bd4d 100644 --- a/core/hw/sh4/sh4_cycles.h +++ b/core/hw/sh4/sh4_cycles.h @@ -27,24 +27,28 @@ class Sh4Cycles public: Sh4Cycles(int cpuRatio = 1) : cpuRatio(cpuRatio) {} + void init(Sh4Context *ctx) { + this->ctx = ctx; + } + void executeCycles(u16 op) { - Sh4cntx.cycle_counter -= countCycles(op); + ctx->cycle_counter -= countCycles(op); } void addCycles(int cycles) const { - Sh4cntx.cycle_counter -= cycles; + ctx->cycle_counter -= cycles; } void addReadAccessCycles(u32 addr, u32 size) const { - Sh4cntx.cycle_counter -= readAccessCycles(addr, size); + ctx->cycle_counter -= readAccessCycles(addr, size); } void addWriteAccessCycles(u32 addr, u32 size) const { - Sh4cntx.cycle_counter -= writeAccessCycles(addr, size); + ctx->cycle_counter -= writeAccessCycles(addr, size); } int countCycles(u16 op); @@ -55,8 +59,8 @@ class Sh4Cycles memOps = 0; } - static u64 now() { - return sh4_sched_now64() + SH4_TIMESLICE - Sh4cntx.cycle_counter; + u64 now() { + return sh4_sched_now64() + SH4_TIMESLICE - ctx->cycle_counter; } int readAccessCycles(u32 addr, u32 size) const { @@ -76,4 +80,5 @@ class Sh4Cycles sh4_eu lastUnit = CO; const int cpuRatio; int memOps = 0; + Sh4Context *ctx = nullptr; }; diff --git a/core/rec-ARM/rec_arm.cpp b/core/rec-ARM/rec_arm.cpp index 2a65ec139..4c0b36a8a 100644 --- a/core/rec-ARM/rec_arm.cpp +++ b/core/rec-ARM/rec_arm.cpp @@ -94,10 +94,12 @@ extern "C" char *stpcpy(char *dst, char const *src) struct DynaRBI : RuntimeBlockInfo { - DynaRBI(Sh4CodeBuffer& codeBuffer) : codeBuffer(codeBuffer) {} + DynaRBI(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) + : sh4ctx(sh4ctx), codeBuffer(codeBuffer) {} u32 Relink() override; Register T_reg; + Sh4Context& sh4ctx; Sh4CodeBuffer& codeBuffer; }; @@ -157,8 +159,10 @@ class Arm32Assembler : public MacroAssembler using BinaryOP = void (MacroAssembler::*)(Register, Register, const Operand&); public: - Arm32Assembler(Sh4CodeBuffer& codeBuffer) : MacroAssembler((u8 *)codeBuffer.get(), codeBuffer.getFreeSpace(), A32), codeBuffer(codeBuffer), reg(*this) {} - Arm32Assembler(Sh4CodeBuffer& codeBuffer, u8 *buffer, size_t size) : MacroAssembler(buffer, size, A32), codeBuffer(codeBuffer), reg(*this) {} + Arm32Assembler(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) + : MacroAssembler((u8 *)codeBuffer.get(), codeBuffer.getFreeSpace(), A32), sh4ctx(sh4ctx), codeBuffer(codeBuffer), reg(*this) {} + Arm32Assembler(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer, u8 *buffer, size_t size) + : MacroAssembler(buffer, size, A32), sh4ctx(sh4ctx), codeBuffer(codeBuffer), reg(*this) {} void compile(RuntimeBlockInfo* block, bool force_checks, bool optimise); void rewrite(Register raddr, Register rt, SRegister ft, DRegister fd, bool write, bool is_sq, mem_op_type optp); @@ -367,6 +371,7 @@ class Arm32Assembler : public MacroAssembler void genMmuLookup(RuntimeBlockInfo* block, const shil_opcode& op, u32 write, Register& raddr); void compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool optimise); + Sh4Context& sh4ctx; Sh4CodeBuffer& codeBuffer; arm_reg_alloc reg; struct CC_PS @@ -416,7 +421,7 @@ class Arm32Dynarec : public Sh4Dynarec sh4Dynarec = this; } - void init(Sh4CodeBuffer& codeBuffer) override; + void init(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) override; void reset() override; RuntimeBlockInfo *allocateBlock() override; void handleException(host_context_t &context) override; @@ -435,7 +440,7 @@ class Arm32Dynarec : public Sh4Dynarec } void compile(RuntimeBlockInfo* block, bool smc_check, bool optimise) override { - ass = new Arm32Assembler(*codeBuffer); + ass = new Arm32Assembler(*sh4ctx, *codeBuffer); ass->compile(block, smc_check, optimise); delete ass; ass = nullptr; @@ -457,6 +462,7 @@ class Arm32Dynarec : public Sh4Dynarec private: void generate_mainloop(); + Sh4Context *sh4ctx = nullptr; Sh4CodeBuffer *codeBuffer = nullptr; bool restarting = false; Arm32Assembler *ass = nullptr; @@ -465,7 +471,7 @@ static Arm32Dynarec instance; u32 DynaRBI::Relink() { - Arm32Assembler ass(codeBuffer, (u8 *)code + relink_offset, host_code_size - relink_offset); + Arm32Assembler ass(sh4ctx, codeBuffer, (u8 *)code + relink_offset, host_code_size - relink_offset); u32 size = ass.relinkBlock(this); @@ -846,7 +852,7 @@ bool Arm32Dynarec::rewrite(host_context_t& context, void *faultAddress) // ignore last 2 bits zeroed to avoid sigbus errors verify(fault_offs == 0 || (fault_offs & ~3) == (sh4_addr & 0x1FFFFFFC)); - ass = new Arm32Assembler(*codeBuffer, (u8 *)ptr, 12); + ass = new Arm32Assembler(*sh4ctx, *codeBuffer, (u8 *)ptr, 12); ass->rewrite(raddr, rt, ft, fd, !read, is_sq, optp); delete ass; ass = nullptr; @@ -2251,10 +2257,10 @@ void Arm32Dynarec::reset() ::mainloop = nullptr; unwinder.clear(); - if (p_sh4rcb->cntx.CpuRunning) + if (sh4ctx->CpuRunning) { // Force the dynarec out of mainloop() to regenerate it - p_sh4rcb->cntx.CpuRunning = 0; + sh4ctx->CpuRunning = 0; restarting = true; } else @@ -2267,7 +2273,7 @@ void Arm32Dynarec::generate_mainloop() return; INFO_LOG(DYNAREC, "Generating main loop"); - Arm32Assembler ass(*codeBuffer); + Arm32Assembler ass(*sh4ctx, *codeBuffer); ass.genMainLoop(); } @@ -2541,7 +2547,7 @@ void Arm32Assembler::genMainLoop() INFO_LOG(DYNAREC, "readm helpers: up to %p", GetCursorAddress()); } -void Arm32Dynarec::init(Sh4CodeBuffer& codeBuffer) +void Arm32Dynarec::init(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) { INFO_LOG(DYNAREC, "Initializing the ARM32 dynarec"); @@ -2563,6 +2569,7 @@ void Arm32Dynarec::init(Sh4CodeBuffer& codeBuffer) ccmap[shop_setab] = hi; ccnmap[shop_setab] = ls; + this->sh4ctx = &sh4ctx; this->codeBuffer = &codeBuffer; } @@ -2574,6 +2581,6 @@ void Arm32Dynarec::handleException(host_context_t &context) RuntimeBlockInfo* Arm32Dynarec::allocateBlock() { generate_mainloop(); // FIXME why is this needed? - return new DynaRBI(*codeBuffer); + return new DynaRBI(*sh4ctx, *codeBuffer); }; #endif diff --git a/core/rec-ARM64/rec_arm64.cpp b/core/rec-ARM64/rec_arm64.cpp index 66d17c106..f9e89e801 100644 --- a/core/rec-ARM64/rec_arm64.cpp +++ b/core/rec-ARM64/rec_arm64.cpp @@ -49,10 +49,12 @@ using namespace vixl::aarch64; struct DynaRBI : RuntimeBlockInfo { - DynaRBI(Sh4CodeBuffer& codeBuffer) : codeBuffer(codeBuffer) {} + DynaRBI(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) + : sh4ctx(sh4ctx), codeBuffer(codeBuffer) {} u32 Relink() override; private: + Sh4Context& sh4ctx; Sh4CodeBuffer& codeBuffer; }; @@ -126,10 +128,11 @@ class Arm64Assembler : public MacroAssembler typedef void (MacroAssembler::*Arm64Fop_RRR)(const VRegister&, const VRegister&, const VRegister&); public: - Arm64Assembler(Sh4CodeBuffer& codeBuffer) : Arm64Assembler(codeBuffer, codeBuffer.get()) { - } + Arm64Assembler(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) + : Arm64Assembler(sh4ctx, codeBuffer, codeBuffer.get()) { } - Arm64Assembler(Sh4CodeBuffer& codeBuffer, void *buffer) : MacroAssembler((u8 *)buffer, codeBuffer.getFreeSpace()), regalloc(this), codeBuffer(codeBuffer) + Arm64Assembler(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer, void *buffer) + : MacroAssembler((u8 *)buffer, codeBuffer.getFreeSpace()), regalloc(this), sh4ctx(sh4ctx), codeBuffer(codeBuffer) { call_regs.push_back((const WRegister*)&w0); call_regs.push_back((const WRegister*)&w1); @@ -264,7 +267,7 @@ class Arm64Assembler : public MacroAssembler regalloc.DoAlloc(block); // scheduler - Ldr(w1, sh4_context_mem_operand(&Sh4cntx.cycle_counter)); + Ldr(w1, sh4_context_mem_operand(&sh4ctx.cycle_counter)); Cmp(w1, 0); Label cycles_remaining; B(&cycles_remaining, pl); @@ -274,7 +277,7 @@ class Arm64Assembler : public MacroAssembler Bind(&cycles_remaining); Sub(w1, w1, block->guest_cycles); - Str(w1, sh4_context_mem_operand(&Sh4cntx.cycle_counter)); + Str(w1, sh4_context_mem_operand(&sh4ctx.cycle_counter)); for (size_t i = 0; i < block->oplist.size(); i++) { @@ -287,7 +290,7 @@ class Arm64Assembler : public MacroAssembler if (op.rs1._imm) // if NeedPC() { Mov(w10, op.rs2._imm); - Str(w10, sh4_context_mem_operand(&Sh4cntx.pc)); + Str(w10, sh4_context_mem_operand(&sh4ctx.pc)); } Mov(x0, x28); @@ -1069,7 +1072,7 @@ class Arm64Assembler : public MacroAssembler MemOperand sh4_context_mem_operand(void *p) { - u32 offset = (u8*)p - (u8*)&p_sh4rcb->cntx; + u32 offset = (u8*)p - (u8*)&sh4ctx; verify((offset & 3) == 0 && offset <= 16380); // FIXME 64-bit regs need multiple of 8 up to 32760 return MemOperand(x28, offset); } @@ -1163,7 +1166,7 @@ class Arm64Assembler : public MacroAssembler #endif { Mov(w29, block->BranchBlock); - Str(w29, sh4_context_mem_operand(&Sh4cntx.pc)); + Str(w29, sh4_context_mem_operand(&sh4ctx.pc)); GenBranch(arm64_no_update); } } @@ -1177,9 +1180,9 @@ class Arm64Assembler : public MacroAssembler // next_pc = branch_pc_value; if (block->has_jcond) - Ldr(w11, sh4_context_mem_operand(&Sh4cntx.jdyn)); + Ldr(w11, sh4_context_mem_operand(&sh4ctx.jdyn)); else - Ldr(w11, sh4_context_mem_operand(&Sh4cntx.sr.T)); + Ldr(w11, sh4_context_mem_operand(&sh4ctx.sr.T)); Cmp(w11, block->BlockType & 1); @@ -1207,7 +1210,7 @@ class Arm64Assembler : public MacroAssembler #endif { Mov(w29, block->BranchBlock); - Str(w29, sh4_context_mem_operand(&Sh4cntx.pc)); + Str(w29, sh4_context_mem_operand(&sh4ctx.pc)); GenBranch(arm64_no_update); } } @@ -1235,7 +1238,7 @@ class Arm64Assembler : public MacroAssembler #endif { Mov(w29, block->NextBlock); - Str(w29, sh4_context_mem_operand(&Sh4cntx.pc)); + Str(w29, sh4_context_mem_operand(&sh4ctx.pc)); GenBranch(arm64_no_update); } } @@ -1247,7 +1250,7 @@ class Arm64Assembler : public MacroAssembler case BET_DynamicRet: // next_pc = *jdyn; - Str(w29, sh4_context_mem_operand(&Sh4cntx.pc)); + Str(w29, sh4_context_mem_operand(&sh4ctx.pc)); if (!mmu_enabled()) { // TODO Call no_update instead (and check CpuRunning less frequently?) @@ -1276,11 +1279,11 @@ class Arm64Assembler : public MacroAssembler Mov(w29, block->NextBlock); // else next_pc = *jdyn (already in w29) - Str(w29, sh4_context_mem_operand(&Sh4cntx.pc)); + Str(w29, sh4_context_mem_operand(&sh4ctx.pc)); GenCallRuntime(UpdateINTC); - Ldr(w29, sh4_context_mem_operand(&Sh4cntx.pc)); + Ldr(w29, sh4_context_mem_operand(&sh4ctx.pc)); GenBranch(arm64_no_update); break; @@ -1450,21 +1453,21 @@ class Arm64Assembler : public MacroAssembler Bind(&intc_sched); // w0 is pc, w1 is cycle_counter - Str(w0, sh4_context_mem_operand(&Sh4cntx.pc)); + Str(w0, sh4_context_mem_operand(&sh4ctx.pc)); // Add timeslice to cycle counter Add(w1, w1, SH4_TIMESLICE); - Str(w1, sh4_context_mem_operand(&Sh4cntx.cycle_counter)); - Ldr(w0, sh4_context_mem_operand(&Sh4cntx.CpuRunning)); + Str(w1, sh4_context_mem_operand(&sh4ctx.cycle_counter)); + Ldr(w0, sh4_context_mem_operand(&sh4ctx.CpuRunning)); Cbz(w0, &end_mainloop); Mov(x29, lr); // Save link register in case we return GenCallRuntime(UpdateSystem_INTC); Cbnz(w0, &do_interrupts); Mov(lr, x29); - Ldr(w0, sh4_context_mem_operand(&Sh4cntx.cycle_counter)); + Ldr(w0, sh4_context_mem_operand(&sh4ctx.cycle_counter)); Ret(); Bind(&do_interrupts); - Ldr(w29, sh4_context_mem_operand(&Sh4cntx.pc)); + Ldr(w29, sh4_context_mem_operand(&sh4ctx.pc)); B(&no_update); Bind(&end_mainloop); @@ -1499,12 +1502,12 @@ class Arm64Assembler : public MacroAssembler // w0: vaddr, w1: addr checkBlockFpu = GetCursorAddress(); Label fpu_enabled; - Ldr(w10, sh4_context_mem_operand(&Sh4cntx.sr.status)); + Ldr(w10, sh4_context_mem_operand(&sh4ctx.sr.status)); Tbz(w10, 15, &fpu_enabled); // test SR.FD bit Mov(w1, Sh4Ex_FpuDisabled); // exception code GenCallRuntime(Do_Exception); - Ldr(w29, sh4_context_mem_operand(&Sh4cntx.pc)); + Ldr(w29, sh4_context_mem_operand(&sh4ctx.pc)); B(&no_update); Bind(&fpu_enabled); // fallthrough @@ -1513,7 +1516,7 @@ class Arm64Assembler : public MacroAssembler // MMU Block check (no fpu) // w0: vaddr, w1: addr checkBlockNoFpu = GetCursorAddress(); - Ldr(w2, sh4_context_mem_operand(&Sh4cntx.pc)); + Ldr(w2, sh4_context_mem_operand(&sh4ctx.pc)); Cmp(w2, w0); Mov(w0, w1); B(&blockCheckFailLabel, ne); @@ -2174,6 +2177,7 @@ class Arm64Assembler : public MacroAssembler RuntimeBlockInfo* block = NULL; const int read_memory_rewrite_size = 5; // ubfx, add, ldr for fast access. calling a handler can use more than 3 depending on offset const int write_memory_rewrite_size = 5; // ubfx, add, str + Sh4Context& sh4ctx; Sh4CodeBuffer& codeBuffer; }; @@ -2184,9 +2188,10 @@ class Arm64Dynarec : public Sh4Dynarec sh4Dynarec = this; } - void init(Sh4CodeBuffer& codeBuffer) override + void init(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) override { INFO_LOG(DYNAREC, "Initializing the ARM64 dynarec"); + this->sh4ctx = &sh4ctx; this->codeBuffer = &codeBuffer; } @@ -2195,10 +2200,10 @@ class Arm64Dynarec : public Sh4Dynarec unwinder.clear(); ::mainloop = nullptr; - if (p_sh4rcb->cntx.CpuRunning) + if (sh4ctx->CpuRunning) { // Force the dynarec out of mainloop() to regenerate it - p_sh4rcb->cntx.CpuRunning = 0; + sh4ctx->CpuRunning = 0; restarting = true; } else @@ -2226,7 +2231,7 @@ class Arm64Dynarec : public Sh4Dynarec { verify(codeBuffer->getFreeSpace() >= 16 * 1024); - compiler = new Arm64Assembler(*codeBuffer); + compiler = new Arm64Assembler(*sh4ctx, *codeBuffer); compiler->compileBlock(block, smc_checks, optimise); @@ -2257,7 +2262,7 @@ class Arm64Dynarec : public Sh4Dynarec if (::mainloop != nullptr) return; jitWriteProtect(*codeBuffer, false); - compiler = new Arm64Assembler(*codeBuffer); + compiler = new Arm64Assembler(*sh4ctx, *codeBuffer); compiler->GenMainloop(); @@ -2269,7 +2274,7 @@ class Arm64Dynarec : public Sh4Dynarec RuntimeBlockInfo* allocateBlock() override { generate_mainloop(); - return new DynaRBI(*codeBuffer); + return new DynaRBI(*sh4ctx, *codeBuffer); } void handleException(host_context_t &context) override @@ -2340,7 +2345,7 @@ class Arm64Dynarec : public Sh4Dynarec // Skip the preceding ops (add, ubfx) u32 *code_rewrite = code_ptr - 2; - Arm64Assembler *assembler = new Arm64Assembler(*codeBuffer, code_rewrite); + Arm64Assembler *assembler = new Arm64Assembler(*sh4ctx, *codeBuffer, code_rewrite); if (is_read) assembler->GenReadMemorySlow(size); else if (!is_read && size >= 4 && (context.x0 >> 26) == 0x38) @@ -2358,6 +2363,7 @@ class Arm64Dynarec : public Sh4Dynarec private: Arm64Assembler* compiler = nullptr; bool restarting = false; + Sh4Context *sh4ctx = nullptr; Sh4CodeBuffer *codeBuffer = nullptr; }; @@ -2368,7 +2374,7 @@ u32 DynaRBI::Relink() #ifndef NO_BLOCK_LINKING //printf("DynaRBI::Relink %08x\n", this->addr); jitWriteProtect(codeBuffer, false); - Arm64Assembler *compiler = new Arm64Assembler(codeBuffer, (u8 *)this->code + this->relink_offset); + Arm64Assembler *compiler = new Arm64Assembler(sh4ctx, codeBuffer, (u8 *)this->code + this->relink_offset); u32 code_size = compiler->RelinkBlock(this); compiler->Finalize(true); diff --git a/core/rec-x64/rec_x64.cpp b/core/rec-x64/rec_x64.cpp index 96ad71b80..d7b6ba8c4 100644 --- a/core/rec-x64/rec_x64.cpp +++ b/core/rec-x64/rec_x64.cpp @@ -121,8 +121,8 @@ class BlockCompiler : public BaseXbyakRec using BaseCompiler = BaseXbyakRec; friend class BaseXbyakRec; - BlockCompiler(Sh4CodeBuffer& codeBuffer) : BaseCompiler(codeBuffer), regalloc(this) { } - BlockCompiler(Sh4CodeBuffer& codeBuffer, u8 *code_ptr) : BaseCompiler(codeBuffer, code_ptr), regalloc(this) { } + BlockCompiler(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) : BaseCompiler(sh4ctx, codeBuffer), regalloc(this) { } + BlockCompiler(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer, u8 *code_ptr) : BaseCompiler(sh4ctx, codeBuffer, code_ptr), regalloc(this) { } void compile(RuntimeBlockInfo* block, bool force_checks, bool optimise) { @@ -136,7 +136,7 @@ class BlockCompiler : public BaseXbyakRec if (mmu_enabled() && block->has_fpu_op) { Xbyak::Label fpu_enabled; - mov(rax, (uintptr_t)&p_sh4rcb->cntx.sr.status); + mov(rax, (uintptr_t)&sh4ctx.sr.status); test(dword[rax], 0x8000); // test SR.FD bit jz(fpu_enabled); mov(call_regs[0], block->vaddr); // pc @@ -145,7 +145,7 @@ class BlockCompiler : public BaseXbyakRec jmp(exit_block, T_NEAR); L(fpu_enabled); } - mov(rax, (uintptr_t)&p_sh4rcb->cntx.cycle_counter); + mov(rax, (uintptr_t)&sh4ctx.cycle_counter); sub(dword[rax], block->guest_cycles); regalloc.DoAlloc(block); @@ -167,12 +167,12 @@ class BlockCompiler : public BaseXbyakRec if (op.rs1._imm) { - mov(rax, (size_t)&p_sh4rcb->cntx.pc); + mov(rax, (size_t)&sh4ctx.pc); mov(dword[rax], op.rs2._imm); } mov(call_regs[1], op.rs3._imm); - mov(call_regs64[0], (uintptr_t)&p_sh4rcb->cntx); + mov(call_regs64[0], (uintptr_t)&sh4ctx); if (!mmu_enabled()) GenCall(OpDesc[op.rs3._imm]->oph); @@ -379,7 +379,7 @@ class BlockCompiler : public BaseXbyakRec } else { - mov(call_regs64[1], (uintptr_t)&p_sh4rcb->cntx); + mov(call_regs64[1], (uintptr_t)&sh4ctx); mov(rax, (size_t)&do_sqw_nommu); saveXmmRegisters(); call(qword[rax]); @@ -472,7 +472,7 @@ class BlockCompiler : public BaseXbyakRec regalloc.Cleanup(); current_opid = -1; - mov(rax, (size_t)&p_sh4rcb->cntx.pc); + mov(rax, (size_t)&sh4ctx.pc); switch (block->BlockType) { @@ -492,9 +492,9 @@ class BlockCompiler : public BaseXbyakRec mov(dword[rax], block->NextBlock); if (block->has_jcond) - mov(rdx, (size_t)&Sh4cntx.jdyn); + mov(rdx, (size_t)&sh4ctx.jdyn); else - mov(rdx, (size_t)&Sh4cntx.sr.T); + mov(rdx, (size_t)&sh4ctx.sr.T); cmp(dword[rdx], block->BlockType & 1); Xbyak::Label branch_not_taken; @@ -509,7 +509,7 @@ class BlockCompiler : public BaseXbyakRec case BET_DynamicCall: case BET_DynamicRet: //next_pc = *jdyn; - mov(rdx, (size_t)&Sh4cntx.jdyn); + mov(rdx, (size_t)&sh4ctx.jdyn); mov(edx, dword[rdx]); mov(dword[rax], edx); break; @@ -518,7 +518,7 @@ class BlockCompiler : public BaseXbyakRec case BET_StaticIntr: if (block->BlockType == BET_DynamicIntr) { //next_pc = *jdyn; - mov(rdx, (size_t)&Sh4cntx.jdyn); + mov(rdx, (size_t)&sh4ctx.jdyn); mov(edx, dword[rdx]); mov(dword[rax], edx); } @@ -684,7 +684,7 @@ class BlockCompiler : public BaseXbyakRec Xbyak::Label run_loop; L(run_loop); Xbyak::Label end_run_loop; - mov(rax, (size_t)&p_sh4rcb->cntx.CpuRunning); + mov(rax, (size_t)&sh4ctx.CpuRunning); mov(edx, dword[rax]); test(edx, edx); @@ -693,11 +693,11 @@ class BlockCompiler : public BaseXbyakRec //slice_loop: Xbyak::Label slice_loop; L(slice_loop); - mov(rax, (size_t)&p_sh4rcb->cntx.pc); + mov(rax, (size_t)&sh4ctx.pc); mov(call_regs[0], dword[rax]); call(bm_GetCodeByVAddr); call(rax); - mov(rax, (uintptr_t)&p_sh4rcb->cntx.cycle_counter); + mov(rax, (uintptr_t)&sh4ctx.cycle_counter); mov(ecx, dword[rax]); test(ecx, ecx); jg(slice_loop); @@ -1058,7 +1058,7 @@ class BlockCompiler : public BaseXbyakRec // same at compile and run times. if (mmu_enabled()) { - mov(rax, (uintptr_t)&p_sh4rcb->cntx.pc); + mov(rax, (uintptr_t)&sh4ctx.pc); cmp(dword[rax], block->vaddr); jne(reinterpret_cast(&ngen_blockcheckfail)); } @@ -1161,7 +1161,7 @@ class BlockCompiler : public BaseXbyakRec shr(r9d, 26); cmp(r9d, 0x38); jne(no_sqw); - mov(rax, (uintptr_t)p_sh4rcb->cntx.sq_buffer); + mov(rax, (uintptr_t)sh4ctx.sq_buffer); and_(call_regs[0], 0x3F); if (size == MemSize::S32) @@ -1318,7 +1318,7 @@ class X64Dynarec : public Sh4Dynarec size_t protSize = codeBuffer->getFreeSpace(); virtmem::jit_set_exec(protStart, protSize, false); - ccCompiler = new BlockCompiler(*codeBuffer); + ccCompiler = new BlockCompiler(*sh4ctx, *codeBuffer); try { ccCompiler->compile(block, smc_checks, optimise); } catch (const Xbyak::Error& e) { @@ -1329,8 +1329,9 @@ class X64Dynarec : public Sh4Dynarec virtmem::jit_set_exec(protStart, protSize, true); } - void init(Sh4CodeBuffer& codeBuffer) override + void init(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) override { + this->sh4ctx = &sh4ctx; this->codeBuffer = &codeBuffer; } @@ -1370,7 +1371,7 @@ class X64Dynarec : public Sh4Dynarec virtmem::jit_set_exec(protStart, protSize, false); u8 *retAddr = *(u8 **)context.rsp - 5; - BlockCompiler compiler(*codeBuffer, retAddr); + BlockCompiler compiler(*sh4ctx, *codeBuffer, retAddr); bool rc = false; try { rc = compiler.rewriteMemAccess(context); @@ -1397,7 +1398,7 @@ class X64Dynarec : public Sh4Dynarec size_t protSize = codeBuffer->getFreeSpace(); virtmem::jit_set_exec(protStart, protSize, false); - BlockCompiler compiler(*codeBuffer); + BlockCompiler compiler(*sh4ctx, *codeBuffer); try { compiler.genMainloop(); } catch (const Xbyak::Error& e) { @@ -1407,6 +1408,7 @@ class X64Dynarec : public Sh4Dynarec } private: + Sh4Context *sh4ctx = nullptr; Sh4CodeBuffer *codeBuffer = nullptr; BlockCompiler *ccCompiler = nullptr; }; diff --git a/core/rec-x64/xbyak_base.h b/core/rec-x64/xbyak_base.h index 044795f74..1fd69bf4a 100644 --- a/core/rec-x64/xbyak_base.h +++ b/core/rec-x64/xbyak_base.h @@ -28,8 +28,9 @@ template class BaseXbyakRec : public Xbyak::CodeGenerator { protected: - BaseXbyakRec(Sh4CodeBuffer& codeBuffer) : BaseXbyakRec(codeBuffer, (u8 *)codeBuffer.get()) { } - BaseXbyakRec(Sh4CodeBuffer& codeBuffer, u8 *code_ptr) : Xbyak::CodeGenerator(codeBuffer.getFreeSpace(), code_ptr), codeBuffer(codeBuffer) { } + BaseXbyakRec(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) : BaseXbyakRec(sh4ctx, codeBuffer, (u8 *)codeBuffer.get()) { } + BaseXbyakRec(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer, u8 *code_ptr) + : Xbyak::CodeGenerator(codeBuffer.getFreeSpace(), code_ptr), sh4ctx(sh4ctx), codeBuffer(codeBuffer) { } using BinaryOp = void (BaseXbyakRec::*)(const Xbyak::Operand&, const Xbyak::Operand&); using BinaryFOp = void (BaseXbyakRec::*)(const Xbyak::Xmm&, const Xbyak::Operand&); @@ -773,6 +774,7 @@ class BaseXbyakRec : public Xbyak::CodeGenerator } } + Sh4Context& sh4ctx; Sh4CodeBuffer& codeBuffer; private: diff --git a/core/rec-x86/rec_x86.cpp b/core/rec-x86/rec_x86.cpp index ac353df0f..d029e8464 100644 --- a/core/rec-x86/rec_x86.cpp +++ b/core/rec-x86/rec_x86.cpp @@ -69,11 +69,13 @@ void X86RegAlloc::Writeback_FPU(u32 reg, s8 nreg) struct DynaRBI : RuntimeBlockInfo { - DynaRBI(Sh4CodeBuffer *codeBuffer) : codeBuffer(codeBuffer) {} + DynaRBI(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) + : sh4ctx(sh4ctx), codeBuffer(codeBuffer) {} u32 Relink() override; private: - Sh4CodeBuffer *codeBuffer; + Sh4Context& sh4ctx; + Sh4CodeBuffer& codeBuffer; }; @@ -108,26 +110,26 @@ void X86Compiler::compile(RuntimeBlockInfo* block, bool force_checks, bool optim if (mmu_enabled() && block->has_fpu_op) { Xbyak::Label fpu_enabled; - mov(eax, dword[&Sh4cntx.sr.status]); + mov(eax, dword[&sh4ctx.sr.status]); test(eax, 0x8000); // test SR.FD bit jz(fpu_enabled); push(Sh4Ex_FpuDisabled); // exception code push(block->vaddr); // pc call((void (*)())Do_Exception); add(esp, 8); - mov(ecx, dword[&Sh4cntx.pc]); + mov(ecx, dword[&sh4ctx.pc]); jmp((const void *)no_update); L(fpu_enabled); } - mov(eax, dword[&Sh4cntx.cycle_counter]); + mov(eax, dword[&sh4ctx.cycle_counter]); test(eax, eax); Xbyak::Label no_up; jg(no_up); mov(ecx, block->vaddr); call((const void *)intc_sched); L(no_up); - sub(dword[&Sh4cntx.cycle_counter], block->guest_cycles); + sub(dword[&sh4ctx.cycle_counter], block->guest_cycles); regalloc.doAlloc(block); @@ -297,16 +299,16 @@ u32 X86Compiler::relinkBlock(RuntimeBlockInfo* block) case BET_DynamicIntr: if (block->BlockType == BET_StaticIntr) { - mov(dword[&Sh4cntx.pc], block->NextBlock); + mov(dword[&sh4ctx.pc], block->NextBlock); } else { mov(eax, dword[GetRegPtr(reg_pc_dyn)]); - mov(dword[&Sh4cntx.pc], eax); + mov(dword[&sh4ctx.pc], eax); } call(UpdateINTC); - mov(ecx, dword[&Sh4cntx.pc]); + mov(ecx, dword[&sh4ctx.pc]); jmp((const void *)no_update); break; @@ -323,7 +325,7 @@ u32 X86Compiler::relinkBlock(RuntimeBlockInfo* block) u32 DynaRBI::Relink() { - X86Compiler *compiler = new X86Compiler(*codeBuffer, (u8*)code + relink_offset); + X86Compiler *compiler = new X86Compiler(sh4ctx, codeBuffer, (u8*)code + relink_offset); u32 codeSize = compiler->relinkBlock(this); delete compiler; @@ -439,14 +441,14 @@ void X86Compiler::genMainloop() Xbyak::Label longjmpLabel; L(longjmpLabel); - mov(ecx, dword[&Sh4cntx.pc]); + mov(ecx, dword[&sh4ctx.pc]); //next_pc _MUST_ be on ecx Xbyak::Label cleanup; //no_update: Xbyak::Label no_updateLabel; L(no_updateLabel); - mov(edx, dword[&Sh4cntx.CpuRunning]); + mov(edx, dword[&sh4ctx.CpuRunning]); cmp(edx, 0); jz(cleanup); if (!mmu_enabled()) @@ -458,14 +460,14 @@ void X86Compiler::genMainloop() } else { - mov(dword[&Sh4cntx.pc], ecx); + mov(dword[&sh4ctx.pc], ecx); call((void *)bm_GetCodeByVAddr); jmp(eax); } //cleanup: L(cleanup); - mov(dword[&Sh4cntx.pc], ecx); + mov(dword[&sh4ctx.pc], ecx); #ifndef _WIN32 // 16-byte alignment add(esp, 12); @@ -481,7 +483,7 @@ void X86Compiler::genMainloop() Xbyak::Label do_iter; L(do_iter); add(esp, 4); // pop intc_sched() return address - mov(ecx, dword[&Sh4cntx.pc]); + mov(ecx, dword[sh4ctx.pc]); jmp(no_updateLabel); //ngen_LinkBlock_Shared_stub: @@ -503,8 +505,8 @@ void X86Compiler::genMainloop() unwinder.endProlog(0); Xbyak::Label intc_schedLabel; L(intc_schedLabel); - add(dword[&Sh4cntx.cycle_counter], SH4_TIMESLICE); - mov(dword[&Sh4cntx.pc], ecx); + add(dword[&sh4ctx.cycle_counter], SH4_TIMESLICE); + mov(dword[&sh4ctx.pc], ecx); call((void *)UpdateSystem_INTC); cmp(eax, 0); jnz(do_iter); @@ -525,7 +527,7 @@ void X86Compiler::genMainloop() //ngen_LinkBlock_Generic_stub: Xbyak::Label ngen_LinkBlock_Generic_label; L(ngen_LinkBlock_Generic_label); - mov(edx, dword[&Sh4cntx.jdyn]); + mov(edx, dword[&sh4ctx.jdyn]); jmp(ngen_LinkBlock_Shared_stub); genMemHandlers(); @@ -568,7 +570,7 @@ void X86Compiler::genMainloop() Xbyak::Label jumpblockLabel; cmp(eax, 0); jne(jumpblockLabel); - mov(ecx, dword[&Sh4cntx.pc]); + mov(ecx, dword[&sh4ctx.pc]); jmp(no_updateLabel); L(jumpblockLabel); } @@ -809,7 +811,7 @@ void X86Compiler::checkBlock(bool smc_checks, RuntimeBlockInfo* block) if (mmu_enabled()) { - mov(eax, dword[&Sh4cntx.pc]); + mov(eax, dword[&sh4ctx.pc]); cmp(eax, block->vaddr); jne(reinterpret_cast(ngen_blockcheckfail)); } @@ -842,8 +844,9 @@ class X86Dynarec : public Sh4Dynarec sh4Dynarec = this; } - void init(Sh4CodeBuffer& codeBuffer) override + void init(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) override { + this->sh4ctx = &sh4ctx; this->codeBuffer = &codeBuffer; } @@ -857,7 +860,7 @@ class X86Dynarec : public Sh4Dynarec if (::mainloop != nullptr) return; - compiler = new X86Compiler(*codeBuffer); + compiler = new X86Compiler(*sh4ctx, *codeBuffer); try { compiler->genMainloop(); @@ -876,10 +879,10 @@ class X86Dynarec : public Sh4Dynarec ::mainloop = nullptr; unwinder.clear(); - if (p_sh4rcb->cntx.CpuRunning) + if (sh4ctx->CpuRunning) { // Force the dynarec out of mainloop() to regenerate it - p_sh4rcb->cntx.CpuRunning = 0; + sh4ctx->CpuRunning = 0; restarting = true; } else @@ -889,7 +892,7 @@ class X86Dynarec : public Sh4Dynarec RuntimeBlockInfo* allocateBlock() override { generate_mainloop(); - return new DynaRBI(codeBuffer); + return new DynaRBI(*sh4ctx, *codeBuffer); } void mainloop(void* v_cntx) override @@ -911,7 +914,7 @@ class X86Dynarec : public Sh4Dynarec void compile(RuntimeBlockInfo* block, bool smc_checks, bool optimise) override { - compiler = new X86Compiler(*codeBuffer); + compiler = new X86Compiler(*sh4ctx, *codeBuffer); try { compiler->compile(block, smc_checks, optimise); @@ -928,7 +931,7 @@ class X86Dynarec : public Sh4Dynarec // init() not called yet return false; u8 *rewriteAddr = *(u8 **)context.esp - 5; - X86Compiler *compiler = new X86Compiler(*codeBuffer, rewriteAddr); + X86Compiler *compiler = new X86Compiler(*sh4ctx, *codeBuffer, rewriteAddr); bool rv = compiler->rewriteMemAccess(context); delete compiler; @@ -956,6 +959,7 @@ class X86Dynarec : public Sh4Dynarec } private: + Sh4Context *sh4ctx = nullptr; Sh4CodeBuffer *codeBuffer = nullptr; X86Compiler *compiler = nullptr; bool restarting = false; diff --git a/core/rec-x86/rec_x86.h b/core/rec-x86/rec_x86.h index 8eecf77fc..ff9133f99 100644 --- a/core/rec-x86/rec_x86.h +++ b/core/rec-x86/rec_x86.h @@ -31,8 +31,8 @@ class X86Compiler : public BaseXbyakRec public: using BaseCompiler = BaseXbyakRec; - X86Compiler(Sh4CodeBuffer& codeBuffer) : BaseCompiler(codeBuffer), regalloc(this) { } - X86Compiler(Sh4CodeBuffer& codeBuffer, u8 *code_ptr) : BaseCompiler(codeBuffer, code_ptr), regalloc(this) { } + X86Compiler(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) : BaseCompiler(sh4ctx, codeBuffer), regalloc(this) { } + X86Compiler(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer, u8 *code_ptr) : BaseCompiler(sh4ctx, codeBuffer, code_ptr), regalloc(this) { } void compile(RuntimeBlockInfo* block, bool force_checks, bool optimise); diff --git a/core/rec-x86/x86_ops.cpp b/core/rec-x86/x86_ops.cpp index f423c39fb..c355965a9 100644 --- a/core/rec-x86/x86_ops.cpp +++ b/core/rec-x86/x86_ops.cpp @@ -139,12 +139,12 @@ void X86Compiler::genMemHandlers() and_(ecx, 0x3F); if (size == MemSize::S32) - mov(dword[(size_t)p_sh4rcb->cntx.sq_buffer + ecx], edx); + mov(dword[(size_t)sh4ctx.sq_buffer + ecx], edx); else if (size >= MemSize::F32) { - movss(dword[(size_t)p_sh4rcb->cntx.sq_buffer + ecx], xmm0); + movss(dword[(size_t)sh4ctx.sq_buffer + ecx], xmm0); if (size == MemSize::F64) - movss(dword[((size_t)p_sh4rcb->cntx.sq_buffer + 4) + ecx], xmm1); + movss(dword[((size_t)sh4ctx.sq_buffer + 4) + ecx], xmm1); } ret(); L(no_sqw); @@ -327,8 +327,8 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& push(block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc } if (op.rs1.is_imm() && op.rs1.imm_value()) - mov(dword[&Sh4cntx.pc], op.rs2.imm_value()); - mov(ecx, (uintptr_t)&Sh4cntx); + mov(dword[&sh4ctx.pc], op.rs2.imm_value()); + mov(ecx, (uintptr_t)&sh4ctx); mov(edx, op.rs3.imm_value()); if (!mmu_enabled()) genCall(OpDesc[op.rs3.imm_value()]->oph); From 7c1c5817d36d4a73e8ec52509060711e6776e6ff Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 9 Nov 2024 16:54:47 +0100 Subject: [PATCH 19/81] sh4: move sqWrite func into sh4 context. Simplify usage Allow passing sh4 context to dynarec canonical implementations Reduce FPCB padding to PAGE_SIZE except on arm32 --- core/hw/sh4/dyna/ngen.h | 15 ++++---- core/hw/sh4/dyna/shil_canonical.h | 31 ++++++----------- core/hw/sh4/interpr/sh4_fpu.cpp | 2 +- core/hw/sh4/interpr/sh4_opcodes.cpp | 9 ++--- core/hw/sh4/sh4_core.h | 1 - core/hw/sh4/sh4_core_regs.cpp | 16 ++++----- core/hw/sh4/sh4_if.h | 21 ++++++----- core/hw/sh4/storeq.cpp | 54 +++++++++++------------------ core/rec-ARM/rec_arm.cpp | 40 +++++++++++++-------- core/rec-ARM64/rec_arm64.cpp | 36 +++++++++---------- core/rec-x64/rec_x64.cpp | 33 ++++++++++-------- core/rec-x86/rec_x86.cpp | 6 ++++ core/rec-x86/x86_ops.cpp | 13 +++---- 13 files changed, 135 insertions(+), 142 deletions(-) diff --git a/core/hw/sh4/dyna/ngen.h b/core/hw/sh4/dyna/ngen.h index 110cf14ea..852f8d15e 100644 --- a/core/hw/sh4/dyna/ngen.h +++ b/core/hw/sh4/dyna/ngen.h @@ -42,13 +42,14 @@ extern void (*ngen_FailedToFindBlock)(); //Canonical callback interface enum CanonicalParamType { - CPT_u32, - CPT_u32rv, - CPT_u64rvL, - CPT_u64rvH, - CPT_f32, - CPT_f32rv, - CPT_ptr, + CPT_u32, // u32 param + CPT_u32rv, // u32 return value + CPT_u64rvL, // u64 return value lsb + CPT_u64rvH, // u64 return value msb + CPT_f32, // f32 param + CPT_f32rv, // f32 return value + CPT_ptr, // register pointer + CPT_sh4ctx, // Sh4Context pointer }; bool rdv_readMemImmediate(u32 addr, int size, void*& ptr, bool& isRam, u32& physAddr, RuntimeBlockInfo* block = nullptr); diff --git a/core/hw/sh4/dyna/shil_canonical.h b/core/hw/sh4/dyna/shil_canonical.h index b4230e34a..21e1a8230 100644 --- a/core/hw/sh4/dyna/shil_canonical.h +++ b/core/hw/sh4/dyna/shil_canonical.h @@ -34,6 +34,7 @@ #define shil_cf_arg_u32(x) sh4Dynarec->canonParam(op, &op->x, CPT_u32); #define shil_cf_arg_f32(x) sh4Dynarec->canonParam(op, &op->x, CPT_f32); #define shil_cf_arg_ptr(x) sh4Dynarec->canonParam(op, &op->x, CPT_ptr); + #define shil_cf_arg_sh4ctx() sh4Dynarec->canonParam(op, nullptr, CPT_sh4ctx); #define shil_cf_rv_u32(x) sh4Dynarec->canonParam(op, &op->x, CPT_u32rv); #define shil_cf_rv_f32(x) sh4Dynarec->canonParam(op, &op->x, CPT_f32rv); #define shil_cf_rv_u64(x) sh4Dynarec->canonParam(op, &op->rd, CPT_u64rvL); sh4Dynarec->canonParam(op, &op->rd2, CPT_u64rvH); @@ -227,11 +228,12 @@ shil_opc_end() shil_opc(sync_fpscr) shil_canonical ( -void, f1, (), - UpdateFPSCR(); +void, f1, (Sh4Context *ctx), + Sh4Context::UpdateFPSCR(ctx); ) shil_compile ( + shil_cf_arg_sh4ctx(); shil_cf(f1); ) shil_opc_end() @@ -660,8 +662,8 @@ shil_opc_end() shil_opc(div1) shil_canonical ( -u64,f1,(u32 a, s32 b, u32 T), - sr_t& sr = Sh4cntx.sr; +u64,f1,(u32 a, s32 b, u32 T, Sh4Context *ctx), + sr_t& sr = ctx->sr; bool qxm = sr.Q ^ sr.M; sr.Q = (int)a < 0; a = (a << 1) | T; @@ -675,6 +677,7 @@ u64,f1,(u32 a, s32 b, u32 T), ) shil_compile ( + shil_cf_arg_sh4ctx(); shil_cf_arg_u32(rs3); shil_cf_arg_u32(rs2); shil_cf_arg_u32(rs1); @@ -796,27 +799,15 @@ shil_opc_end() shil_opc(pref) shil_canonical ( -void,f1,(u32 r1), - if ((r1>>26) == 0x38) do_sqw_mmu(r1); -) - -shil_canonical -( -void,f2,(u32 r1), - if ((r1>>26) == 0x38) do_sqw_nommu(r1, &p_sh4rcb->cntx); +void,f1,(u32 r1, Sh4Context *ctx), + if ((r1 >> 26) == 0x38) ctx->doSqWrite(r1, ctx); ) shil_compile ( + shil_cf_arg_sh4ctx(); shil_cf_arg_u32(rs1); - if (CCN_MMUCR.AT) - { - shil_cf(f1); - } - else - { - shil_cf(f2); - } + shil_cf(f1); ) shil_opc_end() diff --git a/core/hw/sh4/interpr/sh4_fpu.cpp b/core/hw/sh4/interpr/sh4_fpu.cpp index e4048e8df..d5e08b432 100644 --- a/core/hw/sh4/interpr/sh4_fpu.cpp +++ b/core/hw/sh4/interpr/sh4_fpu.cpp @@ -487,7 +487,7 @@ sh4op(i1111_1011_1111_1101) { ctx->fpscr.FR = 1 - ctx->fpscr.FR; - UpdateFPSCR(); + Sh4Context::UpdateFPSCR(ctx); } //fschg diff --git a/core/hw/sh4/interpr/sh4_opcodes.cpp b/core/hw/sh4/interpr/sh4_opcodes.cpp index df43c32aa..26df085e0 100644 --- a/core/hw/sh4/interpr/sh4_opcodes.cpp +++ b/core/hw/sh4/interpr/sh4_opcodes.cpp @@ -1150,10 +1150,7 @@ sh4op(i0000_nnnn_1000_0011) if ((Dest >> 26) == 0x38) // Store Queue { - if (CCN_MMUCR.AT) - do_sqw_mmu(Dest); - else - do_sqw_nommu(Dest, ctx); + ctx->doSqWrite(Dest, ctx); } else { @@ -1888,7 +1885,7 @@ sh4op(i0100_nnnn_0110_0110) u32 n = GetN(op); ReadMemU32(ctx->fpscr.full, ctx->r[n]); - UpdateFPSCR(); + Sh4Context::UpdateFPSCR(ctx); ctx->r[n] += 4; } @@ -1911,7 +1908,7 @@ sh4op(i0100_nnnn_0110_1010) { u32 n = GetN(op); ctx->fpscr.full = ctx->r[n]; - UpdateFPSCR(); + Sh4Context::UpdateFPSCR(ctx); } //ldc ,SR diff --git a/core/hw/sh4/sh4_core.h b/core/hw/sh4/sh4_core.h index 4a0aa6779..4e9902c39 100644 --- a/core/hw/sh4/sh4_core.h +++ b/core/hw/sh4/sh4_core.h @@ -4,7 +4,6 @@ #include int UpdateSystem_INTC(); -void UpdateFPSCR(); bool UpdateSR(); void RestoreHostRoundingMode(); void setDefaultRoundingMode(); diff --git a/core/hw/sh4/sh4_core_regs.cpp b/core/hw/sh4/sh4_core_regs.cpp index 4a23d2b1a..365ddbeaf 100644 --- a/core/hw/sh4/sh4_core_regs.cpp +++ b/core/hw/sh4/sh4_core_regs.cpp @@ -16,11 +16,6 @@ static void ChangeGPR() std::swap((u32 (&)[8])Sh4cntx.r, Sh4cntx.r_bank); } -static void ChangeFP() -{ - std::swap((f32 (&)[16])Sh4cntx.xffr, *(f32 (*)[16])&Sh4cntx.xffr[16]); -} - //called when sr is changed and we must check for reg banks etc. //returns true if interrupt pending bool UpdateSR() @@ -134,13 +129,14 @@ static void setHostRoundingMode(u32 roundingMode, u32 denorm2zero) } //called when fpscr is changed and we must check for reg banks etc.. -void UpdateFPSCR() +void DYNACALL Sh4Context::UpdateFPSCR(Sh4Context *ctx) { - if (Sh4cntx.fpscr.FR != Sh4cntx.old_fpscr.FR) - ChangeFP(); // FPU bank change + if (ctx->fpscr.FR != ctx->old_fpscr.FR) + // FPU bank change + std::swap((f32 (&)[16])ctx->xffr, *(f32 (*)[16])&ctx->xffr[16]); - Sh4cntx.old_fpscr = Sh4cntx.fpscr; - setHostRoundingMode(p_sh4rcb->cntx.fpscr.RM, p_sh4rcb->cntx.fpscr.DN); + ctx->old_fpscr = ctx->fpscr; + setHostRoundingMode(ctx->fpscr.RM, ctx->fpscr.DN); } void RestoreHostRoundingMode() diff --git a/core/hw/sh4/sh4_if.h b/core/hw/sh4/sh4_if.h index 491f4e666..a06a37a90 100644 --- a/core/hw/sh4/sh4_if.h +++ b/core/hw/sh4/sh4_if.h @@ -119,6 +119,10 @@ struct alignas(32) SQBuffer { u8 data[32]; }; +void setSqwHandler(); +struct Sh4Context; +typedef void DYNACALL SQWriteFunc(u32 dst, Sh4Context *ctx); + struct alignas(64) Sh4Context { union @@ -160,6 +164,8 @@ struct alignas(64) Sh4Context u32 temp_reg; int cycle_counter; + + SQWriteFunc *doSqWrite; }; u64 raw[64]; }; @@ -204,6 +210,8 @@ struct alignas(64) Sh4Context fr(n * 2 + 1) = t.sgl[0]; } + static void DYNACALL UpdateFPSCR(Sh4Context *ctx); + private: union DoubleReg { @@ -213,11 +221,6 @@ struct alignas(64) Sh4Context }; static_assert(sizeof(Sh4Context) == 512, "Invalid Sh4Context size"); -void setSqwHandler(); -void DYNACALL do_sqw_mmu(u32 dst); - -typedef void DYNACALL SQWriteFunc(u32 dst, Sh4Context *ctx); - #define FPCB_SIZE (RAM_SIZE_MAX/2) #define FPCB_MASK (FPCB_SIZE -1) #if HOST_CPU == CPU_ARM @@ -226,21 +229,17 @@ typedef void DYNACALL SQWriteFunc(u32 dst, Sh4Context *ctx); // want to be an i8r4 value that can be substracted in one op (such as 0x4100000) #define FPCB_PAD 0x100000 #else -#define FPCB_PAD 0x10000 +#define FPCB_PAD PAGE_SIZE #endif struct alignas(PAGE_SIZE) Sh4RCB { void* fpcb[FPCB_SIZE]; - u8 _pad[FPCB_PAD - sizeof(Sh4Context) - sizeof(void *)]; - SQWriteFunc *do_sqw_nommu; + u8 _pad[FPCB_PAD - sizeof(Sh4Context)]; Sh4Context cntx; }; static_assert((sizeof(Sh4RCB) % PAGE_SIZE) == 0, "sizeof(Sh4RCB) not multiple of PAGE_SIZE"); extern Sh4RCB* p_sh4rcb; - -#define do_sqw_nommu sh4rcb.do_sqw_nommu - #define sh4rcb (*p_sh4rcb) #define Sh4cntx (sh4rcb.cntx) diff --git a/core/hw/sh4/storeq.cpp b/core/hw/sh4/storeq.cpp index 126ffc709..2f4f71267 100644 --- a/core/hw/sh4/storeq.cpp +++ b/core/hw/sh4/storeq.cpp @@ -22,14 +22,13 @@ static u32 CCN_QACR_TR[2]; template -void DYNACALL do_sqw(u32 Dest, Sh4Context *ctx) +static void DYNACALL sqWrite(u32 dest, Sh4Context *ctx) { - u32 Address; - + u32 address; //Translate the SQ addresses as needed if (mmu_on) { - mmu_TranslateSQW(Dest, &Address); + mmu_TranslateSQW(dest, &address); } else { @@ -38,57 +37,46 @@ void DYNACALL do_sqw(u32 Dest, Sh4Context *ctx) u32 QACR = CCN_QACR_TR[0]; //QACR has already 0xE000_0000 - Address = QACR + (Dest & ~0x1f); + address = QACR + (dest & ~0x1f); } - if (((Address >> 26) & 7) != 4)//Area 4 + if (((address >> 26) & 7) != 4)//Area 4 { - const SQBuffer *sq = &ctx->sq_buffer[(Dest >> 5) & 1]; - WriteMemBlock_nommu_sq(Address, sq); + const SQBuffer *sq = &ctx->sq_buffer[(dest >> 5) & 1]; + WriteMemBlock_nommu_sq(address, sq); } else { - TAWriteSQ(Address, ctx->sq_buffer); // TODO pass the correct SQBuffer instead of letting TAWriteSQ deal with it + TAWriteSQ(address, ctx->sq_buffer); // TODO pass the correct SQBuffer instead of letting TAWriteSQ deal with it } } -void DYNACALL do_sqw_mmu(u32 dst) { - do_sqw(dst, &p_sh4rcb->cntx); -} - -static void DYNACALL do_sqw_simplemmu(u32 dst, Sh4Context *ctx) { - do_sqw(dst, ctx); -} - //yes, this micro optimization makes a difference -static void DYNACALL do_sqw_nommu_area_3(u32 dst, Sh4Context *ctx) +static void DYNACALL sqWrite_nommu_area_3(u32 dest, Sh4Context *ctx) { SQBuffer *pmem = (SQBuffer *)((u8 *)ctx + sizeof(Sh4Context) + 0x0C000000); - pmem += (dst & (RAM_SIZE_MAX - 1)) >> 5; - *pmem = ctx->sq_buffer[(dst >> 5) & 1]; + pmem += (dest & (RAM_SIZE_MAX - 1)) >> 5; + *pmem = ctx->sq_buffer[(dest >> 5) & 1]; } -static void DYNACALL do_sqw_nommu_area_3_nonvmem(u32 dst, Sh4Context *ctx) +static void DYNACALL sqWrite_nommu_area_3_nonvmem(u32 dest, Sh4Context *ctx) { u8* pmem = &mem_b[0]; - memcpy((SQBuffer *)&pmem[dst & (RAM_MASK - 0x1F)], &ctx->sq_buffer[(dst >> 5) & 1], sizeof(SQBuffer)); -} - -static void DYNACALL do_sqw_nommu_full(u32 dst, Sh4Context *ctx) { - do_sqw(dst, ctx); + memcpy((SQBuffer *)&pmem[dest & (RAM_MASK - 0x1F)], &ctx->sq_buffer[(dest >> 5) & 1], sizeof(SQBuffer)); } -static void DYNACALL sqWriteTA(u32 dst, Sh4Context *ctx) +static void DYNACALL sqWriteTA(u32 dest, Sh4Context *ctx) { - TAWriteSQ(dst, ctx->sq_buffer); + TAWriteSQ(dest, ctx->sq_buffer); } void setSqwHandler() { + Sh4Context& ctx = p_sh4rcb->cntx; if (CCN_MMUCR.AT == 1) { - do_sqw_nommu = &do_sqw_simplemmu; + ctx.doSqWrite = &sqWrite; } else { @@ -100,17 +88,17 @@ void setSqwHandler() { case 3: if (addrspace::virtmemEnabled()) - do_sqw_nommu = &do_sqw_nommu_area_3; + ctx.doSqWrite = &sqWrite_nommu_area_3; else - do_sqw_nommu = &do_sqw_nommu_area_3_nonvmem; + ctx.doSqWrite = &sqWrite_nommu_area_3_nonvmem; break; case 4: - do_sqw_nommu = &sqWriteTA; + ctx.doSqWrite = &sqWriteTA; break; default: - do_sqw_nommu = &do_sqw_nommu_full; + ctx.doSqWrite = &sqWrite; break; } } diff --git a/core/rec-ARM/rec_arm.cpp b/core/rec-ARM/rec_arm.cpp index 4c0b36a8a..9203f110e 100644 --- a/core/rec-ARM/rec_arm.cpp +++ b/core/rec-ARM/rec_arm.cpp @@ -89,8 +89,8 @@ extern "C" char *stpcpy(char *dst, char const *src) } #endif -#undef do_sqw_nommu #define rcbOffset(x) (-sizeof(Sh4RCB) + offsetof(Sh4RCB, x)) +#define ctxOffset(x) (-sizeof(Sh4Context) + offsetof(Sh4Context, x)) struct DynaRBI : RuntimeBlockInfo { @@ -590,6 +590,7 @@ void Arm32Assembler::canonParam(const shil_opcode *op, const shil_param *par, Ca case CPT_u32: case CPT_ptr: case CPT_f32: + case CPT_sh4ctx: { CC_PS t = { tp, par }; CC_pars.push_back(t); @@ -614,6 +615,10 @@ void Arm32Assembler::canonCall(const shil_opcode *op, void *function) { Mov(rd, (u32)param.par->reg_ptr()); } + else if (param.type == CPT_sh4ctx) + { + Mov(rd, reinterpret_cast(&sh4ctx)); + } else { if (param.par->is_reg()) @@ -1198,10 +1203,10 @@ static void interpreter_fallback(Sh4Context *ctx, u16 op, OpCallFP *oph, u32 pc) } } -static void do_sqw_mmu_no_ex(u32 addr, u32 pc) +static void do_sqw_mmu_no_ex(u32 addr, Sh4Context *ctx, u32 pc) { try { - do_sqw_mmu(addr); + ctx->doSqWrite(addr, ctx); } catch (SH4ThrownException& ex) { if (pc & 1) { @@ -1680,6 +1685,11 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op call((void *)UpdateSR); break; + case shop_sync_fpscr: + Sub(r0, r8, sizeof(Sh4Context)); + call((void *)Sh4Context::UpdateFPSCR); + break; + case shop_test: case shop_seteq: case shop_setge: @@ -1805,15 +1815,15 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op cc = al; } + Sub(r1, r8, sizeof(Sh4Context)); if (mmu_enabled()) { - Mov(r1, block->vaddr + op->guest_offs - (op->delay_slot ? 1 : 0)); // pc + Mov(r2, block->vaddr + op->guest_offs - (op->delay_slot ? 1 : 0)); // pc call((void *)do_sqw_mmu_no_ex, cc); } else { - Ldr(r2, MemOperand(r8, rcbOffset(do_sqw_nommu))); - Sub(r1, r8, sizeof(Sh4Context)); + Ldr(r2, MemOperand(r8, ctxOffset(doSqWrite))); Blx(cc, r2); } } @@ -2187,7 +2197,7 @@ void Arm32Assembler::compile(RuntimeBlockInfo* block, bool force_checks, bool op } //scheduler - Ldr(r1, MemOperand(r8, rcbOffset(cntx.cycle_counter))); + Ldr(r1, MemOperand(r8, ctxOffset(cycle_counter))); Cmp(r1, 0); Label cyclesRemaining; B(pl, &cyclesRemaining); @@ -2205,7 +2215,7 @@ void Arm32Assembler::compile(RuntimeBlockInfo* block, bool force_checks, bool op { Sub(r1, r1, cycles); } - Str(r1, MemOperand(r8, rcbOffset(cntx.cycle_counter))); + Str(r1, MemOperand(r8, ctxOffset(cycle_counter))); //compile the block's opcodes shil_opcode* op; @@ -2373,7 +2383,7 @@ void Arm32Assembler::genMainLoop() Ldr(r8, MemOperand(sp)); // r8: context Mov(r9, (uintptr_t)mmuAddressLUT); // r9: mmu LUT } - Ldr(r4, MemOperand(r8, rcbOffset(cntx.pc))); // r4: pc + Ldr(r4, MemOperand(r8, ctxOffset(pc))); // r4: pc B(&no_updateLabel); // Go to mainloop ! // this code is here for fall-through behavior of do_iter Label do_iter; @@ -2381,9 +2391,9 @@ void Arm32Assembler::genMainLoop() // intc_sched: r0 is pc, r1 is cycle_counter intc_sched = GetCursorAddress(); Add(r1, r1, SH4_TIMESLICE); - Str(r1, MemOperand(r8, rcbOffset(cntx.cycle_counter))); - Str(r0, MemOperand(r8, rcbOffset(cntx.pc))); - Ldr(r0, MemOperand(r8, rcbOffset(cntx.CpuRunning))); + Str(r1, MemOperand(r8, ctxOffset(cycle_counter))); + Str(r0, MemOperand(r8, ctxOffset(pc))); + Ldr(r0, MemOperand(r8, ctxOffset(CpuRunning))); Cmp(r0, 0); B(eq, &cleanup); Mov(r4, lr); @@ -2391,17 +2401,17 @@ void Arm32Assembler::genMainLoop() Cmp(r0, 0); B(ne, &do_iter); Mov(lr, r4); - Ldr(r0, MemOperand(r8, rcbOffset(cntx.cycle_counter))); + Ldr(r0, MemOperand(r8, ctxOffset(cycle_counter))); Bx(lr); // do_iter: Bind(&do_iter); - Ldr(r4, MemOperand(r8, rcbOffset(cntx.pc))); + Ldr(r4, MemOperand(r8, ctxOffset(pc))); // no_update: no_update = GetCursorAddress(); Bind(&no_updateLabel); // next_pc _MUST_ be on r4 - Ldr(r0, MemOperand(r8, rcbOffset(cntx.CpuRunning))); + Ldr(r0, MemOperand(r8, ctxOffset(CpuRunning))); Cmp(r0, 0); B(eq, &cleanup); diff --git a/core/rec-ARM64/rec_arm64.cpp b/core/rec-ARM64/rec_arm64.cpp index f9e89e801..8bf27b8e8 100644 --- a/core/rec-ARM64/rec_arm64.cpp +++ b/core/rec-ARM64/rec_arm64.cpp @@ -45,8 +45,6 @@ using namespace vixl::aarch64; #include "oslib/virtmem.h" #include "emulator.h" -#undef do_sqw_nommu - struct DynaRBI : RuntimeBlockInfo { DynaRBI(Sh4Context& sh4ctx, Sh4CodeBuffer& codeBuffer) @@ -105,10 +103,10 @@ static void interpreter_fallback(Sh4Context *ctx, u16 op, OpCallFP *oph, u32 pc) } } -static void do_sqw_mmu_no_ex(u32 addr, u32 pc) +static void do_sqw_mmu_no_ex(u32 addr, Sh4Context *ctx, u32 pc) { try { - do_sqw_mmu(addr); + ctx->doSqWrite(addr, ctx); } catch (SH4ThrownException& ex) { if (pc & 1) { @@ -394,7 +392,8 @@ class Arm64Assembler : public MacroAssembler GenCallRuntime(UpdateSR); break; case shop_sync_fpscr: - GenCallRuntime(UpdateFPSCR); + Mov(x0, x28); + GenCallRuntime(Sh4Context::UpdateFPSCR); break; case shop_swaplb: @@ -792,17 +791,15 @@ class Arm64Assembler : public MacroAssembler Mov(w0, regalloc.MapRegister(op.rs1)); } + Mov(x1, x28); if (mmu_enabled()) { - Mov(w1, block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc - + Mov(w2, block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc GenCallRuntime(do_sqw_mmu_no_ex); } else { - Sub(x9, x28, offsetof(Sh4RCB, cntx) - offsetof(Sh4RCB, do_sqw_nommu)); - Ldr(x9, MemOperand(x9)); - Mov(x1, x28); + Ldr(x9, sh4_context_mem_operand(&sh4ctx.doSqWrite)); Blr(x9); } Bind(¬_sqw); @@ -984,7 +981,7 @@ class Arm64Assembler : public MacroAssembler CC_pars.clear(); } - void canonParam(const shil_opcode& op, const shil_param& prm, CanonicalParamType tp) + void canonParam(const shil_opcode& op, const shil_param *prm, CanonicalParamType tp) { switch (tp) { @@ -992,24 +989,25 @@ class Arm64Assembler : public MacroAssembler case CPT_u32: case CPT_ptr: case CPT_f32: + case CPT_sh4ctx: { - CC_PS t = { tp, &prm }; + CC_PS t = { tp, prm }; CC_pars.push_back(t); } break; case CPT_u64rvL: case CPT_u32rv: - host_reg_to_shil_param(prm, w0); + host_reg_to_shil_param(*prm, w0); break; case CPT_u64rvH: Lsr(x10, x0, 32); - host_reg_to_shil_param(prm, w10); + host_reg_to_shil_param(*prm, w10); break; case CPT_f32rv: - host_reg_to_shil_param(prm, s0); + host_reg_to_shil_param(*prm, s0); break; } } @@ -1027,10 +1025,8 @@ class Arm64Assembler : public MacroAssembler switch (CC_pars[i].type) { // push the params - case CPT_u32: shil_param_to_host_reg(prm, *call_regs[regused++]); - break; case CPT_f32: @@ -1047,8 +1043,12 @@ class Arm64Assembler : public MacroAssembler verify(prm.is_reg()); // push the ptr itself Mov(*call_regs64[regused++], reinterpret_cast(prm.reg_ptr())); + break; + case CPT_sh4ctx: + Mov(*call_regs64[regused++], reinterpret_cast(&sh4ctx)); break; + case CPT_u32rv: case CPT_u64rvL: case CPT_u64rvH: @@ -2246,7 +2246,7 @@ class Arm64Dynarec : public Sh4Dynarec void canonParam(const shil_opcode *op, const shil_param *par, CanonicalParamType tp) override { - compiler->canonParam(*op, *par, tp); + compiler->canonParam(*op, par, tp); } void canonCall(const shil_opcode *op, void *function) override diff --git a/core/rec-x64/rec_x64.cpp b/core/rec-x64/rec_x64.cpp index d7b6ba8c4..2941f1982 100644 --- a/core/rec-x64/rec_x64.cpp +++ b/core/rec-x64/rec_x64.cpp @@ -86,10 +86,10 @@ static void interpreter_fallback(Sh4Context *ctx, u16 op, OpCallFP *oph, u32 pc) } } -static void do_sqw_mmu_no_ex(u32 addr, u32 pc) +static void do_sqw_mmu_no_ex(u32 addr, Sh4Context *ctx, u32 pc) { try { - do_sqw_mmu(addr); + ctx->doSqWrite(addr, ctx); } catch (SH4ThrownException& ex) { handle_sh4_exception(ex, pc); } @@ -296,7 +296,8 @@ class BlockCompiler : public BaseXbyakRec GenCall(UpdateSR); break; case shop_sync_fpscr: - GenCall(UpdateFPSCR); + mov(call_regs64[0], (uintptr_t)&sh4ctx); + GenCall(Sh4Context::UpdateFPSCR); break; case shop_negc: @@ -371,16 +372,15 @@ class BlockCompiler : public BaseXbyakRec mov(call_regs[0], rn); } + mov(call_regs64[1], (uintptr_t)&sh4ctx); if (mmu_enabled()) { - mov(call_regs[1], block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc - + mov(call_regs[2], block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc GenCall(do_sqw_mmu_no_ex); } else { - mov(call_regs64[1], (uintptr_t)&sh4ctx); - mov(rax, (size_t)&do_sqw_nommu); + mov(rax, (size_t)&sh4ctx.doSqWrite); saveXmmRegisters(); call(qword[rax]); restoreXmmRegisters(); @@ -551,35 +551,36 @@ class BlockCompiler : public BaseXbyakRec CC_pars.clear(); } - void canonParam(const shil_opcode& op, const shil_param& prm, CanonicalParamType tp) { + void canonParam(const shil_opcode& op, const shil_param *prm, CanonicalParamType tp) { switch (tp) { case CPT_u32: case CPT_ptr: case CPT_f32: + case CPT_sh4ctx: { - CC_PS t = { tp, &prm }; + CC_PS t = { tp, prm }; CC_pars.push_back(t); + break; } - break; // store from EAX case CPT_u64rvL: case CPT_u32rv: mov(rcx, rax); - host_reg_to_shil_param(prm, ecx); + host_reg_to_shil_param(*prm, ecx); break; case CPT_u64rvH: // assuming CPT_u64rvL has just been called shr(rcx, 32); - host_reg_to_shil_param(prm, ecx); + host_reg_to_shil_param(*prm, ecx); break; // store from xmm0 case CPT_f32rv: - host_reg_to_shil_param(prm, xmm0); + host_reg_to_shil_param(*prm, xmm0); break; } } @@ -610,6 +611,10 @@ class BlockCompiler : public BaseXbyakRec mov(call_regs64[regused++], (size_t)prm.reg_ptr()); break; + case CPT_sh4ctx: + mov(call_regs64[regused++], (uintptr_t)&sh4ctx); + break; + default: // Other cases handled in canonParam break; @@ -1351,7 +1356,7 @@ class X64Dynarec : public Sh4Dynarec } void canonParam(const shil_opcode* op, const shil_param* par, CanonicalParamType tp) override { - ccCompiler->canonParam(*op, *par, tp); + ccCompiler->canonParam(*op, par, tp); } void canonCall(const shil_opcode* op, void* function) override { diff --git a/core/rec-x86/rec_x86.cpp b/core/rec-x86/rec_x86.cpp index d029e8464..bb23a11c2 100644 --- a/core/rec-x86/rec_x86.cpp +++ b/core/rec-x86/rec_x86.cpp @@ -365,6 +365,12 @@ void X86Compiler::ngen_CC_param(const shil_opcode& op, const shil_param& param, unwinder.allocStackPtr(getCurr(), 4); break; + case CPT_sh4ctx: + push((uintptr_t)&sh4ctx); + CC_stackSize += 4; + unwinder.allocStackPtr(getCurr(), 4); + break; + // store from EAX case CPT_u64rvL: case CPT_u32rv: diff --git a/core/rec-x86/x86_ops.cpp b/core/rec-x86/x86_ops.cpp index c355965a9..c7c14c8ae 100644 --- a/core/rec-x86/x86_ops.cpp +++ b/core/rec-x86/x86_ops.cpp @@ -307,10 +307,10 @@ static void DYNACALL interpreter_fallback(Sh4Context *ctx, u16 op, OpCallFP *oph } } -static void DYNACALL do_sqw_mmu_no_ex(u32 addr, u32 pc) +static void DYNACALL do_sqw_mmu_no_ex(u32 addr, Sh4Context *ctx, u32 pc) { try { - do_sqw_mmu(addr); + ctx->doSqWrite(addr, ctx); } catch (SH4ThrownException& ex) { handle_sh4_exception(ex, pc); } @@ -487,7 +487,8 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& genCallCdecl(UpdateSR); break; case shop_sync_fpscr: - genCallCdecl(UpdateFPSCR); + mov(ecx, (uintptr_t)&sh4ctx); + genCall(Sh4Context::UpdateFPSCR); break; case shop_pref: @@ -520,16 +521,16 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& mov(ecx, rn); } + mov(edx, (size_t)&sh4rcb.cntx); if (mmu_enabled()) { - mov(edx, block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc + push(block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc genCall(do_sqw_mmu_no_ex); } else { - mov(edx, (size_t)&sh4rcb.cntx); freezeXMM(); - call(dword[&do_sqw_nommu]); + call(dword[&sh4ctx.doSqWrite]); thawXMM(); } L(no_sqw); From 129673a84b2333f082f2b32f19e2d249a423b723 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 9 Nov 2024 18:44:51 +0100 Subject: [PATCH 20/81] dynarec: use sh4 ctx to get register pointers. prefer offset Move restoreHostRoundingMode() into Sh4Context --- core/debug/debug_agent.h | 14 +++--- core/hw/pvr/Renderer_if.cpp | 6 +-- core/hw/sh4/dyna/blockmanager.cpp | 2 +- core/hw/sh4/dyna/driver.cpp | 6 +-- core/hw/sh4/dyna/shil.cpp | 4 +- core/hw/sh4/dyna/shil.h | 5 +- core/hw/sh4/interpr/sh4_interpreter.cpp | 4 +- core/hw/sh4/sh4_core.h | 1 - core/hw/sh4/sh4_core_regs.cpp | 4 +- core/hw/sh4/sh4_if.h | 4 +- core/rec-ARM/rec_arm.cpp | 2 +- core/rec-ARM64/rec_arm64.cpp | 64 ++++++++++++++----------- core/rec-x64/rec_x64.cpp | 60 +++++++++++------------ core/rec-x64/xbyak_base.h | 22 ++++----- core/rec-x86/rec_x86.cpp | 46 +++++++++--------- core/rec-x86/rec_x86.h | 8 ++-- core/rec-x86/x86_ops.cpp | 32 ++++++------- 17 files changed, 144 insertions(+), 140 deletions(-) diff --git a/core/debug/debug_agent.h b/core/debug/debug_agent.h index 51b0ccf74..d2e03310c 100644 --- a/core/debug/debug_agent.h +++ b/core/debug/debug_agent.h @@ -122,9 +122,9 @@ class DebugAgent for (u32 i = 0; i < Sh4RegList.size(); i++) { if (Sh4RegList[i] == reg_sr_status) - allregs[i] = p_sh4rcb->cntx.sr.getFull(); + allregs[i] = Sh4cntx.sr.getFull(); else if (Sh4RegList[i] != NoReg) - allregs[i] = *GetRegPtr(Sh4RegList[i]); + allregs[i] = *GetRegPtr(Sh4cntx, Sh4RegList[i]); } *regs = &allregs[0]; return allregs.size(); @@ -134,7 +134,7 @@ class DebugAgent { for (u32 i = 0; i < Sh4RegList.size(); i++) if (Sh4RegList[i] != NoReg) - *GetRegPtr(Sh4RegList[i]) = regs[i]; + *GetRegPtr(Sh4cntx, Sh4RegList[i]) = regs[i]; } u32 readReg(u32 regNum) @@ -143,9 +143,9 @@ class DebugAgent return 0; Sh4RegType reg = Sh4RegList[regNum]; if (reg == reg_sr_status) - return p_sh4rcb->cntx.sr.getFull(); + return Sh4cntx.sr.getFull(); if (reg != NoReg) - return *GetRegPtr(reg); + return *GetRegPtr(Sh4cntx, reg); return 0; } void writeReg(u32 regNum, u32 value) @@ -154,9 +154,9 @@ class DebugAgent return; Sh4RegType reg = Sh4RegList[regNum]; if (reg == reg_sr_status) - p_sh4rcb->cntx.sr.setFull(value); + Sh4cntx.sr.setFull(value); else if (reg != NoReg) - *GetRegPtr(reg) = value; + *GetRegPtr(Sh4cntx, reg) = value; } const u8 *readMem(u32 addr, u32 len) diff --git a/core/hw/pvr/Renderer_if.cpp b/core/hw/pvr/Renderer_if.cpp index 55289b3ac..4e987f8e5 100644 --- a/core/hw/pvr/Renderer_if.cpp +++ b/core/hw/pvr/Renderer_if.cpp @@ -7,6 +7,7 @@ #include "serialize.h" #include "hw/holly/holly_intc.h" #include "hw/sh4/sh4_if.h" +#include "hw/sh4/sh4_core.h" #include "profiler/fc_profiler.h" #include "network/ggpo.h" @@ -90,15 +91,12 @@ class PvrMessageQueue } else { - void setDefaultRoundingMode(); - void RestoreHostRoundingMode(); - setDefaultRoundingMode(); // drain the queue after switching to !threaded rendering while (!queue.empty()) waitAndExecute(); execute(msg); - RestoreHostRoundingMode(); + Sh4cntx.restoreHostRoundingMode(); } } diff --git a/core/hw/sh4/dyna/blockmanager.cpp b/core/hw/sh4/dyna/blockmanager.cpp index d53700a49..ea8cce072 100644 --- a/core/hw/sh4/dyna/blockmanager.cpp +++ b/core/hw/sh4/dyna/blockmanager.cpp @@ -40,7 +40,7 @@ static bm_Map blkmap; u32 protected_blocks; u32 unprotected_blocks; -#define FPCA(x) ((DynarecCodeEntryPtr&)sh4rcb.fpcb[(x>>1)&FPCB_MASK]) +#define FPCA(x) ((DynarecCodeEntryPtr&)p_sh4rcb->fpcb[(x>>1)&FPCB_MASK]) // addr must be a physical address // This returns an executable address diff --git a/core/hw/sh4/dyna/driver.cpp b/core/hw/sh4/dyna/driver.cpp index 7981513ce..78009e77b 100644 --- a/core/hw/sh4/dyna/driver.cpp +++ b/core/hw/sh4/dyna/driver.cpp @@ -101,10 +101,10 @@ void Sh4Recompiler::ResetCache() void Sh4Recompiler::Run() { - RestoreHostRoundingMode(); + getContext()->restoreHostRoundingMode(); - u8 *sh4_dyna_rcb = (u8 *)getContext() + sizeof(Sh4cntx); - INFO_LOG(DYNAREC, "cntx // fpcb offset: %td // pc offset: %td // pc %08X", (u8*)&sh4rcb.fpcb - sh4_dyna_rcb, + u8 *sh4_dyna_rcb = (u8 *)getContext() + sizeof(Sh4Context); + INFO_LOG(DYNAREC, "cntx // fpcb offset: %td // pc offset: %td // pc %08X", (u8*)p_sh4rcb->fpcb - sh4_dyna_rcb, (u8*)&getContext()->pc - sh4_dyna_rcb, getContext()->pc); sh4Dynarec->mainloop(sh4_dyna_rcb); diff --git a/core/hw/sh4/dyna/shil.cpp b/core/hw/sh4/dyna/shil.cpp index c044173c0..3e3131c99 100644 --- a/core/hw/sh4/dyna/shil.cpp +++ b/core/hw/sh4/dyna/shil.cpp @@ -58,9 +58,9 @@ u32 getRegOffset(Sh4RegType reg) } } -u32* GetRegPtr(u32 reg) +u32* GetRegPtr(Sh4Context& ctx, u32 reg) { - return (u32 *)((u8 *)&p_sh4rcb->cntx + getRegOffset((Sh4RegType)reg)); + return (u32 *)((u8 *)&ctx + getRegOffset((Sh4RegType)reg)); } std::string name_reg(Sh4RegType reg) diff --git a/core/hw/sh4/dyna/shil.h b/core/hw/sh4/dyna/shil.h index cec8498c6..a53cf0adb 100644 --- a/core/hw/sh4/dyna/shil.h +++ b/core/hw/sh4/dyna/shil.h @@ -128,7 +128,7 @@ enum Sh4RegType }; u32 getRegOffset(Sh4RegType reg); -u32* GetRegPtr(u32 reg); +u32* GetRegPtr(Sh4Context& ctx, u32 reg); enum shil_param_type { @@ -231,7 +231,8 @@ struct shil_param bool is_imm_s8() const { return is_imm() && (int8_t)_imm == (int32_t)_imm; } - u32* reg_ptr() const { verify(is_reg()); return GetRegPtr(_reg); } + u32* reg_ptr(Sh4Context& ctx) const { verify(is_reg()); return GetRegPtr(ctx, _reg); } + u32 reg_offset() const { verify(is_reg()); return getRegOffset(_reg); } s32 reg_nofs() const { verify(is_reg()); return (int)getRegOffset(_reg) - sizeof(Sh4Context); } u32 reg_aofs() const { return -reg_nofs(); } diff --git a/core/hw/sh4/interpr/sh4_interpreter.cpp b/core/hw/sh4/interpr/sh4_interpreter.cpp index 3e45e9d88..6b826463e 100644 --- a/core/hw/sh4/interpr/sh4_interpreter.cpp +++ b/core/hw/sh4/interpr/sh4_interpreter.cpp @@ -41,7 +41,7 @@ u16 Sh4Interpreter::ReadNexOp() void Sh4Interpreter::Run() { Instance = this; - RestoreHostRoundingMode(); + ctx->restoreHostRoundingMode(); try { do @@ -83,7 +83,7 @@ void Sh4Interpreter::Step() verify(!ctx->CpuRunning); Instance = this; - RestoreHostRoundingMode(); + ctx->restoreHostRoundingMode(); try { u32 op = ReadNexOp(); ExecuteOpcode(op); diff --git a/core/hw/sh4/sh4_core.h b/core/hw/sh4/sh4_core.h index 4e9902c39..868616dc3 100644 --- a/core/hw/sh4/sh4_core.h +++ b/core/hw/sh4/sh4_core.h @@ -5,7 +5,6 @@ int UpdateSystem_INTC(); bool UpdateSR(); -void RestoreHostRoundingMode(); void setDefaultRoundingMode(); struct SH4ThrownException diff --git a/core/hw/sh4/sh4_core_regs.cpp b/core/hw/sh4/sh4_core_regs.cpp index 365ddbeaf..0ff120f1c 100644 --- a/core/hw/sh4/sh4_core_regs.cpp +++ b/core/hw/sh4/sh4_core_regs.cpp @@ -139,11 +139,11 @@ void DYNACALL Sh4Context::UpdateFPSCR(Sh4Context *ctx) setHostRoundingMode(ctx->fpscr.RM, ctx->fpscr.DN); } -void RestoreHostRoundingMode() +void Sh4Context::restoreHostRoundingMode() { old_rm = 0xFF; old_dn = 0xFF; - setHostRoundingMode(p_sh4rcb->cntx.fpscr.RM, p_sh4rcb->cntx.fpscr.DN); + setHostRoundingMode(fpscr.RM, fpscr.DN); } void setDefaultRoundingMode() diff --git a/core/hw/sh4/sh4_if.h b/core/hw/sh4/sh4_if.h index a06a37a90..32b5d682c 100644 --- a/core/hw/sh4/sh4_if.h +++ b/core/hw/sh4/sh4_if.h @@ -211,6 +211,7 @@ struct alignas(64) Sh4Context } static void DYNACALL UpdateFPSCR(Sh4Context *ctx); + void restoreHostRoundingMode(); private: union DoubleReg @@ -240,8 +241,7 @@ struct alignas(PAGE_SIZE) Sh4RCB static_assert((sizeof(Sh4RCB) % PAGE_SIZE) == 0, "sizeof(Sh4RCB) not multiple of PAGE_SIZE"); extern Sh4RCB* p_sh4rcb; -#define sh4rcb (*p_sh4rcb) -#define Sh4cntx (sh4rcb.cntx) +#define Sh4cntx (p_sh4rcb->cntx) //Get an interface to sh4 interpreter Sh4Executor *Get_Sh4Interpreter(); diff --git a/core/rec-ARM/rec_arm.cpp b/core/rec-ARM/rec_arm.cpp index 9203f110e..727d0e5fd 100644 --- a/core/rec-ARM/rec_arm.cpp +++ b/core/rec-ARM/rec_arm.cpp @@ -613,7 +613,7 @@ void Arm32Assembler::canonCall(const shil_opcode *op, void *function) CC_PS& param = CC_pars[i]; if (param.type == CPT_ptr) { - Mov(rd, (u32)param.par->reg_ptr()); + Mov(rd, (u32)param.par->reg_ptr(sh4ctx)); } else if (param.type == CPT_sh4ctx) { diff --git a/core/rec-ARM64/rec_arm64.cpp b/core/rec-ARM64/rec_arm64.cpp index 8bf27b8e8..5d5ac174e 100644 --- a/core/rec-ARM64/rec_arm64.cpp +++ b/core/rec-ARM64/rec_arm64.cpp @@ -212,7 +212,7 @@ class Arm64Assembler : public MacroAssembler Add(*ret_reg, regalloc.MapRegister(op.rs1), op.rs3._imm); else { - Ldr(*ret_reg, sh4_context_mem_operand(op.rs1.reg_ptr())); + Ldr(*ret_reg, sh4_context_mem_operand(op.rs1._reg)); Add(*ret_reg, *ret_reg, op.rs3._imm); } } @@ -222,8 +222,8 @@ class Arm64Assembler : public MacroAssembler Add(*ret_reg, regalloc.MapRegister(op.rs1), regalloc.MapRegister(op.rs3)); else { - Ldr(*ret_reg, sh4_context_mem_operand(op.rs1.reg_ptr())); - Ldr(w8, sh4_context_mem_operand(op.rs3.reg_ptr())); + Ldr(*ret_reg, sh4_context_mem_operand(op.rs1._reg)); + Ldr(w8, sh4_context_mem_operand(op.rs3._reg)); Add(*ret_reg, *ret_reg, w8); } } @@ -242,7 +242,7 @@ class Arm64Assembler : public MacroAssembler } else { - Ldr(*ret_reg, sh4_context_mem_operand(op.rs1.reg_ptr())); + Ldr(*ret_reg, sh4_context_mem_operand(op.rs1._reg)); } } else @@ -782,7 +782,7 @@ class Arm64Assembler : public MacroAssembler Lsr(w1, regalloc.MapRegister(op.rs1), 26); else { - Ldr(w0, sh4_context_mem_operand(op.rs1.reg_ptr())); + Ldr(w0, sh4_context_mem_operand(op.rs1._reg)); Lsr(w1, w0, 26); } Cmp(w1, 0x38); @@ -905,17 +905,17 @@ class Arm64Assembler : public MacroAssembler else { Ldr(x2, MemOperand(x1)); - Str(x2, sh4_context_mem_operand(op.rd.reg_ptr())); + Str(x2, sh4_context_mem_operand(op.rd._reg)); } break; /* fall back to the canonical implementations for better precision case shop_fipr: - Add(x9, x28, sh4_context_mem_operand(op.rs1.reg_ptr()).GetOffset()); + Add(x9, x28, op.rs1.reg_offset()); Ld1(v0.V4S(), MemOperand(x9)); if (op.rs1._reg != op.rs2._reg) { - Add(x9, x28, sh4_context_mem_operand(op.rs2.reg_ptr()).GetOffset()); + Add(x9, x28, op.rs2.reg_offset()); Ld1(v1.V4S(), MemOperand(x9)); Fmul(v0.V4S(), v0.V4S(), v1.V4S()); } @@ -926,9 +926,9 @@ class Arm64Assembler : public MacroAssembler break; case shop_ftrv: - Add(x9, x28, sh4_context_mem_operand(op.rs1.reg_ptr()).GetOffset()); + Add(x9, x28, op.rs1.reg_offset()); Ld1(v0.V4S(), MemOperand(x9)); - Add(x9, x28, sh4_context_mem_operand(op.rs2.reg_ptr()).GetOffset()); + Add(x9, x28, op.rs2.reg_offset()); Ld1(v1.V4S(), MemOperand(x9, 16, PostIndex)); Ld1(v2.V4S(), MemOperand(x9, 16, PostIndex)); Ld1(v3.V4S(), MemOperand(x9, 16, PostIndex)); @@ -937,14 +937,14 @@ class Arm64Assembler : public MacroAssembler Fmla(v5.V4S(), v2.V4S(), s0, 1); Fmla(v5.V4S(), v3.V4S(), s0, 2); Fmla(v5.V4S(), v4.V4S(), s0, 3); - Add(x9, x28, sh4_context_mem_operand(op.rd.reg_ptr()).GetOffset()); + Add(x9, x28, op.rd.reg_offset()); St1(v5.V4S(), MemOperand(x9)); break; */ case shop_frswap: - Add(x9, x28, sh4_context_mem_operand(op.rs1.reg_ptr()).GetOffset()); - Add(x10, x28, sh4_context_mem_operand(op.rd.reg_ptr()).GetOffset()); + Add(x9, x28, op.rs1.reg_offset()); + Add(x10, x28, op.rd.reg_offset()); Ld4(v0.V2D(), v1.V2D(), v2.V2D(), v3.V2D(), MemOperand(x9)); Ld4(v4.V2D(), v5.V2D(), v6.V2D(), v7.V2D(), MemOperand(x10)); St4(v4.V2D(), v5.V2D(), v6.V2D(), v7.V2D(), MemOperand(x9)); @@ -1042,7 +1042,7 @@ class Arm64Assembler : public MacroAssembler case CPT_ptr: verify(prm.is_reg()); // push the ptr itself - Mov(*call_regs64[regused++], reinterpret_cast(prm.reg_ptr())); + Mov(*call_regs64[regused++], reinterpret_cast(prm.reg_ptr(sh4ctx))); break; case CPT_sh4ctx: @@ -1064,8 +1064,8 @@ class Arm64Assembler : public MacroAssembler if (ccParam.type == CPT_ptr && prm.count() == 2 && regalloc.IsAllocf(prm) && (op->rd._reg == prm._reg || op->rd2._reg == prm._reg)) { // fsca rd param is a pointer to a 64-bit reg so reload the regs if allocated - Ldr(regalloc.MapVRegister(prm, 0), sh4_context_mem_operand(GetRegPtr(prm._reg))); - Ldr(regalloc.MapVRegister(prm, 1), sh4_context_mem_operand(GetRegPtr(prm._reg + 1))); + Ldr(regalloc.MapVRegister(prm, 0), sh4_context_mem_operand(prm._reg)); + Ldr(regalloc.MapVRegister(prm, 1), sh4_context_mem_operand((Sh4RegType)(prm._reg + 1))); } } } @@ -1076,6 +1076,12 @@ class Arm64Assembler : public MacroAssembler verify((offset & 3) == 0 && offset <= 16380); // FIXME 64-bit regs need multiple of 8 up to 32760 return MemOperand(x28, offset); } + MemOperand sh4_context_mem_operand(Sh4RegType reg) + { + u32 offset = getRegOffset(reg); + verify((offset & 3) == 0 && offset <= 16380); // FIXME 64-bit regs need multiple of 8 up to 32760 + return MemOperand(x28, offset); + } void GenReadMemorySlow(u32 size) { @@ -1785,9 +1791,9 @@ class Arm64Assembler : public MacroAssembler break; } if (op.size == 8) - Str(x1, sh4_context_mem_operand(op.rd.reg_ptr())); + Str(x1, sh4_context_mem_operand(op.rd._reg)); else - Str(w1, sh4_context_mem_operand(op.rd.reg_ptr())); + Str(w1, sh4_context_mem_operand(op.rd._reg)); } } else @@ -1801,14 +1807,14 @@ class Arm64Assembler : public MacroAssembler if (regalloc.IsAllocf(op.rd)) Fmov(regalloc.MapVRegister(op.rd, 0), w0); else - Str(w0, sh4_context_mem_operand(op.rd.reg_ptr())); + Str(w0, sh4_context_mem_operand(op.rd._reg)); Mov(w0, addr + 4); GenCallRuntime((void (*)())ptr); if (regalloc.IsAllocf(op.rd)) Fmov(regalloc.MapVRegister(op.rd, 1), w0); else - Str(w0, sh4_context_mem_operand((u8*)op.rd.reg_ptr() + 4)); + Str(w0, sh4_context_mem_operand((Sh4RegType)(op.rd._reg + 1))); } else { @@ -2100,14 +2106,14 @@ class Arm64Assembler : public MacroAssembler { if (param.is_r64f() && !regalloc.IsAllocf(param)) { - Ldr(reg, sh4_context_mem_operand(param.reg_ptr())); + Ldr(reg, sh4_context_mem_operand(param._reg)); } else if (param.is_r32f() || param.is_r64f()) { if (regalloc.IsAllocf(param)) Fmov(reg.W(), regalloc.MapVRegister(param, 0)); else - Ldr(reg.W(), sh4_context_mem_operand(param.reg_ptr())); + Ldr(reg.W(), sh4_context_mem_operand(param._reg)); if (param.is_r64f()) { Fmov(w15, regalloc.MapVRegister(param, 1)); @@ -2119,7 +2125,7 @@ class Arm64Assembler : public MacroAssembler if (regalloc.IsAllocg(param)) Mov(reg.W(), regalloc.MapRegister(param)); else - Ldr(reg.W(), sh4_context_mem_operand(param.reg_ptr())); + Ldr(reg.W(), sh4_context_mem_operand(param._reg)); } } else @@ -2141,7 +2147,7 @@ class Arm64Assembler : public MacroAssembler } else { - Str((const Register&)reg, sh4_context_mem_operand(param.reg_ptr())); + Str((const Register&)reg, sh4_context_mem_operand(param._reg)); } } else if (regalloc.IsAllocg(param)) @@ -2160,7 +2166,7 @@ class Arm64Assembler : public MacroAssembler } else { - Str(reg, sh4_context_mem_operand(param.reg_ptr())); + Str(reg, sh4_context_mem_operand(param._reg)); } } @@ -2389,18 +2395,18 @@ u32 DynaRBI::Relink() void Arm64RegAlloc::Preload(u32 reg, eReg nreg) { - assembler->Ldr(Register(nreg, 32), assembler->sh4_context_mem_operand(GetRegPtr(reg))); + assembler->Ldr(Register(nreg, 32), assembler->sh4_context_mem_operand((Sh4RegType)reg)); } void Arm64RegAlloc::Writeback(u32 reg, eReg nreg) { - assembler->Str(Register(nreg, 32), assembler->sh4_context_mem_operand(GetRegPtr(reg))); + assembler->Str(Register(nreg, 32), assembler->sh4_context_mem_operand((Sh4RegType)reg)); } void Arm64RegAlloc::Preload_FPU(u32 reg, eFReg nreg) { - assembler->Ldr(VRegister(nreg, 32), assembler->sh4_context_mem_operand(GetRegPtr(reg))); + assembler->Ldr(VRegister(nreg, 32), assembler->sh4_context_mem_operand((Sh4RegType)reg)); } void Arm64RegAlloc::Writeback_FPU(u32 reg, eFReg nreg) { - assembler->Str(VRegister(nreg, 32), assembler->sh4_context_mem_operand(GetRegPtr(reg))); + assembler->Str(VRegister(nreg, 32), assembler->sh4_context_mem_operand((Sh4RegType)reg)); } #endif // FEAT_SHREC == DYNAREC_JIT diff --git a/core/rec-x64/rec_x64.cpp b/core/rec-x64/rec_x64.cpp index 2941f1982..08e5df24e 100644 --- a/core/rec-x64/rec_x64.cpp +++ b/core/rec-x64/rec_x64.cpp @@ -64,7 +64,7 @@ static void ngen_blockcheckfail(u32 pc) { rdv_BlockCheckFail(pc); } -static void handle_sh4_exception(SH4ThrownException& ex, u32 pc) +static void handle_sh4_exception(Sh4Context *ctx, SH4ThrownException& ex, u32 pc) { if (pc & 1) { @@ -73,7 +73,7 @@ static void handle_sh4_exception(SH4ThrownException& ex, u32 pc) pc--; } Do_Exception(pc, ex.expEvn); - p_sh4rcb->cntx.cycle_counter += 4; // probably more is needed + ctx->cycle_counter += 4; // probably more is needed handleException(); } @@ -82,7 +82,7 @@ static void interpreter_fallback(Sh4Context *ctx, u16 op, OpCallFP *oph, u32 pc) try { oph(ctx, op); } catch (SH4ThrownException& ex) { - handle_sh4_exception(ex, pc); + handle_sh4_exception(ctx, ex, pc); } } @@ -91,7 +91,7 @@ static void do_sqw_mmu_no_ex(u32 addr, Sh4Context *ctx, u32 pc) try { ctx->doSqWrite(addr, ctx); } catch (SH4ThrownException& ex) { - handle_sh4_exception(ex, pc); + handle_sh4_exception(ctx, ex, pc); } } @@ -187,9 +187,9 @@ class BlockCompiler : public BaseXbyakRec verify(op.rs1.is_r64f()); #if ALLOC_F64 == false - mov(rax, (uintptr_t)op.rs1.reg_ptr()); + mov(rax, (uintptr_t)op.rs1.reg_ptr(sh4ctx)); mov(rax, qword[rax]); - mov(rcx, (uintptr_t)op.rd.reg_ptr()); + mov(rcx, (uintptr_t)op.rd.reg_ptr(sh4ctx)); mov(qword[rcx], rax); #else Xbyak::Xmm rd0 = regalloc.MapXRegister(op.rd, 0); @@ -226,7 +226,7 @@ class BlockCompiler : public BaseXbyakRec add(call_regs[0], regalloc.MapRegister(op.rs3)); else { - mov(rax, (uintptr_t)op.rs3.reg_ptr()); + mov(rax, (uintptr_t)op.rs3.reg_ptr(sh4ctx)); add(call_regs[0], dword[rax]); } } @@ -238,7 +238,7 @@ class BlockCompiler : public BaseXbyakRec #if ALLOC_F64 == false if (size == MemSize::S64) { - mov(rcx, (uintptr_t)op.rd.reg_ptr()); + mov(rcx, (uintptr_t)op.rd.reg_ptr(sh4ctx)); mov(qword[rcx], rax); } else @@ -263,7 +263,7 @@ class BlockCompiler : public BaseXbyakRec add(call_regs[0], regalloc.MapRegister(op.rs3)); else { - mov(rax, (uintptr_t)op.rs3.reg_ptr()); + mov(rax, (uintptr_t)op.rs3.reg_ptr(sh4ctx)); add(call_regs[0], dword[rax]); } } @@ -272,7 +272,7 @@ class BlockCompiler : public BaseXbyakRec #if ALLOC_F64 == false if (op.size == 8) { - mov(rax, (uintptr_t)op.rs2.reg_ptr()); + mov(rax, (uintptr_t)op.rs2.reg_ptr(sh4ctx)); mov(call_regs64[1], qword[rax]); } else @@ -361,7 +361,7 @@ class BlockCompiler : public BaseXbyakRec } else { - mov(rax, (uintptr_t)op.rs1.reg_ptr()); + mov(rax, (uintptr_t)op.rs1.reg_ptr(sh4ctx)); mov(eax, dword[rax]); rn = eax; } @@ -390,8 +390,8 @@ class BlockCompiler : public BaseXbyakRec break; case shop_frswap: - mov(rax, (uintptr_t)op.rs1.reg_ptr()); - mov(rcx, (uintptr_t)op.rd.reg_ptr()); + mov(rax, (uintptr_t)op.rs1.reg_ptr(sh4ctx)); + mov(rcx, (uintptr_t)op.rd.reg_ptr(sh4ctx)); if (cpu.has(Cpu::tAVX512F)) { vmovaps(zmm0, zword[rax]); @@ -608,7 +608,7 @@ class BlockCompiler : public BaseXbyakRec //push the ptr itself case CPT_ptr: verify(prm.is_reg()); - mov(call_regs64[regused++], (size_t)prm.reg_ptr()); + mov(call_regs64[regused++], (size_t)prm.reg_ptr(sh4ctx)); break; case CPT_sh4ctx: @@ -627,9 +627,9 @@ class BlockCompiler : public BaseXbyakRec const shil_param& prm = *ccParam.prm; if (ccParam.type == CPT_ptr && prm.count() == 2 && regalloc.IsAllocf(prm) && (op.rd._reg == prm._reg || op.rd2._reg == prm._reg)) { // fsca rd param is a pointer to a 64-bit reg so reload the regs if allocated - mov(rax, (size_t)GetRegPtr(prm._reg)); + mov(rax, (size_t)GetRegPtr(sh4ctx, prm._reg)); movss(regalloc.MapXRegister(prm, 0), dword[rax]); - mov(rax, (size_t)GetRegPtr(prm._reg + 1)); + mov(rax, (size_t)GetRegPtr(sh4ctx, prm._reg + 1)); movss(regalloc.MapXRegister(prm, 1), dword[rax]); } } @@ -638,22 +638,22 @@ class BlockCompiler : public BaseXbyakRec void RegPreload(u32 reg, Xbyak::Operand::Code nreg) { - mov(rax, (size_t)GetRegPtr(reg)); + mov(rax, (size_t)GetRegPtr(sh4ctx, reg)); mov(Xbyak::Reg32(nreg), dword[rax]); } void RegWriteback(u32 reg, Xbyak::Operand::Code nreg) { - mov(rax, (size_t)GetRegPtr(reg)); + mov(rax, (size_t)GetRegPtr(sh4ctx, reg)); mov(dword[rax], Xbyak::Reg32(nreg)); } void RegPreload_FPU(u32 reg, s8 nreg) { - mov(rax, (size_t)GetRegPtr(reg)); + mov(rax, (size_t)GetRegPtr(sh4ctx, reg)); movss(Xbyak::Xmm(nreg), dword[rax]); } void RegWriteback_FPU(u32 reg, s8 nreg) { - mov(rax, (size_t)GetRegPtr(reg)); + mov(rax, (size_t)GetRegPtr(sh4ctx, reg)); movss(dword[rax], Xbyak::Xmm(nreg)); } @@ -867,7 +867,7 @@ class BlockCompiler : public BaseXbyakRec else { movsx(eax, byte[rax]); - mov(rcx, (uintptr_t)op.rd.reg_ptr()); + mov(rcx, (uintptr_t)op.rd.reg_ptr(sh4ctx)); mov(dword[rcx], eax); } break; @@ -878,7 +878,7 @@ class BlockCompiler : public BaseXbyakRec else { movsx(eax, word[rax]); - mov(rcx, (uintptr_t)op.rd.reg_ptr()); + mov(rcx, (uintptr_t)op.rd.reg_ptr(sh4ctx)); mov(dword[rcx], eax); } break; @@ -891,7 +891,7 @@ class BlockCompiler : public BaseXbyakRec else { mov(eax, dword[rax]); - mov(rcx, (uintptr_t)op.rd.reg_ptr()); + mov(rcx, (uintptr_t)op.rd.reg_ptr(sh4ctx)); mov(dword[rcx], eax); } break; @@ -899,7 +899,7 @@ class BlockCompiler : public BaseXbyakRec case 8: #if ALLOC_F64 == false mov(rcx, qword[rax]); - mov(rax, (uintptr_t)op.rd.reg_ptr()); + mov(rax, (uintptr_t)op.rd.reg_ptr(sh4ctx)); mov(qword[rax], rcx); #else movd(regalloc.MapXRegister(op.rd, 0), dword[rax]); @@ -921,7 +921,7 @@ class BlockCompiler : public BaseXbyakRec mov(call_regs[0], addr); GenCall((void (*)())ptr); #if ALLOC_F64 == false - mov(rcx, (size_t)op.rd.reg_ptr()); + mov(rcx, (size_t)op.rd.reg_ptr(sh4ctx)); mov(dword[rcx], eax); #else movd(regalloc.MapXRegister(op.rd, 0), eax); @@ -930,7 +930,7 @@ class BlockCompiler : public BaseXbyakRec mov(call_regs[0], addr + 4); GenCall((void (*)())ptr); #if ALLOC_F64 == false - mov(rcx, (size_t)op.rd.reg_ptr() + 4); + mov(rcx, (size_t)op.rd.reg_ptr(sh4ctx) + 4); mov(dword[rcx], eax); #else movd(regalloc.MapXRegister(op.rd, 1), eax); @@ -990,7 +990,7 @@ class BlockCompiler : public BaseXbyakRec mov(byte[rax], (u8)op.rs2._imm); else { - mov(rcx, (uintptr_t)op.rs2.reg_ptr()); + mov(rcx, (uintptr_t)op.rs2.reg_ptr(sh4ctx)); mov(cl, byte[rcx]); mov(byte[rax], cl); } @@ -1003,7 +1003,7 @@ class BlockCompiler : public BaseXbyakRec mov(word[rax], (u16)op.rs2._imm); else { - mov(rcx, (uintptr_t)op.rs2.reg_ptr()); + mov(rcx, (uintptr_t)op.rs2.reg_ptr(sh4ctx)); mov(cx, word[rcx]); mov(word[rax], cx); } @@ -1018,7 +1018,7 @@ class BlockCompiler : public BaseXbyakRec mov(dword[rax], op.rs2._imm); else { - mov(rcx, (uintptr_t)op.rs2.reg_ptr()); + mov(rcx, (uintptr_t)op.rs2.reg_ptr(sh4ctx)); mov(ecx, dword[rcx]); mov(dword[rax], ecx); } @@ -1026,7 +1026,7 @@ class BlockCompiler : public BaseXbyakRec case 8: #if ALLOC_F64 == false - mov(rcx, (uintptr_t)op.rs2.reg_ptr()); + mov(rcx, (uintptr_t)op.rs2.reg_ptr(sh4ctx)); mov(rcx, qword[rcx]); mov(qword[rax], rcx); #else diff --git a/core/rec-x64/xbyak_base.h b/core/rec-x64/xbyak_base.h index 1fd69bf4a..cdbc01ad7 100644 --- a/core/rec-x64/xbyak_base.h +++ b/core/rec-x64/xbyak_base.h @@ -583,7 +583,7 @@ class BaseXbyakRec : public Xbyak::CodeGenerator mov(rcx, (uintptr_t)&sin_table); mov(rcx, qword[rcx + rax * 8]); #if ALLOC_F64 == false - mov(rdx, (uintptr_t)op.rd.reg_ptr()); + mov(rdx, (uintptr_t)op.rd.reg_ptr(sh4ctx)); mov(qword[rdx], rcx); #else movd(mapXRegister(op.rd, 0), ecx); @@ -601,8 +601,8 @@ class BaseXbyakRec : public Xbyak::CodeGenerator verify(!isAllocAny(op.rd)); mov(ecx, dword[(size_t)&sin_table + eax * 8]); mov(edx, dword[(size_t)&sin_table[0].u[1] + eax * 8]); - mov(dword[op.rd.reg_ptr()], ecx); - mov(dword[op.rd.reg_ptr() + 1], edx); + mov(dword[op.rd.reg_ptr(sh4ctx)], ecx); + mov(dword[op.rd.reg_ptr(sh4ctx) + 1], edx); #endif } break; @@ -680,13 +680,13 @@ class BaseXbyakRec : public Xbyak::CodeGenerator if (ArchX64) { #ifndef XBYAK32 - mov(rax, (size_t)param.reg_ptr()); + mov(rax, (size_t)param.reg_ptr(sh4ctx)); mov(reg.cvt32(), dword[rax]); #endif } else { - mov(reg.cvt32(), dword[param.reg_ptr()]); + mov(reg.cvt32(), dword[param.reg_ptr(sh4ctx)]); } } } @@ -703,7 +703,7 @@ class BaseXbyakRec : public Xbyak::CodeGenerator if (ArchX64) { #ifndef XBYAK32 - mov(rax, (size_t)param.reg_ptr()); + mov(rax, (size_t)param.reg_ptr(sh4ctx)); if (!reg.isXMM()) mov(reg.cvt32(), dword[rax]); else @@ -713,9 +713,9 @@ class BaseXbyakRec : public Xbyak::CodeGenerator else { if (!reg.isXMM()) - mov(reg.cvt32(), dword[param.reg_ptr()]); + mov(reg.cvt32(), dword[param.reg_ptr(sh4ctx)]); else - movss((const Xbyak::Xmm &)reg, dword[param.reg_ptr()]); + movss((const Xbyak::Xmm &)reg, dword[param.reg_ptr(sh4ctx)]); } } } @@ -757,7 +757,7 @@ class BaseXbyakRec : public Xbyak::CodeGenerator if (ArchX64) { #ifndef XBYAK32 - mov(rax, (size_t)param.reg_ptr()); + mov(rax, (size_t)param.reg_ptr(sh4ctx)); if (!reg.isXMM()) mov(dword[rax], reg.cvt32()); else @@ -767,9 +767,9 @@ class BaseXbyakRec : public Xbyak::CodeGenerator else { if (!reg.isXMM()) - mov(dword[param.reg_ptr()], reg.cvt32()); + mov(dword[param.reg_ptr(sh4ctx)], reg.cvt32()); else - movss(dword[param.reg_ptr()], (const Xbyak::Xmm &)reg); + movss(dword[param.reg_ptr(sh4ctx)], (const Xbyak::Xmm &)reg); } } } diff --git a/core/rec-x86/rec_x86.cpp b/core/rec-x86/rec_x86.cpp index bb23a11c2..6d422fc89 100644 --- a/core/rec-x86/rec_x86.cpp +++ b/core/rec-x86/rec_x86.cpp @@ -182,7 +182,7 @@ u32 X86Compiler::relinkBlock(RuntimeBlockInfo* block) mov(ecx, block->NextBlock); - cmp(dword[GetRegPtr(block->has_jcond ? reg_pc_dyn : reg_sr_T)], (u32)block->BlockType & 1); + cmp(dword[block->has_jcond ? &sh4ctx.jdyn : &sh4ctx.sr.T], (u32)block->BlockType & 1); Xbyak::Label branch_not_taken; jne(branch_not_taken, T_SHORT); @@ -195,7 +195,7 @@ u32 X86Compiler::relinkBlock(RuntimeBlockInfo* block) case BET_DynamicCall: case BET_DynamicRet: //next_pc = *jdyn; - mov(ecx, dword[GetRegPtr(reg_pc_dyn)]); + mov(ecx, dword[&sh4ctx.jdyn]); break; case BET_DynamicIntr: @@ -203,16 +203,16 @@ u32 X86Compiler::relinkBlock(RuntimeBlockInfo* block) if (block->BlockType == BET_DynamicIntr) { //next_pc = *jdyn; - mov(ecx, dword[GetRegPtr(reg_pc_dyn)]); - mov(dword[&next_pc], ecx); + mov(ecx, dword[&sh4ctx.jdyn]); + mov(dword[&sh4ctx.pc], ecx); } else { //next_pc = next_pc_value; - mov(dword[&next_pc], block->NextBlock); + mov(dword[&sh4ctx.pc], block->NextBlock); } call(UpdateINTC); - mov(ecx, dword[&next_pc]); + mov(ecx, dword[&sh4ctx.pc]); break; default: @@ -227,7 +227,7 @@ u32 X86Compiler::relinkBlock(RuntimeBlockInfo* block) { case BET_Cond_0: case BET_Cond_1: - cmp(dword[GetRegPtr(block->has_jcond ? reg_pc_dyn : reg_sr_T)], (u32)block->BlockType & 1); + cmp(dword[block->has_jcond ? &sh4ctx.jdyn : &sh4ctx.sr.T], (u32)block->BlockType & 1); if (mmu_enabled()) { @@ -269,7 +269,7 @@ u32 X86Compiler::relinkBlock(RuntimeBlockInfo* block) case BET_DynamicRet: case BET_DynamicCall: case BET_DynamicJump: - mov(ecx, dword[GetRegPtr(reg_pc_dyn)]); + mov(ecx, dword[&sh4ctx.jdyn]); jmp((const void *)no_update); break; @@ -303,7 +303,7 @@ u32 X86Compiler::relinkBlock(RuntimeBlockInfo* block) } else { - mov(eax, dword[GetRegPtr(reg_pc_dyn)]); + mov(eax, dword[&sh4ctx.jdyn]); mov(dword[&sh4ctx.pc], eax); } call(UpdateINTC); @@ -360,7 +360,7 @@ void X86Compiler::ngen_CC_param(const shil_opcode& op, const shil_param& param, //push the ptr itself case CPT_ptr: verify(param.is_reg()); - push((uintptr_t)param.reg_ptr()); + push((uintptr_t)param.reg_ptr(sh4ctx)); CC_stackSize += 4; unwinder.allocStackPtr(getCurr(), 4); break; @@ -384,8 +384,8 @@ void X86Compiler::ngen_CC_param(const shil_opcode& op, const shil_param& param, // store from ST(0) case CPT_f32rv: - fstp(dword[param.reg_ptr()]); - movss(regalloc.MapXRegister(param), dword[param.reg_ptr()]); + fstp(dword[param.reg_ptr(sh4ctx)]); + movss(regalloc.MapXRegister(param), dword[param.reg_ptr(sh4ctx)]); break; } } @@ -460,7 +460,7 @@ void X86Compiler::genMainloop() if (!mmu_enabled()) { mov(esi, ecx); // save sh4 pc in ESI, used below if FPCB is still empty for this address - mov(eax, (size_t)&p_sh4rcb->fpcb[0]); + mov(eax, (uintptr_t)&sh4ctx + sizeof(Sh4Context) - sizeof(Sh4RCB) + offsetof(Sh4RCB, fpcb)); // address of fpcb[0] and_(ecx, RAM_SIZE_MAX - 2); jmp(dword[eax + ecx * 2]); } @@ -627,7 +627,7 @@ bool X86Compiler::genReadMemImmediate(const shil_opcode& op, RuntimeBlockInfo* b else { movsx(eax, byte[ptr]); - mov(dword[op.rd.reg_ptr()], eax); + mov(dword[op.rd.reg_ptr(sh4ctx)], eax); } break; @@ -637,7 +637,7 @@ bool X86Compiler::genReadMemImmediate(const shil_opcode& op, RuntimeBlockInfo* b else { movsx(eax, word[ptr]); - mov(dword[op.rd.reg_ptr()], eax); + mov(dword[op.rd.reg_ptr(sh4ctx)], eax); } break; @@ -649,7 +649,7 @@ bool X86Compiler::genReadMemImmediate(const shil_opcode& op, RuntimeBlockInfo* b else { mov(eax, dword[ptr]); - mov(dword[op.rd.reg_ptr()], eax); + mov(dword[op.rd.reg_ptr(sh4ctx)], eax); } break; @@ -662,7 +662,7 @@ bool X86Compiler::genReadMemImmediate(const shil_opcode& op, RuntimeBlockInfo* b else { movq(xmm0, qword[ptr]); - movq(qword[op.rd.reg_ptr()], xmm0); + movq(qword[op.rd.reg_ptr(sh4ctx)], xmm0); } break; @@ -681,11 +681,11 @@ bool X86Compiler::genReadMemImmediate(const shil_opcode& op, RuntimeBlockInfo* b // Need to call the handler twice mov(ecx, addr); genCall((void (DYNACALL *)())ptr); - mov(dword[op.rd.reg_ptr()], eax); + mov(dword[op.rd.reg_ptr(sh4ctx)], eax); mov(ecx, addr + 4); genCall((void (DYNACALL *)())ptr); - mov(dword[op.rd.reg_ptr() + 1], eax); + mov(dword[op.rd.reg_ptr(sh4ctx) + 1], eax); } else { @@ -749,7 +749,7 @@ bool X86Compiler::genWriteMemImmediate(const shil_opcode& op, RuntimeBlockInfo* mov(byte[ptr], (u8)op.rs2.imm_value()); else { - mov(al, byte[op.rs2.reg_ptr()]); + mov(al, byte[op.rs2.reg_ptr(sh4ctx)]); mov(byte[ptr], al); } break; @@ -761,7 +761,7 @@ bool X86Compiler::genWriteMemImmediate(const shil_opcode& op, RuntimeBlockInfo* mov(word[ptr], (u16)op.rs2.imm_value()); else { - mov(cx, word[op.rs2.reg_ptr()]); + mov(cx, word[op.rs2.reg_ptr(sh4ctx)]); mov(word[ptr], cx); } break; @@ -775,7 +775,7 @@ bool X86Compiler::genWriteMemImmediate(const shil_opcode& op, RuntimeBlockInfo* mov(dword[ptr], op.rs2.imm_value()); else { - mov(ecx, dword[op.rs2.reg_ptr()]); + mov(ecx, dword[op.rs2.reg_ptr(sh4ctx)]); mov(dword[ptr], ecx); } break; @@ -788,7 +788,7 @@ bool X86Compiler::genWriteMemImmediate(const shil_opcode& op, RuntimeBlockInfo* } else { - movq(xmm0, qword[op.rs2.reg_ptr()]); + movq(xmm0, qword[op.rs2.reg_ptr(sh4ctx)]); movq(qword[ptr], xmm0); } break; diff --git a/core/rec-x86/rec_x86.h b/core/rec-x86/rec_x86.h index ff9133f99..cfd41096b 100644 --- a/core/rec-x86/rec_x86.h +++ b/core/rec-x86/rec_x86.h @@ -50,22 +50,22 @@ class X86Compiler : public BaseXbyakRec void regPreload(u32 reg, Xbyak::Operand::Code nreg) { DEBUG_LOG(DYNAREC, "RegPreload reg %d -> %s", reg, Xbyak::Reg32(nreg).toString()); - mov(Xbyak::Reg32(nreg), dword[GetRegPtr(reg)]); + mov(Xbyak::Reg32(nreg), dword[GetRegPtr(sh4ctx, reg)]); } void regWriteback(u32 reg, Xbyak::Operand::Code nreg) { DEBUG_LOG(DYNAREC, "RegWriteback reg %d <- %s", reg, Xbyak::Reg32(nreg).toString()); - mov(dword[GetRegPtr(reg)], Xbyak::Reg32(nreg)); + mov(dword[GetRegPtr(sh4ctx, reg)], Xbyak::Reg32(nreg)); } void regPreload_FPU(u32 reg, s8 nreg) { DEBUG_LOG(DYNAREC, "RegPreload_FPU reg %d -> xmm%d", reg, nreg); - movss(Xbyak::Xmm(nreg), dword[GetRegPtr(reg)]); + movss(Xbyak::Xmm(nreg), dword[GetRegPtr(sh4ctx, reg)]); } void regWriteback_FPU(u32 reg, s8 nreg) { DEBUG_LOG(DYNAREC, "RegWriteback_FPU reg %d <- xmm%d", reg, nreg); - movss(dword[GetRegPtr(reg)], Xbyak::Xmm(nreg)); + movss(dword[GetRegPtr(sh4ctx, reg)], Xbyak::Xmm(nreg)); } void genMainloop(); diff --git a/core/rec-x86/x86_ops.cpp b/core/rec-x86/x86_ops.cpp index c7c14c8ae..e539389fd 100644 --- a/core/rec-x86/x86_ops.cpp +++ b/core/rec-x86/x86_ops.cpp @@ -283,7 +283,7 @@ void X86Compiler::genMmuLookup(RuntimeBlockInfo* block, const shil_opcode& op, u } [[noreturn]] -static void DYNACALL handle_sh4_exception(SH4ThrownException& ex, u32 pc) +static void DYNACALL handle_sh4_exception(Sh4Context *ctx, SH4ThrownException& ex, u32 pc) { if (pc & 1) { @@ -292,7 +292,7 @@ static void DYNACALL handle_sh4_exception(SH4ThrownException& ex, u32 pc) pc--; } Do_Exception(pc, ex.expEvn); - p_sh4rcb->cntx.cycle_counter += 4; // probably more is needed + ctx->cycle_counter += 4; // probably more is needed X86Compiler::handleException(); // not reached std::abort(); @@ -303,7 +303,7 @@ static void DYNACALL interpreter_fallback(Sh4Context *ctx, u16 op, OpCallFP *oph try { oph(ctx, op); } catch (SH4ThrownException& ex) { - handle_sh4_exception(ex, pc); + handle_sh4_exception(ctx, ex, pc); } } @@ -312,7 +312,7 @@ static void DYNACALL do_sqw_mmu_no_ex(u32 addr, Sh4Context *ctx, u32 pc) try { ctx->doSqWrite(addr, ctx); } catch (SH4ThrownException& ex) { - handle_sh4_exception(ex, pc); + handle_sh4_exception(ctx, ex, pc); } } @@ -346,8 +346,8 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& movss(regalloc.MapXRegister(op.rd, 1), regalloc.MapXRegister(op.rs1, 1)); #else verify(!regalloc.IsAllocAny(op.rd)); - movq(xmm0, qword[op.rs1.reg_ptr()]); - movq(qword[op.rd.reg_ptr()], xmm0); + movq(xmm0, qword[op.rs1.reg_ptr(sh4ctx)]); + movq(qword[op.rd.reg_ptr(sh4ctx)], xmm0); #endif break; @@ -363,7 +363,7 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& else if (regalloc.IsAllocg(op.rs3)) add(ecx, regalloc.MapRegister(op.rs3)); else - add(ecx, dword[op.rs3.reg_ptr()]); + add(ecx, dword[op.rs3.reg_ptr(sh4ctx)]); } int memOpSize; @@ -410,8 +410,8 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& else { verify(!regalloc.IsAllocAny(op.rd)); - movss(dword[op.rd.reg_ptr()], xmm0); - movss(dword[op.rd.reg_ptr() + 1], xmm1); + movss(dword[op.rd.reg_ptr(sh4ctx)], xmm0); + movss(dword[op.rd.reg_ptr(sh4ctx) + 1], xmm1); } } } @@ -428,7 +428,7 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& else if (regalloc.IsAllocg(op.rs3)) add(ecx, regalloc.MapRegister(op.rs3)); else - add(ecx, dword[op.rs3.reg_ptr()]); + add(ecx, dword[op.rs3.reg_ptr(sh4ctx)]); } int memOpSize; @@ -464,8 +464,8 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& } else { - movd(xmm0, dword[op.rs2.reg_ptr()]); - movd(xmm1, dword[op.rs2.reg_ptr() + 1]); + movd(xmm0, dword[op.rs2.reg_ptr(sh4ctx)]); + movd(xmm1, dword[op.rs2.reg_ptr(sh4ctx) + 1]); } } const u8 *start = getCurr(); @@ -511,7 +511,7 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& } else { - mov(eax, dword[op.rs1.reg_ptr()]); + mov(eax, dword[op.rs1.reg_ptr(sh4ctx)]); rn = eax; } mov(ecx, rn); @@ -521,7 +521,7 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& mov(ecx, rn); } - mov(edx, (size_t)&sh4rcb.cntx); + mov(edx, (uintptr_t)&sh4ctx); if (mmu_enabled()) { push(block->vaddr + op.guest_offs - (op.delay_slot ? 1 : 0)); // pc @@ -549,8 +549,8 @@ void X86Compiler::genOpcode(RuntimeBlockInfo* block, bool optimise, shil_opcode& break; case shop_frswap: - mov(eax, (uintptr_t)op.rs1.reg_ptr()); - mov(ecx, (uintptr_t)op.rd.reg_ptr()); + mov(eax, (uintptr_t)op.rs1.reg_ptr(sh4ctx)); + mov(ecx, (uintptr_t)op.rd.reg_ptr(sh4ctx)); for (int i = 0; i < 4; i++) { movaps(xmm0, xword[eax + (i * 16)]); From 76638df001dcdfd7b475ce7cab80ee22f1a4822c Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 13 Nov 2024 10:31:45 +0100 Subject: [PATCH 21/81] sh4: split xffr array. Fix FPCB_PAD on windows Split xffr[32] into xf[16] and fr[16]. Set FPCB_PAD size to 64_KB. Issue #1736 Get rid of shil_param::reg_aofs --- core/hw/sh4/dyna/shil.cpp | 4 +- core/hw/sh4/dyna/shil.h | 1 - core/hw/sh4/interpr/sh4_fpu.cpp | 116 ++++++++++++++++---------------- core/hw/sh4/sh4_core_regs.cpp | 2 +- core/hw/sh4/sh4_if.h | 38 +++++------ core/hw/sh4/sh4_opcode_list.cpp | 8 +-- core/rec-ARM/rec_arm.cpp | 18 ++--- core/reios/reios.cpp | 2 +- tests/src/sh4_ops.h | 34 ++++++---- 9 files changed, 111 insertions(+), 112 deletions(-) diff --git a/core/hw/sh4/dyna/shil.cpp b/core/hw/sh4/dyna/shil.cpp index 3e3131c99..7428f8926 100644 --- a/core/hw/sh4/dyna/shil.cpp +++ b/core/hw/sh4/dyna/shil.cpp @@ -25,11 +25,11 @@ u32 getRegOffset(Sh4RegType reg) } if (reg >= reg_fr_0 && reg <= reg_fr_15) { const size_t regofs = (reg - reg_fr_0) * sizeof(float); - return offsetof(Sh4Context, xffr[16]) + regofs; + return offsetof(Sh4Context, fr[0]) + regofs; } if (reg >= reg_xf_0 && reg <= reg_xf_15) { const size_t regofs = (reg - reg_xf_0) * sizeof(float); - return offsetof(Sh4Context, xffr[0]) + regofs; + return offsetof(Sh4Context, xf[0]) + regofs; } switch (reg) { diff --git a/core/hw/sh4/dyna/shil.h b/core/hw/sh4/dyna/shil.h index a53cf0adb..787570855 100644 --- a/core/hw/sh4/dyna/shil.h +++ b/core/hw/sh4/dyna/shil.h @@ -234,7 +234,6 @@ struct shil_param u32* reg_ptr(Sh4Context& ctx) const { verify(is_reg()); return GetRegPtr(ctx, _reg); } u32 reg_offset() const { verify(is_reg()); return getRegOffset(_reg); } s32 reg_nofs() const { verify(is_reg()); return (int)getRegOffset(_reg) - sizeof(Sh4Context); } - u32 reg_aofs() const { return -reg_nofs(); } u32 imm_value() const { verify(is_imm()); return _imm; } diff --git a/core/hw/sh4/interpr/sh4_fpu.cpp b/core/hw/sh4/interpr/sh4_fpu.cpp index d5e08b432..231f45fe3 100644 --- a/core/hw/sh4/interpr/sh4_fpu.cpp +++ b/core/hw/sh4/interpr/sh4_fpu.cpp @@ -34,8 +34,8 @@ sh4op(i1111_nnnn_mmmm_0000) { u32 n = GetN(op); u32 m = GetM(op); - ctx->fr(n) += ctx->fr(m); - CHECK_FPU_32(ctx->fr(n)); + ctx->fr[n] += ctx->fr[m]; + CHECK_FPU_32(ctx->fr[n]); } else { @@ -53,8 +53,8 @@ sh4op(i1111_nnnn_mmmm_0001) u32 n = GetN(op); u32 m = GetM(op); - ctx->fr(n) -= ctx->fr(m); - CHECK_FPU_32(ctx->fr(n)); + ctx->fr[n] -= ctx->fr[m]; + CHECK_FPU_32(ctx->fr[n]); } else { @@ -70,8 +70,8 @@ sh4op(i1111_nnnn_mmmm_0010) { u32 n = GetN(op); u32 m = GetM(op); - ctx->fr(n) *= ctx->fr(m); - CHECK_FPU_32(ctx->fr(n)); + ctx->fr[n] *= ctx->fr[m]; + CHECK_FPU_32(ctx->fr[n]); } else { @@ -88,9 +88,9 @@ sh4op(i1111_nnnn_mmmm_0011) u32 n = GetN(op); u32 m = GetM(op); - ctx->fr(n) /= ctx->fr(m); + ctx->fr[n] /= ctx->fr[m]; - CHECK_FPU_32(ctx->fr(n)); + CHECK_FPU_32(ctx->fr[n]); } else { @@ -107,7 +107,7 @@ sh4op(i1111_nnnn_mmmm_0100) u32 n = GetN(op); u32 m = GetM(op); - ctx->sr.T = ctx->fr(m) == ctx->fr(n); + ctx->sr.T = ctx->fr[m] == ctx->fr[n]; } else { @@ -122,7 +122,7 @@ sh4op(i1111_nnnn_mmmm_0101) u32 n = GetN(op); u32 m = GetM(op); - if (ctx->fr(n) > ctx->fr(m)) + if (ctx->fr[n] > ctx->fr[m]) ctx->sr.T = 1; else ctx->sr.T = 0; @@ -281,7 +281,7 @@ sh4op(i1111_nnnn_mmmm_1100) { u32 n = GetN(op); u32 m = GetM(op); - ctx->fr(n) = ctx->fr(m); + ctx->fr[n] = ctx->fr[m]; } else { @@ -339,14 +339,14 @@ sh4op(i1111_nnn0_1111_1101) #ifdef NATIVE_FSCA float rads = pi_index / (65536.0f / 2) * float(M_PI); - ctx->fr(n + 0) = sinf(rads); - ctx->fr(n + 1) = cosf(rads); + ctx->fr[n + 0] = sinf(rads); + ctx->fr[n + 1] = cosf(rads); - CHECK_FPU_32(ctx->fr(n)); - CHECK_FPU_32(ctx->fr(n + 1)); + CHECK_FPU_32(ctx->fr[n]); + CHECK_FPU_32(ctx->fr[n + 1]); #else - ctx->fr(n + 0) = sin_table[pi_index].u[0]; - ctx->fr(n + 1) = sin_table[pi_index].u[1]; + ctx->fr[n + 0] = sin_table[pi_index].u[0]; + ctx->fr[n + 1] = sin_table[pi_index].u[1]; #endif } @@ -360,8 +360,8 @@ sh4op(i1111_nnnn_0111_1101) u32 n = GetN(op); if (ctx->fpscr.PR==0) { - ctx->fr(n) = 1.f / sqrtf(ctx->fr(n)); - CHECK_FPU_32(ctx->fr(n)); + ctx->fr[n] = 1.f / sqrtf(ctx->fr[n]); + CHECK_FPU_32(ctx->fr[n]); } else iNimp("FSRRA : Double precision mode"); @@ -404,12 +404,12 @@ sh4op(i1111_nnmm_1110_1101) int m=(GetN(op)&0x3)<<2; if (ctx->fpscr.PR == 0) { - double idp = (double)ctx->fr(n + 0) * ctx->fr(m + 0); - idp += (double)ctx->fr(n + 1) * ctx->fr(m + 1); - idp += (double)ctx->fr(n + 2) * ctx->fr(m + 2); - idp += (double)ctx->fr(n + 3) * ctx->fr(m + 3); + double idp = (double)ctx->fr[n + 0] * ctx->fr[m + 0]; + idp += (double)ctx->fr[n + 1] * ctx->fr[m + 1]; + idp += (double)ctx->fr[n + 2] * ctx->fr[m + 2]; + idp += (double)ctx->fr[n + 3] * ctx->fr[m + 3]; - ctx->fr(n + 3) = fixNaN((float)idp); + ctx->fr[n + 3] = fixNaN((float)idp); } else { @@ -425,7 +425,7 @@ sh4op(i1111_nnnn_1000_1101) u32 n = GetN(op); - ctx->fr(n) = 0.0f; + ctx->fr[n] = 0.0f; } @@ -437,7 +437,7 @@ sh4op(i1111_nnnn_1001_1101) u32 n = GetN(op); - ctx->fr(n) = 1.0f; + ctx->fr[n] = 1.0f; } //flds ,FPUL @@ -461,7 +461,7 @@ sh4op(i1111_nnnn_0010_1101) if (ctx->fpscr.PR == 0) { u32 n = GetN(op); - ctx->fr(n) = (float)(int)ctx->fpul; + ctx->fr[n] = (float)(int)ctx->fpul; } else { @@ -503,8 +503,8 @@ sh4op(i1111_nnnn_0110_1101) { u32 n = GetN(op); - ctx->fr(n) = sqrtf(ctx->fr(n)); - CHECK_FPU_32(ctx->fr(n)); + ctx->fr[n] = sqrtf(ctx->fr[n]); + CHECK_FPU_32(ctx->fr[n]); } else { @@ -519,14 +519,14 @@ sh4op(i1111_nnnn_0011_1101) if (ctx->fpscr.PR == 0) { u32 n = GetN(op); - ctx->fpul = (u32)(s32)ctx->fr(n); + ctx->fpul = (u32)(s32)ctx->fr[n]; if ((s32)ctx->fpul > 0x7fffff80) ctx->fpul = 0x7fffffff; // Intel CPUs convert out of range float numbers to 0x80000000. Manually set the correct sign - else if (ctx->fpul == 0x80000000 && ctx->fr(n) == ctx->fr(n)) + else if (ctx->fpul == 0x80000000 && ctx->fr[n] == ctx->fr[n]) { - if (*(int *)&ctx->fr(n) > 0) // Using integer math to avoid issues with Inf and NaN + if (*(int *)&ctx->fr[n] > 0) // Using integer math to avoid issues with Inf and NaN ctx->fpul--; } } @@ -554,8 +554,8 @@ sh4op(i1111_nnnn_mmmm_1110) u32 n = GetN(op); u32 m = GetM(op); - ctx->fr(n) = std::fma(ctx->fr(0), ctx->fr(m), ctx->fr(n)); - CHECK_FPU_32(ctx->fr(n)); + ctx->fr[n] = std::fma(ctx->fr[0], ctx->fr[m], ctx->fr[n]); + CHECK_FPU_32(ctx->fr[n]); } else { @@ -578,30 +578,30 @@ sh4op(i1111_nn01_1111_1101) if (ctx->fpscr.PR==0) { - double v1 = (double)ctx->xf(0) * ctx->fr(n + 0) + - (double)ctx->xf(4) * ctx->fr(n + 1) + - (double)ctx->xf(8) * ctx->fr(n + 2) + - (double)ctx->xf(12) * ctx->fr(n + 3); - - double v2 = (double)ctx->xf(1) * ctx->fr(n + 0) + - (double)ctx->xf(5) * ctx->fr(n + 1) + - (double)ctx->xf(9) * ctx->fr(n + 2) + - (double)ctx->xf(13) * ctx->fr(n + 3); - - double v3 = (double)ctx->xf(2) * ctx->fr(n + 0) + - (double)ctx->xf(6) * ctx->fr(n + 1) + - (double)ctx->xf(10) * ctx->fr(n + 2) + - (double)ctx->xf(14) * ctx->fr(n + 3); - - double v4 = (double)ctx->xf(3) * ctx->fr(n + 0) + - (double)ctx->xf(7) * ctx->fr(n + 1) + - (double)ctx->xf(11) * ctx->fr(n + 2) + - (double)ctx->xf(15) * ctx->fr(n + 3); - - ctx->fr(n + 0) = fixNaN((float)v1); - ctx->fr(n + 1) = fixNaN((float)v2); - ctx->fr(n + 2) = fixNaN((float)v3); - ctx->fr(n + 3) = fixNaN((float)v4); + double v1 = (double)ctx->xf[0] * ctx->fr[n + 0] + + (double)ctx->xf[4] * ctx->fr[n + 1] + + (double)ctx->xf[8] * ctx->fr[n + 2] + + (double)ctx->xf[12] * ctx->fr[n + 3]; + + double v2 = (double)ctx->xf[1] * ctx->fr[n + 0] + + (double)ctx->xf[5] * ctx->fr[n + 1] + + (double)ctx->xf[9] * ctx->fr[n + 2] + + (double)ctx->xf[13] * ctx->fr[n + 3]; + + double v3 = (double)ctx->xf[2] * ctx->fr[n + 0] + + (double)ctx->xf[6] * ctx->fr[n + 1] + + (double)ctx->xf[10] * ctx->fr[n + 2] + + (double)ctx->xf[14] * ctx->fr[n + 3]; + + double v4 = (double)ctx->xf[3] * ctx->fr[n + 0] + + (double)ctx->xf[7] * ctx->fr[n + 1] + + (double)ctx->xf[11] * ctx->fr[n + 2] + + (double)ctx->xf[15] * ctx->fr[n + 3]; + + ctx->fr[n + 0] = fixNaN((float)v1); + ctx->fr[n + 1] = fixNaN((float)v2); + ctx->fr[n + 2] = fixNaN((float)v3); + ctx->fr[n + 3] = fixNaN((float)v4); } else { diff --git a/core/hw/sh4/sh4_core_regs.cpp b/core/hw/sh4/sh4_core_regs.cpp index 0ff120f1c..7542f1388 100644 --- a/core/hw/sh4/sh4_core_regs.cpp +++ b/core/hw/sh4/sh4_core_regs.cpp @@ -133,7 +133,7 @@ void DYNACALL Sh4Context::UpdateFPSCR(Sh4Context *ctx) { if (ctx->fpscr.FR != ctx->old_fpscr.FR) // FPU bank change - std::swap((f32 (&)[16])ctx->xffr, *(f32 (*)[16])&ctx->xffr[16]); + std::swap(ctx->xf, ctx->fr); ctx->old_fpscr = ctx->fpscr; setHostRoundingMode(ctx->fpscr.RM, ctx->fpscr.DN); diff --git a/core/hw/sh4/sh4_if.h b/core/hw/sh4/sh4_if.h index 32b5d682c..217e53071 100644 --- a/core/hw/sh4/sh4_if.h +++ b/core/hw/sh4/sh4_if.h @@ -103,7 +103,7 @@ struct fpscr_t class Sh4Executor { public: - virtual ~Sh4Executor() {} + virtual ~Sh4Executor() = default; virtual void Run() = 0; virtual void Start() = 0; virtual void Stop() = 0; @@ -131,7 +131,8 @@ struct alignas(64) Sh4Context { SQBuffer sq_buffer[2]; - f32 xffr[32]; + float xf[16]; + float fr[16]; u32 r[16]; union @@ -170,44 +171,36 @@ struct alignas(64) Sh4Context u64 raw[64]; }; - f32& fr(int idx) { - assert(idx >= 0 && idx <= 15); - return xffr[idx + 16]; - } - f32& xf(int idx) { - assert(idx >= 0 && idx <= 15); - return xffr[idx]; - } u32& fr_hex(int idx) { assert(idx >= 0 && idx <= 15); - return reinterpret_cast(fr(idx)); + return reinterpret_cast(fr[idx]); } u64& dr_hex(int idx) { assert(idx >= 0 && idx <= 7); - return *reinterpret_cast(&fr(idx * 2)); + return *reinterpret_cast(&fr[idx * 2]); } u64& xd_hex(int idx) { assert(idx >= 0 && idx <= 7); - return *reinterpret_cast(&xf(idx * 2)); + return *reinterpret_cast(&xf[idx * 2]); } - f64 getDR(u32 n) + double getDR(u32 n) { assert(n <= 7); DoubleReg t; - t.sgl[1] = fr(n * 2); - t.sgl[0] = fr(n * 2 + 1); + t.sgl[1] = fr[n * 2]; + t.sgl[0] = fr[n * 2 + 1]; return t.dbl; } - void setDR(u32 n, f64 val) + void setDR(u32 n, double val) { assert(n <= 7); DoubleReg t; t.dbl = val; - fr(n * 2) = t.sgl[1]; - fr(n * 2 + 1) = t.sgl[0]; + fr[n * 2] = t.sgl[1]; + fr[n * 2 + 1] = t.sgl[0]; } static void DYNACALL UpdateFPSCR(Sh4Context *ctx); @@ -216,8 +209,8 @@ struct alignas(64) Sh4Context private: union DoubleReg { - f64 dbl; - f32 sgl[2]; + double dbl; + float sgl[2]; }; }; static_assert(sizeof(Sh4Context) == 512, "Invalid Sh4Context size"); @@ -230,7 +223,8 @@ static_assert(sizeof(Sh4Context) == 512, "Invalid Sh4Context size"); // want to be an i8r4 value that can be substracted in one op (such as 0x4100000) #define FPCB_PAD 0x100000 #else -#define FPCB_PAD PAGE_SIZE +// For other systems we could use PAGE_SIZE, except on windows that has a 64 KB granularity for memory mapping +#define FPCB_PAD 64_KB #endif struct alignas(PAGE_SIZE) Sh4RCB { diff --git a/core/hw/sh4/sh4_opcode_list.cpp b/core/hw/sh4/sh4_opcode_list.cpp index 67ac81d2d..654fd6ac6 100644 --- a/core/hw/sh4/sh4_opcode_list.cpp +++ b/core/hw/sh4/sh4_opcode_list.cpp @@ -461,25 +461,25 @@ std::string disassemble_op(const char* tx1, u32 pc, u16 opcode) } else if (strcmp2(tx1,"FREG_N>")) { - sprintf(buf,"FR%d=%f ", GetN(opcode), p_sh4rcb->cntx.xffr[16 + GetN(opcode)]); + sprintf(buf,"FR%d=%f ", GetN(opcode), p_sh4rcb->cntx.fr[GetN(opcode)]); regs += buf; sprintf(buf,"FR%d",GetN(opcode)); } else if (strcmp2(tx1,"FREG_M>")) { - sprintf(buf,"FR%d=%f ", GetM(opcode), p_sh4rcb->cntx.xffr[16 + GetM(opcode)]); + sprintf(buf,"FR%d=%f ", GetM(opcode), p_sh4rcb->cntx.fr[GetM(opcode)]); regs += buf; sprintf(buf,"FR%d",GetM(opcode)); } else if (strcmp2(tx1, "FREG_M_SD_F>")) { - sprintf(buf,"FR%d=%f ", GetM(opcode), p_sh4rcb->cntx.xffr[16 + GetM(opcode)]); + sprintf(buf,"FR%d=%f ", GetM(opcode), p_sh4rcb->cntx.fr[GetM(opcode)]); regs += buf; sprintf(buf,"FR%d", GetM(opcode)); } else if (strcmp2(tx1,"FREG_0>")) { - sprintf(buf,"FR0=%f ", p_sh4rcb->cntx.xffr[16]); + sprintf(buf,"FR0=%f ", p_sh4rcb->cntx.fr[0]); regs += buf; sprintf(buf,"FR0"); } diff --git a/core/rec-ARM/rec_arm.cpp b/core/rec-ARM/rec_arm.cpp index 727d0e5fd..4552a3c5a 100644 --- a/core/rec-ARM/rec_arm.cpp +++ b/core/rec-ARM/rec_arm.cpp @@ -2007,14 +2007,14 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op QRegister _r1 = q0; QRegister _r2 = q0; - Sub(r0, r8, op->rs1.reg_aofs()); - if (op->rs2.reg_aofs() == op->rs1.reg_aofs()) + Sub(r0, r8, -op->rs1.reg_nofs()); + if (op->rs2.reg_nofs() == op->rs1.reg_nofs()) { Vldm(r0, NO_WRITE_BACK, DRegisterList(d0, 2)); } else { - Sub(r1, r8, op->rs2.reg_aofs()); + Sub(r1, r8, -op->rs2.reg_nofs()); Vldm(r0, NO_WRITE_BACK, DRegisterList(d0, 2)); Vldm(r1, NO_WRITE_BACK, DRegisterList(d2, 2)); _r2 = q1; @@ -2039,12 +2039,12 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op case shop_ftrv: { Register rdp = r1; - Sub(r2, r8, op->rs2.reg_aofs()); - Sub(r1, r8, op->rs1.reg_aofs()); - if (op->rs1.reg_aofs() != op->rd.reg_aofs()) + Sub(r2, r8, -op->rs2.reg_nofs()); + Sub(r1, r8, -op->rs1.reg_nofs()); + if (op->rs1.reg_nofs() != op->rd.reg_nofs()) { rdp = r0; - Sub(r0, r8, op->rd.reg_aofs()); + Sub(r0, r8, -op->rd.reg_nofs()); } #if 1 @@ -2100,8 +2100,8 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op break; case shop_frswap: - Sub(r0, r8, op->rs1.reg_aofs()); - Sub(r1, r8, op->rd.reg_aofs()); + Sub(r0, r8, -op->rs1.reg_nofs()); + Sub(r1, r8, -op->rd.reg_nofs()); //Assumes no FPU reg alloc here //frswap touches all FPU regs, so all spans should be clear here .. Vldm(r1, NO_WRITE_BACK, DRegisterList(d0, 8)); diff --git a/core/reios/reios.cpp b/core/reios/reios.cpp index e9a9a5dc9..a7a65a8d9 100644 --- a/core/reios/reios.cpp +++ b/core/reios/reios.cpp @@ -501,7 +501,7 @@ static void reios_setup_naomi(u32 boot_addr) { SR 0x60000000 0x00000001 FPSRC 0x00040001 - - xffr 0x13e1fe40 float [32] + - xf,fr 0x13e1fe40 float [32] [0x0] 1.00000000 float [0x1] 0.000000000 float [0x2] 0.000000000 float diff --git a/tests/src/sh4_ops.h b/tests/src/sh4_ops.h index 25b34fcff..b9d7b2084 100644 --- a/tests/src/sh4_ops.h +++ b/tests/src/sh4_ops.h @@ -35,17 +35,18 @@ class Sh4OpTest : public ::testing::Test { virtual void PrepareOp(u16 op, u16 op2 = 0, u16 op3 = 0) = 0; virtual void RunOp(int numOp = 1) = 0; - void ClearRegs() { - for (int i = 0; i < 16; i++) - r(i) = REG_MAGIC; - for (int i = 0; i < 32; i++) - *(u32 *)&ctx->xffr[i] = REG_MAGIC; + void ClearRegs() + { + std::fill(std::begin(ctx->r), std::end(ctx->r), REG_MAGIC); + std::fill(std::begin(reinterpret_cast(ctx->xf)), std::end(reinterpret_cast(ctx->xf)), REG_MAGIC); + std::fill(std::begin(reinterpret_cast(ctx->fr)), std::end(reinterpret_cast(ctx->fr)), REG_MAGIC); sr().setFull(0x700000F0); mac() = 0; gbr() = REG_MAGIC; checkedRegs.clear(); } - void AssertState() { + void AssertState() + { for (int i = 0; i < 16; i++) if (checkedRegs.count(&ctx->r[i]) == 0) ASSERT_CLEAN_REG(i); @@ -53,10 +54,15 @@ class Sh4OpTest : public ::testing::Test { { ASSERT_EQ(ctx->gbr, REG_MAGIC); } - for (int i = 0; i < 32; i++) - if (checkedRegs.count((u32 *)&ctx->xffr[i]) == 0) + for (int i = 0; i < 16; i++) + if (checkedRegs.count((u32 *)&ctx->xf[i]) == 0) + { + ASSERT_EQ(*(u32 *)&ctx->xf[i], REG_MAGIC); + } + for (int i = 0; i < 16; i++) + if (checkedRegs.count((u32 *)&ctx->fr[i]) == 0) { - ASSERT_EQ(*(u32 *)&ctx->xffr[i], REG_MAGIC); + ASSERT_EQ(*(u32 *)&ctx->fr[i], REG_MAGIC); } } static u16 Rm(int r) { return r << 4; } @@ -70,15 +76,15 @@ class Sh4OpTest : public ::testing::Test { u32& mach() { return ctx->mac.l; } u32& macl() { return ctx->mac.h; } sr_t& sr() { return ctx->sr; } - f32& fr(int regNum) { checkedRegs.insert((u32 *)&ctx->fr(regNum)); return ctx->fr(regNum); } + f32& fr(int regNum) { checkedRegs.insert((u32 *)&ctx->fr[regNum]); return ctx->fr[regNum]; } double getDr(int regNum) { - checkedRegs.insert((u32 *)&ctx->fr(regNum * 2)); - checkedRegs.insert((u32 *)&ctx->fr(regNum * 2 + 1)); + checkedRegs.insert((u32 *)&ctx->fr[regNum * 2]); + checkedRegs.insert((u32 *)&ctx->fr[regNum * 2 + 1]); return ctx->getDR(regNum); } void setDr(int regNum, double d) { - checkedRegs.insert((u32 *)&ctx->fr(regNum * 2)); - checkedRegs.insert((u32 *)&ctx->fr(regNum * 2 + 1)); + checkedRegs.insert((u32 *)&ctx->fr[regNum * 2]); + checkedRegs.insert((u32 *)&ctx->fr[regNum * 2 + 1]); ctx->setDR(regNum, d); } From bdd2d5af7fae9a4aac0be201c00a68de2997c5a9 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 14 Nov 2024 13:05:48 +0100 Subject: [PATCH 22/81] naomi: emulate dimm registers for gd cart Emulate 1.02 dimm board without network so that the bios detects it and shows the expected GD-ROM SYSTEM boot screen. Fix rom board serial id issue (error 04) with ikaruga, tetkiwam, puyofev[j] and sprtjam. Issue #1735 Add naomigd and naomidev bios definitions (unused). Use netdimm for mj1 and wccf* series (still not working). --- core/hw/flashrom/x76f100.h | 5 + core/hw/naomi/gdcartridge.cpp | 239 ++++++++++++++++++++++++++++++++++ core/hw/naomi/gdcartridge.h | 56 ++++++-- core/hw/naomi/naomi.cpp | 9 ++ core/hw/naomi/naomi.h | 5 +- core/hw/naomi/naomi_cart.cpp | 8 +- core/hw/naomi/naomi_roms.cpp | 39 +++++- core/hw/naomi/netdimm.cpp | 118 ++--------------- core/hw/naomi/netdimm.h | 43 +----- core/serialize.h | 3 +- 10 files changed, 362 insertions(+), 163 deletions(-) diff --git a/core/hw/flashrom/x76f100.h b/core/hw/flashrom/x76f100.h index bb8496a02..b27d5ae13 100644 --- a/core/hw/flashrom/x76f100.h +++ b/core/hw/flashrom/x76f100.h @@ -62,6 +62,11 @@ class X76F100SerialFlash memcpy(this->data, &data[index], sizeof(this->data)); } + const u8 *getProtectedData() const + { + return data; + } + void serialize(Serializer& ser) const; void deserialize(Deserializer& deser); diff --git a/core/hw/naomi/gdcartridge.cpp b/core/hw/naomi/gdcartridge.cpp index 5a2b3176a..c3e60ba32 100644 --- a/core/hw/naomi/gdcartridge.cpp +++ b/core/hw/naomi/gdcartridge.cpp @@ -14,6 +14,13 @@ #include "stdclass.h" #include "emulator.h" #include "oslib/storage.h" +#include "naomi_regs.h" +#include "hw/holly/sb.h" +#include "hw/holly/holly_intc.h" +#include "hw/mem/addrspace.h" +#include "serialize.h" +#include "hw/sh4/sh4_sched.h" +#include "naomi.h" /* @@ -642,3 +649,235 @@ bool GDCartridge::Read(u32 offset, u32 size, void *dst) memcpy(dst, &dimm_data[addr], std::min(size, dimm_data_size - addr)); return true; } + +GDCartridge::GDCartridge(u32 size) : NaomiCartridge(size) +{ + schedId = sh4_sched_register(0, [](int tag, int sch_cycl, int jitter, void *arg){ + return ((GDCartridge *)arg)->schedCallback(); + }, this); +} + +GDCartridge::~GDCartridge() +{ + free(dimm_data); + sh4_sched_unregister(schedId); +} + +u32 GDCartridge::ReadMem(u32 address, u32 size) +{ + switch (address) + { + case NAOMI_DIMM_COMMAND: + DEBUG_LOG(NAOMI, "DIMM COMMAND read -> %x", dimm_command); + return dimm_command; + case NAOMI_DIMM_OFFSETL: + DEBUG_LOG(NAOMI, "DIMM OFFSETL read -> %x", dimm_offsetl); + return dimm_offsetl; + case NAOMI_DIMM_PARAMETERL: + DEBUG_LOG(NAOMI, "DIMM PARAMETERL read -> %x", dimm_parameterl); + return dimm_parameterl; + case NAOMI_DIMM_PARAMETERH: + DEBUG_LOG(NAOMI, "DIMM PARAMETERH read -> %x", dimm_parameterh); + return dimm_parameterh; + case NAOMI_DIMM_STATUS: + { + u32 rc = DIMM_STATUS & ~(((SB_ISTEXT >> 3) & 1) << 8); + static u32 lastRc; + if (rc != lastRc) + DEBUG_LOG(NAOMI, "DIMM STATUS read -> %x", rc); + lastRc = rc; + return rc; + } + default: + return NaomiCartridge::ReadMem(address, size); + } +} + +void GDCartridge::WriteMem(u32 address, u32 data, u32 size) +{ + switch (address) + { + case NAOMI_DIMM_COMMAND: + dimm_command = data; + DEBUG_LOG(NAOMI, "DIMM COMMAND Write<%d>: %x", size, data); + return; + + case NAOMI_DIMM_OFFSETL: + dimm_offsetl = data; + DEBUG_LOG(NAOMI, "DIMM OFFSETL Write<%d>: %x", size, data); + return; + case NAOMI_DIMM_PARAMETERL: + dimm_parameterl = data; + DEBUG_LOG(NAOMI, "DIMM PARAMETERL Write<%d>: %x", size, data); + return; + case NAOMI_DIMM_PARAMETERH: + dimm_parameterh = data; + DEBUG_LOG(NAOMI, "DIMM PARAMETERH Write<%d>: %x", size, data); + return; + + case NAOMI_DIMM_STATUS: + DEBUG_LOG(NAOMI, "DIMM STATUS Write<%d>: %x", size, data); + if (data & 0x100) + // write 0 seems ignored + asic_CancelInterrupt(holly_EXP_PCI); + if ((data & 1) == 0) + // irq to dimm + process(); + return; + + default: + NaomiCartridge::WriteMem(address, data, size); + return; + } +} + +void GDCartridge::process() +{ + INFO_LOG(NAOMI, "NetDIMM cmd %04x sock %d offset %04x paramh/l %04x %04x", (dimm_command >> 9) & 0x3f, + dimm_command & 0xff, dimm_offsetl, dimm_parameterh, dimm_parameterl); + + int cmdGroup = (dimm_command >> 13) & 3; + int cmd = (dimm_command >> 9) & 0xf; + switch (cmdGroup) + { + case 0: // system commands + systemCmd(cmd); + break; + case 1: // network commands + WARN_LOG(NAOMI, "Network command received cmd %x. Need full NetDIMM?", cmd); + returnToNaomi(true, 0, -1); + break; + default: + WARN_LOG(NAOMI, "Unknown DIMM command group %d cmd %x", cmdGroup, cmd); + returnToNaomi(true, 0, -1); + break; + } +} + +void GDCartridge::returnToNaomi(bool failed, u16 offsetl, u32 parameter) +{ + dimm_command = ((dimm_command & 0x7e00) + 0x400) | (failed ? 0xff : 0x4); + dimm_offsetl = offsetl; + dimm_parameterh = parameter >> 16; + dimm_parameterl = parameter; + verify(((SB_ISTEXT >> 3) & 1) == 0); + asic_RaiseInterrupt(holly_EXP_PCI); +} + +void GDCartridge::systemCmd(int cmd) +{ + switch (cmd) + { + case 0xf: // startup + INFO_LOG(NAOMI, "NetDIMM startup"); + // bit 16,17: dimm0 size (none, 128, 256, 512) + // bit 18,19: dimm1 size + // bit 28: network enabled (network settings appear in bios menu) + // bit 29: set + // bit 30: gd-rom connected + // bit 31: mobile/ppp network? + // (| 30, 70, F0, 1F0, 3F0, 7F0) + // | offset >> 20 (dimm buffers offset @ size - 16MB) + // offset = (64MB << 0-5) - 16MB + // vf4 forces this value to 0f000000 (256MB) if != 1f000000 (512MB) + if (dimm_data_size == 512_MB) + addrspace::write32(0xc01fc04, (3 << 16) | 0x60000000 | (dimm_data_size >> 20)); // dimm board config 1 x 512 MB + else if (dimm_data_size == 256_MB) + addrspace::write32(0xc01fc04, (2 << 16) | 0x60000000 | (dimm_data_size >> 20)); // dimm board config 1 x 256 MB + else + addrspace::write32(0xc01fc04, (1 << 16) | 0x60000000 | (dimm_data_size >> 20)); // dimm board config 1 x 128 MB + addrspace::write32(0xc01fc0c, 0x1020000 | 0x264); // fw version 1.02 + // DIMM board serial Id + { + const u32 *serial = (u32 *)(getGameSerialId() + 0x20); // get only the serial id + addrspace::write32(0xc01fc40, *serial++); + addrspace::write32(0xc01fc44, *serial++); + addrspace::write32(0xc01fc48, *serial++); + addrspace::write32(0xc01fc4c, *serial++); + } + // SET_BASE_ADDRESS(0c000000, 0) + dimm_command = 0x8600; + dimm_offsetl = 0; + dimm_parameterl = 0; + dimm_parameterh = 0x0c00; + asic_RaiseInterrupt(holly_EXP_PCI); + sh4_sched_request(schedId, SH4_MAIN_CLOCK); + + break; + + case 0: // nop + case 1: // control read + case 3: // set base address + case 4: // peek8 + case 5: // peek16 + case 6: // peek32 + case 8: // poke8 + case 9: // poke16 + case 10: // poke32 + // These are callbacks from naomi + INFO_LOG(NAOMI, "System callback command %x", cmd); + break; + + default: + WARN_LOG(NAOMI, "Unknown system command %x", cmd); + break; + } +} + +void GDCartridge::Serialize(Serializer &ser) const +{ + NaomiCartridge::Serialize(ser); + ser << dimm_command; + ser << dimm_offsetl; + ser << dimm_parameterl; + ser << dimm_parameterh; + sh4_sched_serialize(ser, schedId); +} + +void GDCartridge::Deserialize(Deserializer &deser) +{ + NaomiCartridge::Deserialize(deser); + if (deser.version() >= Deserializer::V53) + { + deser >> dimm_command; + deser >> dimm_offsetl; + deser >> dimm_parameterl; + deser >> dimm_parameterh; + sh4_sched_deserialize(deser, schedId); + } +} + +int GDCartridge::schedCallback() +{ + if (SB_ISTEXT & 8) // holly_EXP_PCI + return SH4_MAIN_CLOCK; + + // regularly peek the test request address + peek(0xc01fc08); + asic_RaiseInterrupt(holly_EXP_PCI); + + u32 testRequest = addrspace::read32(0xc01fc08); + if (testRequest & 1) + { + // bios dimm (fake) test + addrspace::write32(0xc01fc08, testRequest & ~1); + bool isMem; + char *p = (char *)addrspace::writeConst(0xc01fd00, isMem, 4); + strcpy(p, "CHECKING DIMM BD"); + p = (char *)addrspace::writeConst(0xc01fd10, isMem, 4); + strcpy(p, "DIMM0 - GOOD"); + p = (char *)addrspace::writeConst(0xc01fd20, isMem, 4); + strcpy(p, "DIMM1 - GOOD"); + p = (char *)addrspace::writeConst(0xc01fd30, isMem, 4); + strcpy(p, "--- COMPLETED---"); + addrspace::write32(0xc01fc0c, 0x0102a264); + } + else if (testRequest != 0) + { + addrspace::write32(0xc01fc08, 0); + addrspace::write32(0xc01fc0c, 0x03170100); + INFO_LOG(NAOMI, "TEST REQUEST %x", testRequest); + } + + return SH4_MAIN_CLOCK; +} diff --git a/core/hw/naomi/gdcartridge.h b/core/hw/naomi/gdcartridge.h index e1c565675..9b4694b88 100644 --- a/core/hw/naomi/gdcartridge.h +++ b/core/hw/naomi/gdcartridge.h @@ -9,22 +9,15 @@ * // copyright-holders:Olivier Galibert * */ - -#ifndef CORE_HW_NAOMI_GDCARTRIDGE_H_ -#define CORE_HW_NAOMI_GDCARTRIDGE_H_ - +#pragma once #include "naomi_cart.h" #include "imgread/common.h" class GDCartridge: public NaomiCartridge { public: - GDCartridge(u32 size) : NaomiCartridge(size) - { - } - ~GDCartridge() override - { - free(dimm_data); - } + GDCartridge(u32 size); + ~GDCartridge() override; + void Init(LoadProgress *progress = nullptr, std::vector *digest = nullptr) override { device_start(progress, digest); @@ -32,12 +25,50 @@ class GDCartridge: public NaomiCartridge { } void* GetDmaPtr(u32 &size) override; bool Read(u32 offset, u32 size, void* dst) override; + u32 ReadMem(u32 address, u32 size) override; + void WriteMem(u32 address, u32 data, u32 size) override; void SetGDRomName(const char *name, const char *parentName) { this->gdrom_name = name; this->gdrom_parent_name = parentName; } + void Serialize(Serializer &ser) const override; + void Deserialize(Deserializer &deser) override; + protected: + virtual void process(); + virtual int schedCallback(); + void returnToNaomi(bool failed, u16 offsetl, u32 parameter); + + template + void peek(u32 address) + { + static_assert(sizeof(T) == 1 || sizeof(T) == 2 || sizeof(T) == 4); + int size; + switch (sizeof(T)) + { + case 1: + size = 4; + break; + case 2: + size = 5; + break; + case 4: + size = 6; + break; + } + dimm_command = ((address >> 16) & 0x1ff) | (size << 9) | 0x8000; + dimm_offsetl = address & 0xffff; + dimm_parameterl = 0; + dimm_parameterh = 0; + } + u8 *dimm_data = nullptr; u32 dimm_data_size = 0; + u16 dimm_command; + u16 dimm_offsetl; + u16 dimm_parameterl; + u16 dimm_parameterh; + static constexpr u16 DIMM_STATUS = 0x111; + int schedId; private: enum { FILENAME_LENGTH=24 }; @@ -70,6 +101,5 @@ class GDCartridge: public NaomiCartridge { u64 des_encrypt_decrypt(u64 src, const u32 *des_subkeys); u64 rev64(u64 src); void read_gdrom(Disc *gdrom, u32 sector, u8* dst, u32 count = 1, LoadProgress *progress = nullptr); + void systemCmd(int cmd); }; - -#endif /* CORE_HW_NAOMI_GDCARTRIDGE_H_ */ diff --git a/core/hw/naomi/naomi.cpp b/core/hw/naomi/naomi.cpp index 20bad2613..9a6fa8fbd 100644 --- a/core/hw/naomi/naomi.cpp +++ b/core/hw/naomi/naomi.cpp @@ -217,11 +217,20 @@ void naomi_reg_Init() dmaSchedId = sh4_sched_register(0, naomiDmaSched); } +// Sets the full content of the rom board serial eeprom (132 bytes) +// including response to reset and read/write passwords. void setGameSerialId(const u8 *data) { romSerialId.setData(data); } +// Return the protected data from the rom board serial eeprom (112 bytes) +// excluding response to reset and passwords. +const u8 *getGameSerialId() +{ + return romSerialId.getProtectedData(); +} + void naomi_reg_Term() { if (multiboard != nullptr) diff --git a/core/hw/naomi/naomi.h b/core/hw/naomi/naomi.h index dc2ffeae3..4abb1a0e3 100644 --- a/core/hw/naomi/naomi.h +++ b/core/hw/naomi/naomi.h @@ -19,6 +19,7 @@ u16 NaomiBoardIDRead(); u16 NaomiGameIDRead(); void NaomiGameIDWrite(u16 data); void setGameSerialId(const u8 *data); +const u8 *getGameSerialId(); void initDriveSimSerialPipe(); void Naomi_setDmaDelay(); @@ -60,7 +61,7 @@ static inline u32 g2ext_readMem(u32 addr, u32 size) if (multiboard != nullptr) return multiboard->readG2Ext(addr, size); - INFO_LOG(NAOMI, "Unhandled G2 Ext read<%d> at %x", size, addr); + DEBUG_LOG(NAOMI, "Unhandled G2 Ext read<%d> at %x", size, addr); return 0; } @@ -71,5 +72,5 @@ static inline void g2ext_writeMem(u32 addr, u32 data, u32 size) else if (multiboard != nullptr) multiboard->writeG2Ext(addr, size, data); else - INFO_LOG(NAOMI, "Unhandled G2 Ext write<%d> at %x: %x", size, addr, data); + DEBUG_LOG(NAOMI, "Unhandled G2 Ext write<%d> at %x: %x", size, addr, data); } diff --git a/core/hw/naomi/naomi_cart.cpp b/core/hw/naomi/naomi_cart.cpp index 2cc79cb43..a9ee7f746 100644 --- a/core/hw/naomi/naomi_cart.cpp +++ b/core/hw/naomi/naomi_cart.cpp @@ -291,7 +291,9 @@ static void loadMameRom(const std::string& path, const std::string& fileName, Lo case GD: { GDCartridge *gdcart; - if (strncmp(game->name, "vf4", 3) == 0) + if (strncmp(game->name, "vf4", 3) == 0 + || strcmp(game->name, "mj1") == 0 + || strncmp(game->name, "wccf", 4) == 0) gdcart = new NetDimm(game->size); else gdcart = new GDCartridge(game->size); @@ -1060,7 +1062,9 @@ void NaomiCartridge::WriteMem(u32 address, u32 data, u32 size) } if (multiboard != nullptr) multiboard->writeG1(address, size, data); - else + else if (address != NAOMI_MBOARD_DATA_addr + && address != NAOMI_MBOARD_OFFSET_addr + && address != NAOMI_MBOARD_STATUS_addr) DEBUG_LOG(NAOMI, "naomiCart::WriteMem<%d>: unknown %08x <= %x", size, address, data); } diff --git a/core/hw/naomi/naomi_roms.cpp b/core/hw/naomi/naomi_roms.cpp index 25723389b..c6315a1a8 100644 --- a/core/hw/naomi/naomi_roms.cpp +++ b/core/hw/naomi/naomi_roms.cpp @@ -142,6 +142,43 @@ const BIOS_t BIOS[] = { 3, "epr-21576h_multi.ic27", 0x000000, 0x200000, 0xcce01f1f }, } }, + { + "naomidev", + { + //ROM_SYSTEM_BIOS( 21, "bios21", "Set4 Dev BIOS" ) + //{ 0, "boot_rom_64b8.ic606", 0x000000, 0x080000, 0x7a50fab9 }, + //ROM_SYSTEM_BIOS( 22, "bios22", "Dev BIOS v1.10" ) + { 0, "develop110.ic27", 0x000000, 0x200000, 0xde7cfdb0 }, + //ROM_SYSTEM_BIOS( 23, "bios23", "Dev BIOS (Nov 1998)" ) + //{ 0, "develop.ic27", 0x000000, 0x200000, 0x309a196a }, + }, + "naomi", + }, + { + "naomigd", + { + //ROM_SYSTEM_BIOS( 2, "bios2", "epr-21576h (Japan)" ) + { 0, "epr-21576h.ic27", 0x000000, 0x200000, 0xd4895685 }, + //ROM_SYSTEM_BIOS( 1, "bios1", "epr-21576g (Japan)" ) + { 0, "epr-21576g.ic27", 0x000000, 0x200000, 0xd2a1c6bf }, + //ROM_SYSTEM_BIOS( 0, "bios0", "epr-21576e (Japan)" ) + { 0, "epr-21576e.ic27", 0x000000, 0x200000, 0x08c0add7 }, + + //ROM_SYSTEM_BIOS( 3, "bios3", "epr-21578h (Export)" ) + { 2, "epr-21578h.ic27", 0x000000, 0x200000, 0x7b452946 }, + //ROM_SYSTEM_BIOS( 4, "bios4", "epr-21578g (Export)" ) + { 2, "epr-21578g.ic27", 0x000000, 0x200000, 0x55413214 }, + //ROM_SYSTEM_BIOS( 5, "bios5", "epr-21578e (Export)" ) + { 2, "epr-21578e.ic27", 0x000000, 0x200000, 0x087f09a3 }, + + //ROM_SYSTEM_BIOS( 6, "bios6", "epr-21577h (USA)" ) + { 1, "epr-21577h.ic27", 0x000000, 0x200000, 0xfdf17452 }, + //ROM_SYSTEM_BIOS( 7, "bios7", "epr-21577g (USA)" ) + { 1, "epr-21577g.ic27", 0x000000, 0x200000, 0x25f64af7 }, + //ROM_SYSTEM_BIOS( 8, "bios8", "epr-21577e (USA)" ) + { 1, "epr-21577e.ic27", 0x000000, 0x200000, 0xcf36e97b }, + }, + }, { "naomi2", { @@ -205,7 +242,7 @@ const Game Games[] = "Giant Gram 2000", 0x0b000000, 0x7f805c3f, - NULL, + "naomi", M1, ROT0, { diff --git a/core/hw/naomi/netdimm.cpp b/core/hw/naomi/netdimm.cpp index 4acc2a562..128f81850 100644 --- a/core/hw/naomi/netdimm.cpp +++ b/core/hw/naomi/netdimm.cpp @@ -33,7 +33,6 @@ const char *SERVER_NAME = "vfnet.flyca.st"; NetDimm::NetDimm(u32 size) : GDCartridge(size) { - schedId = sh4_sched_register(0, schedCallback, this); if (serverIp == 0) { hostent *hp = gethostbyname(SERVER_NAME); @@ -44,11 +43,6 @@ NetDimm::NetDimm(u32 size) : GDCartridge(size) } } -NetDimm::~NetDimm() -{ - sh4_sched_unregister(schedId); -} - void NetDimm::Init(LoadProgress *progress, std::vector *digest) { GDCartridge::Init(progress, digest); @@ -56,74 +50,6 @@ void NetDimm::Init(LoadProgress *progress, std::vector *digest) finalTuned = strcmp(game->name, "vf4tuned") == 0; } -u32 NetDimm::ReadMem(u32 address, u32 size) -{ - switch (address) - { - case NAOMI_DIMM_COMMAND: - DEBUG_LOG(NAOMI, "DIMM COMMAND read -> %x", dimm_command); - return dimm_command; - case NAOMI_DIMM_OFFSETL: - DEBUG_LOG(NAOMI, "DIMM OFFSETL read -> %x", dimm_offsetl); - return dimm_offsetl; - case NAOMI_DIMM_PARAMETERL: - DEBUG_LOG(NAOMI, "DIMM PARAMETERL read -> %x", dimm_parameterl); - return dimm_parameterl; - case NAOMI_DIMM_PARAMETERH: - DEBUG_LOG(NAOMI, "DIMM PARAMETERH read -> %x", dimm_parameterh); - return dimm_parameterh; - case NAOMI_DIMM_STATUS: - { - u32 rc = DIMM_STATUS & ~(((SB_ISTEXT >> 3) & 1) << 8); - static u32 lastRc; - if (rc != lastRc) - DEBUG_LOG(NAOMI, "DIMM STATUS read -> %x", rc); - lastRc = rc; - return rc; - } - default: - return GDCartridge::ReadMem(address, size); - } -} - -void NetDimm::WriteMem(u32 address, u32 data, u32 size) -{ - switch (address) - { - case NAOMI_DIMM_COMMAND: - dimm_command = data; - DEBUG_LOG(NAOMI, "DIMM COMMAND Write<%d>: %x", size, data); - return; - - case NAOMI_DIMM_OFFSETL: - dimm_offsetl = data; - DEBUG_LOG(NAOMI, "DIMM OFFSETL Write<%d>: %x", size, data); - return; - case NAOMI_DIMM_PARAMETERL: - dimm_parameterl = data; - DEBUG_LOG(NAOMI, "DIMM PARAMETERL Write<%d>: %x", size, data); - return; - case NAOMI_DIMM_PARAMETERH: - dimm_parameterh = data; - DEBUG_LOG(NAOMI, "DIMM PARAMETERH Write<%d>: %x", size, data); - return; - - case NAOMI_DIMM_STATUS: - DEBUG_LOG(NAOMI, "DIMM STATUS Write<%d>: %x", size, data); - if (data & 0x100) - // write 0 seems ignored - asic_CancelInterrupt(holly_EXP_PCI); - if ((data & 1) == 0) - // irq to dimm - process(); - return; - - default: - GDCartridge::WriteMem(address, data, size); - return; - } -} - bool NetDimm::Write(u32 offset, u32 size, u32 data) { // u8 b0 = data; @@ -137,11 +63,6 @@ bool NetDimm::Write(u32 offset, u32 size, u32 data) return true; } -int NetDimm::schedCallback(int tag, int sch_cycl, int jitter, void *arg) -{ - return ((NetDimm *)arg)->schedCallback(); -} - int NetDimm::schedCallback() { fd_set readFds {}; @@ -356,16 +277,6 @@ int NetDimm::schedCallback() return SH4_MAIN_CLOCK; } -void NetDimm::returnToNaomi(bool failed, u16 offsetl, u32 parameter) -{ - dimm_command = ((dimm_command & 0x7e00) + 0x400) | (failed ? 0xff : 0x4); - dimm_offsetl = offsetl; - dimm_parameterh = parameter >> 16; - dimm_parameterl = parameter; - verify(((SB_ISTEXT >> 3) & 1) == 0); - asic_RaiseInterrupt(holly_EXP_PCI); -} - void NetDimm::systemCmd(int cmd) { switch (cmd) @@ -386,6 +297,8 @@ void NetDimm::systemCmd(int cmd) addrspace::write32(0xc01fc04, (3 << 16) | 0x70000000 | (dimmBufferOffset >> 20)); // dimm board config 1 x 512 MB else if (dimm_data_size == 256_MB) addrspace::write32(0xc01fc04, (2 << 16) | 0x70000000 | (dimmBufferOffset >> 20)); // dimm board config 1 x 256 MB + else if (dimm_data_size == 128_MB) + addrspace::write32(0xc01fc04, (1 << 16) | 0x70000000 | (dimmBufferOffset >> 20)); // dimm board config 1 x 128 MB else die("Unsupported dimm mem size"); addrspace::write32(0xc01fc0c, 0x3170000 | 0x264); // fw version | 100/264/364? @@ -397,10 +310,14 @@ void NetDimm::systemCmd(int cmd) addrspace::write32(0xc01fc24, 0x3e000a); addrspace::write32(0xc01fc28, 0x18077f); addrspace::write32(0xc01fc2c, 0x10014); - // PIC16? - //addrspace::write32(0xc01fc40, .); - // ... - //addrspace::write32(0xc01fc54, .); + // DIMM board serial Id + { + const u32 *serial = (u32 *)(getGameSerialId() + 0x20); // get only the serial id + addrspace::write32(0xc01fc40, *serial++); + addrspace::write32(0xc01fc44, *serial++); + addrspace::write32(0xc01fc48, *serial++); + addrspace::write32(0xc01fc4c, *serial++); + } addrspace::write32(0xc01fc18, 0x10002); // net mode (2 or 4 is mobile), bit 16 dhcp? // network order addrspace::write32(0xc01fc60, htonl(0xc0a80101)); // ip address (192.168.1.1) @@ -810,29 +727,20 @@ void NetDimm::process() netCmd(cmd); break; default: - WARN_LOG(NAOMI, "Unknown DIMM command group %d cmd %x\n", cmdGroup, cmd); + WARN_LOG(NAOMI, "Unknown DIMM command group %d cmd %x", cmdGroup, cmd); returnToNaomi(true, 0, -1); break; } } -void NetDimm::Serialize(Serializer &ser) const -{ - GDCartridge::Serialize(ser); - ser << dimm_command; - ser << dimm_offsetl; - ser << dimm_parameterl; - ser << dimm_parameterh; - sh4_sched_serialize(ser, schedId); -} - void NetDimm::Deserialize(Deserializer &deser) { GDCartridge::Deserialize(deser); for (Socket& socket : sockets) socket.close(); - if (deser.version() >= Deserializer::V36) + if (deser.version() >= Deserializer::V36 && deser.version() < Deserializer::V53) { + // moved to parent class in v53 deser >> dimm_command; deser >> dimm_offsetl; deser >> dimm_parameterl; diff --git a/core/hw/naomi/netdimm.h b/core/hw/naomi/netdimm.h index 436e592ae..e936c70e0 100644 --- a/core/hw/naomi/netdimm.h +++ b/core/hw/naomi/netdimm.h @@ -24,23 +24,18 @@ class NetDimm : public GDCartridge { public: NetDimm(u32 size); - ~NetDimm() override; void Init(LoadProgress *progress = nullptr, std::vector *digest = nullptr) override; - u32 ReadMem(u32 address, u32 size) override; - void WriteMem(u32 address, u32 data, u32 size) override; - bool Write(u32 offset, u32 size, u32 data) override; - void Serialize(Serializer &ser) const override; void Deserialize(Deserializer &deser) override; +protected: + void process() override; + int schedCallback() override; + private: - void returnToNaomi(bool failed, u16 offsetl, u32 parameter); - static int schedCallback(int tag, int sch_cycl, int jitter, void *arg); - int schedCallback(); - void process(); void systemCmd(int cmd); void netCmd(int cmd); @@ -67,29 +62,6 @@ class NetDimm : public GDCartridge dimm_parameterh = value >> 16; } - template - void peek(u32 address) - { - static_assert(sizeof(T) == 1 || sizeof(T) == 2 || sizeof(T) == 4); - int size; - switch (sizeof(T)) - { - case 1: - size = 4; - break; - case 2: - size = 5; - break; - case 4: - size = 6; - break; - } - dimm_command = ((address >> 16) & 0x1ff) | (size << 9) | 0x8000; - dimm_offsetl = address & 0xffff; - dimm_parameterl = 0; - dimm_parameterh = 0; - } - sock_t getSocket(int idx) { if (idx < 1 || idx > (int)sockets.size()) @@ -105,13 +77,6 @@ class NetDimm : public GDCartridge return false; } - u16 dimm_command; - u16 dimm_offsetl; - u16 dimm_parameterl; - u16 dimm_parameterh; - static constexpr u16 DIMM_STATUS = 0x111; - int schedId; - struct Socket { Socket() = default; Socket(sock_t fd) : fd(fd) {} diff --git a/core/serialize.h b/core/serialize.h index e367f8fce..434b7a9c1 100644 --- a/core/serialize.h +++ b/core/serialize.h @@ -63,7 +63,8 @@ class SerializeBase V50, V51, V52, - Current = V52, + V53, + Current = V53, Next = Current + 1, }; From fbd52a72d44b7e2e0c62947eda4f9d19708a519e Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sun, 17 Nov 2024 20:39:30 +0100 Subject: [PATCH 23/81] naomi: load and decrypt gd-rom on demand Shorten load times and avoid ANR on android with libretro when loading a GD-ROM game. Load and decrypt data by 16 KB chunk when needed. Issue #1457 Issue #1738 --- core/hw/naomi/gdcartridge.cpp | 53 +++++++++++++++++++++-------------- core/hw/naomi/gdcartridge.h | 15 +++++++--- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/core/hw/naomi/gdcartridge.cpp b/core/hw/naomi/gdcartridge.cpp index c3e60ba32..734aab23b 100644 --- a/core/hw/naomi/gdcartridge.cpp +++ b/core/hw/naomi/gdcartridge.cpp @@ -21,6 +21,7 @@ #include "serialize.h" #include "hw/sh4/sh4_sched.h" #include "naomi.h" +#include /* @@ -455,6 +456,7 @@ void GDCartridge::device_start(LoadProgress *progress, std::vector *digest) dimm_data = NULL; } dimm_data_size = 0; + loadedSegments.clear(); char name[128]; memset(name,'\0',128); @@ -501,7 +503,6 @@ void GDCartridge::device_start(LoadProgress *progress, std::vector *digest) u8 buffer[2048]; std::string parent = hostfs::storage().getParentPath(settings.content.path); std::string gdrom_path = get_file_basename(settings.content.fileName) + "/" + gdrom_name; - std::unique_ptr gdrom; try { gdrom_path = hostfs::storage().getSubPath(parent, gdrom_path); gdrom = std::unique_ptr(OpenDisc(gdrom_path + ".chd", digest)); @@ -536,7 +537,7 @@ void GDCartridge::device_start(LoadProgress *progress, std::vector *digest) // directory u8 dir_sector[2048]; // find data of file - u32 file_start, file_size; + u32 file_size; if (netpic == 0) { u32 dir = ((buffer[0x2 + 0] << 0) | @@ -596,25 +597,11 @@ void GDCartridge::device_start(LoadProgress *progress, std::vector *digest) if (dimm_data_size != file_rounded_size) memset(dimm_data + file_rounded_size, 0, dimm_data_size - file_rounded_size); - // read encrypted data into dimm_data - u32 sectors = file_rounded_size / 2048; - read_gdrom(gdrom.get(), file_start, dimm_data, sectors, progress); + loadedSegments.resize(dimm_data_size / SEGMENT_SIZE); + std::fill(loadedSegments.begin() + (file_rounded_size + SEGMENT_SIZE - 1) / SEGMENT_SIZE, + loadedSegments.end(), true); - // decrypt loaded data - u32 des_subkeys[32]; des_generate_subkeys(rev64(key), des_subkeys); - - for (u32 i = 0; i < file_rounded_size; i += 8) - { - if ((i & 0xfff) == 0 && progress != nullptr) - { - if (progress->cancelled) - throw LoadCancelledException(); - progress->label = "Decrypting..."; - progress->progress = (float)(i + 8) / file_rounded_size; - } - *(u64 *)(dimm_data + i) = des_encrypt_decrypt(*(u64 *)(dimm_data + i), des_subkeys); - } } if (!dimm_data) @@ -622,6 +609,27 @@ void GDCartridge::device_start(LoadProgress *progress, std::vector *digest) } } +void GDCartridge::loadSegments(u32 offset, u32 size) +{ + const u32 lastSegment = (offset + size - 1) / SEGMENT_SIZE; + for (u32 segment = offset / SEGMENT_SIZE; segment <= lastSegment; segment++) + { + if (loadedSegments[segment]) + continue; + DEBUG_LOG(NAOMI, "Loading segment %d", segment); + // load data + read_gdrom(gdrom.get(), file_start + (segment * SEGMENT_SIZE) / 2048, + dimm_data + segment * SEGMENT_SIZE, + SEGMENT_SIZE / 2048, + nullptr); + // decrypt loaded data + u64 *pData = (u64 *)(dimm_data + segment * SEGMENT_SIZE); + for (u32 i = 0; i < SEGMENT_SIZE; i += 8, pData++) + *pData = des_encrypt_decrypt(*pData, des_subkeys); + loadedSegments[segment] = true; + } +} + void GDCartridge::device_reset() { dimm_cur_address = 0; @@ -635,6 +643,7 @@ void *GDCartridge::GetDmaPtr(u32 &size) } dimm_cur_address = DmaOffset & (dimm_data_size-1); size = std::min(size, dimm_data_size - dimm_cur_address); + loadSegments(dimm_cur_address, size); return dimm_data + dimm_cur_address; } @@ -645,8 +654,10 @@ bool GDCartridge::Read(u32 offset, u32 size, void *dst) *(u32 *)dst = 0; return true; } - u32 addr = offset & (dimm_data_size-1); - memcpy(dst, &dimm_data[addr], std::min(size, dimm_data_size - addr)); + u32 addr = offset & (dimm_data_size - 1); + size = std::min(size, dimm_data_size - addr); + loadSegments(addr, size); + memcpy(dst, &dimm_data[addr], size); return true; } diff --git a/core/hw/naomi/gdcartridge.h b/core/hw/naomi/gdcartridge.h index 9b4694b88..7ec87e756 100644 --- a/core/hw/naomi/gdcartridge.h +++ b/core/hw/naomi/gdcartridge.h @@ -13,13 +13,13 @@ #include "naomi_cart.h" #include "imgread/common.h" -class GDCartridge: public NaomiCartridge { +class GDCartridge: public NaomiCartridge +{ public: GDCartridge(u32 size); ~GDCartridge() override; - void Init(LoadProgress *progress = nullptr, std::vector *digest = nullptr) override - { + void Init(LoadProgress *progress = nullptr, std::vector *digest = nullptr) override { device_start(progress, digest); device_reset(); } @@ -71,7 +71,7 @@ class GDCartridge: public NaomiCartridge { int schedId; private: - enum { FILENAME_LENGTH=24 }; + static constexpr int FILENAME_LENGTH = 24; const char *gdrom_name = nullptr; const char *gdrom_parent_name = nullptr; @@ -91,6 +91,12 @@ class GDCartridge: public NaomiCartridge { static const u32 DES_MASK_TABLE[]; static const u8 DES_ROTATE_TABLE[16]; + std::vector loadedSegments; + static constexpr u32 SEGMENT_SIZE = 16_KB; + std::unique_ptr gdrom; + u32 file_start = 0; + u32 des_subkeys[32]; + void device_start(LoadProgress *progress, std::vector *digest); void device_reset(); void find_file(const char *name, const u8 *dir_sector, u32 &file_start, u32 &file_size); @@ -101,5 +107,6 @@ class GDCartridge: public NaomiCartridge { u64 des_encrypt_decrypt(u64 src, const u32 *des_subkeys); u64 rev64(u64 src); void read_gdrom(Disc *gdrom, u32 sector, u8* dst, u32 count = 1, LoadProgress *progress = nullptr); + void loadSegments(u32 offset, u32 size); void systemCmd(int cmd); }; From 6612e3fef898a3a2228e593f1ff240d0c8119b60 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 20 Nov 2024 11:46:34 +0100 Subject: [PATCH 24/81] gdrom: update settings and more when swapping disks Set forced and per game settings. Update game id and save state file name. Reset cheats and custom textures. Update game title and window title. Issue #1748 hle bios: reboot the game when "exit to bios" vector is called --- core/achievements/achievements.cpp | 6 ++-- core/emulator.cpp | 50 +++++++++++++++++++++++++++--- core/emulator.h | 9 ++++++ core/hw/gdrom/gdromv3.cpp | 2 +- core/imgread/common.cpp | 36 +++++++++++++++------ core/imgread/common.h | 14 ++++++--- core/reios/reios.cpp | 16 ++++++++-- core/ui/gui.cpp | 19 ++++++------ shell/libretro/libretro.cpp | 32 +++++++++---------- 9 files changed, 130 insertions(+), 54 deletions(-) diff --git a/core/achievements/achievements.cpp b/core/achievements/achievements.cpp index abdd54979..b2ff0ed63 100644 --- a/core/achievements/achievements.cpp +++ b/core/achievements/achievements.cpp @@ -779,8 +779,7 @@ std::string Achievements::getGameHash() { if (settings.platform.isConsole()) { - const u32 diskType = libGDR_GetDiscType(); - if (diskType == NoDisk || diskType == Open) + if (!gdr::isLoaded()) return {}; // Reopen the disk locally to avoid threading issues (CHD) try { @@ -964,7 +963,8 @@ void Achievements::unloadGame() void Achievements::diskChange() { - if (!active) + if (!active || settings.content.path.empty()) + // Don't unload the game when the lid is open while swapping disks return; std::string hash = getGameHash(); if (hash == "") { diff --git a/core/emulator.cpp b/core/emulator.cpp index 1ef865b39..ceed3c9de 100644 --- a/core/emulator.cpp +++ b/core/emulator.cpp @@ -51,6 +51,7 @@ #include "hw/sh4/dyna/ngen.h" settings_t settings; +constexpr char const *BIOS_TITLE = "Dreamcast BIOS"; static void loadSpecialSettings() { @@ -573,14 +574,14 @@ void Emulator::loadGame(const char *path, LoadProgress *progress) // Boot BIOS if (!nvmem::loadFiles()) throw FlycastException("No BIOS file found in " + hostfs::getFlashSavePath("", "")); - InitDrive(""); + gdr::initDrive(""); } else { std::string extension = get_file_extension(settings.content.path); if (extension != "elf") { - if (InitDrive(settings.content.path)) + if (gdr::initDrive(settings.content.path)) { loadGameSpecificSettings(); if (config::UseReios || !nvmem::loadFiles()) @@ -597,18 +598,18 @@ void Emulator::loadGame(const char *path, LoadProgress *progress) settings.content.path.clear(); if (!nvmem::loadFiles()) throw FlycastException("This media cannot be loaded"); - InitDrive(""); + gdr::initDrive(""); } } else { // Elf only supported with HLE BIOS nvmem::loadHle(); - InitDrive(""); + gdr::initDrive(""); } } if (settings.content.path.empty()) - settings.content.title = "Dreamcast BIOS"; + settings.content.title = BIOS_TITLE; if (progress) progress->progress = 1.0f; @@ -1063,4 +1064,43 @@ bool Emulator::restartCpu() return true; } +void Emulator::insertGdrom(const std::string& path) +{ + if (settings.platform.isArcade()) + return; + gdr::insertDisk(path); + diskChange(); +} + +void Emulator::openGdrom() +{ + if (settings.platform.isArcade()) + return; + gdr::openLid(); + diskChange(); +} + +void Emulator::diskChange() +{ + config::Settings::instance().reset(); + config::Settings::instance().load(false); + if (!settings.content.path.empty()) + { + hostfs::FileInfo info = hostfs::storage().getFileInfo(settings.content.path); + settings.content.fileName = info.name; + loadGameSpecificSettings(); + } + else + { + settings.content.fileName.clear(); + settings.content.gameId.clear(); + settings.content.title = BIOS_TITLE; + } + cheatManager.reset(settings.content.gameId); + if (cheatManager.isWidescreen()) + config::ScreenStretching.override(134); // 4:3 -> 16:9 + custom_texture.Terminate(); + EventManager::event(Event::DiskChange); +} + Emulator emu; diff --git a/core/emulator.h b/core/emulator.h index 945fc193d..4afd195a0 100644 --- a/core/emulator.h +++ b/core/emulator.h @@ -174,6 +174,14 @@ class Emulator * Load the machine state from the passed deserializer */ void loadstate(Deserializer& deser); + /** + * Insert a new media in the GD-ROM drive. + */ + void insertGdrom(const std::string& path); + /** + * Open the GD-ROM drive lid. + */ + void openGdrom(); Sh4Executor *getSh4Executor(); @@ -182,6 +190,7 @@ class Emulator private: bool checkStatus(bool wait = false); void runInternal(); + void diskChange(); enum State { Uninitialized = 0, diff --git a/core/hw/gdrom/gdromv3.cpp b/core/hw/gdrom/gdromv3.cpp index afe9d3aa3..e61096c9f 100644 --- a/core/hw/gdrom/gdromv3.cpp +++ b/core/hw/gdrom/gdromv3.cpp @@ -1326,7 +1326,7 @@ void gdrom_reg_Reset(bool hard) memcpy(GD_HardwareInfo.drive_info, "SE ", sizeof(GD_HardwareInfo.drive_info)); memcpy(GD_HardwareInfo.system_version, "Rev 6.43", sizeof(GD_HardwareInfo.system_version)); memcpy(GD_HardwareInfo.system_date, "990408", sizeof(GD_HardwareInfo.system_date)); - TermDrive(); + gdr::termDrive(); } SB_GDST = 0; SB_GDEN = 0; diff --git a/core/imgread/common.cpp b/core/imgread/common.cpp index b99ed778f..6e7199241 100644 --- a/core/imgread/common.cpp +++ b/core/imgread/common.cpp @@ -96,9 +96,11 @@ Disc* OpenDisc(const std::string& path, std::vector *digest) throw FlycastException("Unknown disk format"); } +namespace gdr { + static bool loadDisk(const std::string& path) { - TermDrive(); + termDrive(); //try all drivers std::vector digest; @@ -122,7 +124,8 @@ static bool loadDisk(const std::string& path) static bool doDiscSwap(const std::string& path); -bool InitDrive(const std::string& path) + +bool initDrive(const std::string& path) { bool rc = doDiscSwap(path); if (rc && disc == nullptr) @@ -141,9 +144,10 @@ bool InitDrive(const std::string& path) return rc; } -void DiscOpenLid() +void openLid() { - TermDrive(); + settings.content.path.clear(); + termDrive(); NullDriveDiscType = Open; gd_setdisc(); } @@ -152,7 +156,7 @@ static bool doDiscSwap(const std::string& path) { if (path.empty()) { - TermDrive(); + termDrive(); NullDriveDiscType = NoDisk; return true; } @@ -164,13 +168,22 @@ static bool doDiscSwap(const std::string& path) return false; } -void TermDrive() +void termDrive() { sh4_sched_request(schedId, -1); delete disc; disc = nullptr; } +bool isOpen() { + return disc == nullptr && NullDriveDiscType == Open; +} + +bool isLoaded() { + return disc != nullptr; +} + +} // namespace gdr // //convert our nice toc struct to dc's native one :) @@ -346,19 +359,22 @@ static int discSwapCallback(int tag, int sch_cycl, int jitter, void *arg) return 0; } -bool DiscSwap(const std::string& path) +namespace gdr +{ + +void insertDisk(const std::string& path) { if (!doDiscSwap(path)) throw FlycastException("This media cannot be loaded"); - EventManager::event(Event::DiskChange); + settings.content.path = path; // Drive is busy after the lid was closed sns_asc = 4; sns_ascq = 1; sns_key = 2; SecNumber.Status = GD_BUSY; sh4_sched_request(schedId, SH4_MAIN_CLOCK); // 1 s +} - return true; } void libGDR_init() @@ -368,7 +384,7 @@ void libGDR_init() } void libGDR_term() { - TermDrive(); + gdr::termDrive(); sh4_sched_unregister(schedId); schedId = -1; } diff --git a/core/imgread/common.h b/core/imgread/common.h index 525eec341..5397f7882 100644 --- a/core/imgread/common.h +++ b/core/imgread/common.h @@ -60,10 +60,16 @@ enum DiskArea DoubleDensity }; -bool InitDrive(const std::string& path); -void TermDrive(); -bool DiscSwap(const std::string& path); -void DiscOpenLid(); +namespace gdr { + +bool initDrive(const std::string& path); +void termDrive(); +void insertDisk(const std::string& path); +void openLid(); +bool isOpen(); +bool isLoaded(); + +} struct Session { diff --git a/core/reios/reios.cpp b/core/reios/reios.cpp index a7a65a8d9..4f00676b4 100644 --- a/core/reios/reios.cpp +++ b/core/reios/reios.cpp @@ -26,6 +26,7 @@ #include "imgread/isofs.h" #include "hw/sh4/sh4_mmr.h" #include "oslib/resources.h" +#include "oslib/oslib.h" #include @@ -133,7 +134,7 @@ ip_meta_t ip_meta; void reios_disk_id() { - if (libGDR_GetDiscType() == Open || libGDR_GetDiscType() == NoDisk) + if (!gdr::isLoaded()) { memset(&ip_meta, 0, sizeof(ip_meta)); return; @@ -351,8 +352,17 @@ static void reios_sys_misc() break; case 1: // Exit to BIOS menu - WARN_LOG(REIOS, "SYS_MISC 1"); - throw FlycastException("Reboot to BIOS"); + { + WARN_LOG(REIOS, "SYS_MISC 1"); + if (gdr::isLoaded()) { + // just restart the game + p_sh4rcb->cntx.pc = 0xa0000000; + os_notify("Reboot to BIOS", 5000); + } + else { + throw FlycastException("Reboot to BIOS"); + } + } break; case 2: // check disk diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 5381fdf9c..c0e13180a 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -709,14 +709,14 @@ static void gui_display_commands() ImGui::NextColumn(); // Insert/Eject Disk - const char *disk_label = libGDR_GetDiscType() == Open ? ICON_FA_COMPACT_DISC " Insert Disk" : ICON_FA_COMPACT_DISC " Eject Disk"; + const char *disk_label = gdr::isOpen() ? ICON_FA_COMPACT_DISC " Insert Disk" : ICON_FA_COMPACT_DISC " Eject Disk"; if (ImGui::Button(disk_label, ScaledVec2(buttonWidth, 50))) { - if (libGDR_GetDiscType() == Open) { + if (gdr::isOpen()) { gui_setState(GuiState::SelectDisk); } else { - DiscOpenLid(); + emu.openGdrom(); gui_setState(GuiState::Closed); } } @@ -3272,11 +3272,15 @@ static void gui_display_content() } if (pressed) { + if (!config::BoxartDisplayMode) + art = boxart.getBoxart(game); + settings.content.title = art.name; + if (settings.content.title.empty() || settings.content.title == game.fileName) + settings.content.title = get_file_basename(game.fileName); if (gui_state == GuiState::SelectDisk) { - settings.content.path = game.path; try { - DiscSwap(game.path); + emu.insertGdrom(game.path); gui_setState(GuiState::Closed); } catch (const FlycastException& e) { gui_error(e.what()); @@ -3284,11 +3288,6 @@ static void gui_display_content() } else { - if (!config::BoxartDisplayMode) - art = boxart.getBoxart(game); - settings.content.title = art.name; - if (settings.content.title.empty() || settings.content.title == game.fileName) - settings.content.title = get_file_basename(game.fileName); std::string gamePath(game.path); scanner.get_mutex().unlock(); gui_start_game(gamePath); diff --git a/shell/libretro/libretro.cpp b/shell/libretro/libretro.cpp index 3c2c503ee..fb4dfd5e3 100644 --- a/shell/libretro/libretro.cpp +++ b/shell/libretro/libretro.cpp @@ -49,21 +49,16 @@ #include "emulator.h" #include "hw/sh4/sh4_mem.h" #include "hw/sh4/sh4_sched.h" -#include "hw/sh4/dyna/blockmanager.h" #include "keyboard_map.h" #include "hw/maple/maple_cfg.h" #include "hw/maple/maple_if.h" -#include "hw/maple/maple_cfg.h" -#include "hw/pvr/spg.h" #include "hw/naomi/naomi_cart.h" #include "hw/naomi/card_reader.h" -#include "imgread/common.h" #include "LogManager.h" #include "cheats.h" #include "rend/osd.h" #include "cfg/option.h" #include "version.h" -#include "rend/transform_matrix.h" #include "oslib/oslib.h" constexpr char slash = path_default_slash_c(); @@ -3519,13 +3514,14 @@ static bool retro_set_eject_state(bool ejected) disc_tray_open = ejected; if (ejected) { - DiscOpenLid(); + emu.openGdrom(); return true; } else { try { - return DiscSwap(disk_paths[disk_index]); + emu.insertGdrom(disk_paths[disk_index]); + return true; } catch (const FlycastException& e) { ERROR_LOG(GDROM, "%s", e.what()); return false; @@ -3546,19 +3542,19 @@ static unsigned retro_get_image_index() static bool retro_set_image_index(unsigned index) { disk_index = index; - if (disk_index >= disk_paths.size()) - { - // No disk in drive - settings.content.path.clear(); - return true; - } - settings.content.path = disk_paths[index]; + try { + if (disk_index >= disk_paths.size()) + { + // No disk in drive + emu.insertGdrom(""); + return true; + } - if (disc_tray_open) - return true; + if (disc_tray_open) + return true; - try { - return DiscSwap(settings.content.path); + emu.insertGdrom(disk_paths[index]); + return true; } catch (const FlycastException& e) { ERROR_LOG(GDROM, "%s", e.what()); return false; From aa95c71ead5629f5535681124771f33cbc7b20bf Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 20 Nov 2024 12:06:39 +0100 Subject: [PATCH 25/81] libretro build fix --- shell/libretro/libretro.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shell/libretro/libretro.cpp b/shell/libretro/libretro.cpp index fb4dfd5e3..7792c905a 100644 --- a/shell/libretro/libretro.cpp +++ b/shell/libretro/libretro.cpp @@ -52,6 +52,8 @@ #include "keyboard_map.h" #include "hw/maple/maple_cfg.h" #include "hw/maple/maple_if.h" +#include "hw/pvr/pvr_regs.h" +#include "hw/pvr/Renderer_if.h" #include "hw/naomi/naomi_cart.h" #include "hw/naomi/card_reader.h" #include "LogManager.h" From b4dcc64ef69ab8712b6acfd8e20ef08b0337da8c Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 22 Nov 2024 14:42:51 +0100 Subject: [PATCH 26/81] android: draw onscreen gamepad with imgui Get rid of Renderer::DrawOSD. gl: make a GlQuadDrawer class. Draw on-screen gamepad using imgui. Clean up vgamepad interface. --- core/hw/pvr/Renderer_if.h | 2 - core/rend/dx11/dx11_renderer.cpp | 8 +- core/rend/dx11/dx11_renderer.h | 2 +- core/rend/dx11/oit/dx11_oitrenderer.cpp | 2 +- core/rend/dx9/d3d_renderer.cpp | 8 +- core/rend/dx9/d3d_renderer.h | 2 +- core/rend/gl4/gldraw.cpp | 1 - core/rend/gl4/gles.cpp | 14 +- core/rend/gles/gldraw.cpp | 15 +- core/rend/gles/gles.cpp | 163 +-------------- core/rend/gles/gles.h | 23 +-- core/rend/gles/postprocess.cpp | 3 +- core/rend/gles/quad.cpp | 99 +++------- core/rend/gles/quad.h | 60 ++++++ core/rend/osd.cpp | 156 --------------- core/rend/osd.h | 15 -- core/rend/vulkan/pipeline.cpp | 77 -------- core/rend/vulkan/pipeline.h | 71 ------- core/rend/vulkan/shaders.cpp | 37 ---- core/rend/vulkan/shaders.h | 18 -- core/rend/vulkan/vulkan_context.cpp | 1 - core/rend/vulkan/vulkan_renderer.cpp | 81 -------- core/rend/vulkan/vulkan_renderer.h | 4 - core/ui/gui.cpp | 19 +- core/ui/gui.h | 2 +- core/ui/gui_android.cpp | 186 +++++++++++++++++- core/ui/gui_android.h | 49 ++++- core/ui/gui_util.cpp | 10 + core/ui/gui_util.h | 2 + core/ui/mainui.cpp | 5 +- .../com/flycast/emulator/BaseGLActivity.java | 11 +- .../flycast/emulator/NativeGLActivity.java | 2 +- .../java/com/flycast/emulator/emu/JNIdc.java | 6 +- .../emulator/emu/VirtualJoystickDelegate.java | 18 +- .../flycast/src/main/jni/src/Android.cpp | 35 ++-- .../src/main/jni/src/android_gamepad.h | 2 +- 36 files changed, 404 insertions(+), 805 deletions(-) create mode 100644 core/rend/gles/quad.h diff --git a/core/hw/pvr/Renderer_if.h b/core/hw/pvr/Renderer_if.h index d7f7f17a8..369498ce3 100644 --- a/core/hw/pvr/Renderer_if.h +++ b/core/hw/pvr/Renderer_if.h @@ -71,8 +71,6 @@ struct Renderer virtual bool Present() { return true; } - virtual void DrawOSD(bool clear_screen) { } - virtual BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) { return nullptr; } }; diff --git a/core/rend/dx11/dx11_renderer.cpp b/core/rend/dx11/dx11_renderer.cpp index 187472b86..1e34526d7 100644 --- a/core/rend/dx11/dx11_renderer.cpp +++ b/core/rend/dx11/dx11_renderer.cpp @@ -514,7 +514,7 @@ bool DX11Renderer::Render() #ifndef LIBRETRO deviceContext->OMSetRenderTargets(1, &theDX11Context.getRenderTarget().get(), nullptr); displayFramebuffer(); - DrawOSD(false); + drawOSD(); renderVideoRouting(); theDX11Context.setFrameRendered(); #else @@ -1022,7 +1022,7 @@ void DX11Renderer::RenderFramebuffer(const FramebufferInfo& info) deviceContext->OMSetRenderTargets(1, &theDX11Context.getRenderTarget().get(), nullptr); displayFramebuffer(); - DrawOSD(false); + drawOSD(); renderVideoRouting(); theDX11Context.setFrameRendered(); #else @@ -1241,10 +1241,10 @@ void DX11Renderer::updateFogTexture() deviceContext->PSSetSamplers(2, 1, &samplers->getSampler(true).get()); } -void DX11Renderer::DrawOSD(bool clear_screen) +void DX11Renderer::drawOSD() { #ifndef LIBRETRO - theDX11Context.setOverlay(!clear_screen); + theDX11Context.setOverlay(true); gui_display_osd(); theDX11Context.setOverlay(false); #endif diff --git a/core/rend/dx11/dx11_renderer.h b/core/rend/dx11/dx11_renderer.h index 2d511f572..2f603a06a 100644 --- a/core/rend/dx11/dx11_renderer.h +++ b/core/rend/dx11/dx11_renderer.h @@ -53,7 +53,6 @@ struct DX11Renderer : public Renderer } bool RenderLastFrame() override; - void DrawOSD(bool clear_screen) override; BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override; bool GetLastFrame(std::vector& data, int& width, int& height) override; @@ -102,6 +101,7 @@ struct DX11Renderer : public Renderer void writeFramebufferToVRAM(); void renderVideoRouting(); void resetContextState(); + void drawOSD(); ComPtr device; ComPtr deviceContext; diff --git a/core/rend/dx11/oit/dx11_oitrenderer.cpp b/core/rend/dx11/oit/dx11_oitrenderer.cpp index e58b90a09..0f6990e42 100644 --- a/core/rend/dx11/oit/dx11_oitrenderer.cpp +++ b/core/rend/dx11/oit/dx11_oitrenderer.cpp @@ -693,7 +693,7 @@ struct DX11OITRenderer : public DX11Renderer #ifndef LIBRETRO deviceContext->OMSetRenderTargets(1, &theDX11Context.getRenderTarget().get(), nullptr); displayFramebuffer(); - DrawOSD(false); + drawOSD(); renderVideoRouting(); theDX11Context.setFrameRendered(); #else diff --git a/core/rend/dx9/d3d_renderer.cpp b/core/rend/dx9/d3d_renderer.cpp index ba830eda3..122a3983f 100644 --- a/core/rend/dx9/d3d_renderer.cpp +++ b/core/rend/dx9/d3d_renderer.cpp @@ -300,7 +300,7 @@ void D3DRenderer::RenderFramebuffer(const FramebufferInfo& info) aspectRatio = getDCFramebufferAspectRatio(); displayFramebuffer(); - DrawOSD(false); + drawOSD(); frameRendered = true; frameRenderedOnce = true; theDXContext.setFrameRendered(); @@ -1167,7 +1167,7 @@ bool D3DRenderer::Render() { aspectRatio = getOutputFramebufferAspectRatio(); displayFramebuffer(); - DrawOSD(false); + drawOSD(); frameRendered = true; frameRenderedOnce = true; theDXContext.setFrameRendered(); @@ -1330,9 +1330,9 @@ void D3DRenderer::updateFogTexture() device->SetSamplerState(2, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); } -void D3DRenderer::DrawOSD(bool clear_screen) +void D3DRenderer::drawOSD() { - theDXContext.setOverlay(!clear_screen); + theDXContext.setOverlay(true); gui_display_osd(); theDXContext.setOverlay(false); } diff --git a/core/rend/dx9/d3d_renderer.h b/core/rend/dx9/d3d_renderer.h index bdea5cbe9..32742915a 100644 --- a/core/rend/dx9/d3d_renderer.h +++ b/core/rend/dx9/d3d_renderer.h @@ -111,7 +111,6 @@ struct D3DRenderer : public Renderer frameRendered = false; return true; } - void DrawOSD(bool clear_screen) override; BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override; void preReset(); void postReset(); @@ -140,6 +139,7 @@ struct D3DRenderer : public Renderer void prepareRttRenderTarget(u32 texAddress, int& vpWidth, int& vpHeight); void readRttRenderTarget(u32 texAddress); void writeFramebufferToVRAM(); + void drawOSD(); RenderStateCache devCache; ComPtr device; diff --git a/core/rend/gl4/gldraw.cpp b/core/rend/gl4/gldraw.cpp index 54e1862bd..efc2ae7c6 100644 --- a/core/rend/gl4/gldraw.cpp +++ b/core/rend/gl4/gldraw.cpp @@ -20,7 +20,6 @@ #include "rend/gles/glcache.h" #include "rend/gles/naomi2.h" #include "rend/tileclip.h" -#include "rend/osd.h" #include diff --git a/core/rend/gl4/gles.cpp b/core/rend/gl4/gles.cpp index 7c7ffc589..e28e3e6a0 100644 --- a/core/rend/gl4/gles.cpp +++ b/core/rend/gl4/gles.cpp @@ -19,10 +19,10 @@ #include "gl4.h" #include "rend/gles/glcache.h" #include "rend/transform_matrix.h" -#include "rend/osd.h" #include "glsl.h" #include "gl4naomi2.h" #include "rend/gles/naomi2.h" +#include "rend/gles/quad.h" #ifdef LIBRETRO #include "rend/gles/postprocess.h" @@ -670,7 +670,7 @@ static void gl_create_resources() } GlVertexArray::unbind(); - initQuad(); + gl.quad = std::make_unique(); glCheck(); } @@ -713,7 +713,7 @@ struct OpenGL4Renderer : OpenGLRenderer if (!config::EmulateFramebuffer) { frameRendered = true; - DrawOSD(false); + drawOSD(); renderVideoRouting(); } restoreCurrentFramebuffer(); @@ -729,14 +729,6 @@ struct OpenGL4Renderer : OpenGLRenderer } bool renderFrame(int width, int height); - - void DrawOSD(bool clearScreen) override - { - drawVmusAndCrosshairs(width, height); -#ifndef LIBRETRO - gui_display_osd(); -#endif - } }; //setup diff --git a/core/rend/gles/gldraw.cpp b/core/rend/gles/gldraw.cpp index f5ccaf9ec..ded6e531d 100644 --- a/core/rend/gles/gldraw.cpp +++ b/core/rend/gles/gldraw.cpp @@ -1,5 +1,6 @@ #include "glcache.h" #include "gles.h" +#include "quad.h" #include "rend/tileclip.h" #include "rend/osd.h" #include "naomi2.h" @@ -667,7 +668,7 @@ void OpenGLRenderer::RenderFramebuffer(const FramebufferInfo& info) else { glcache.Disable(GL_BLEND); - drawQuad(gl.dcfb.tex, false, false); + gl.quad->draw(gl.dcfb.tex, false, false); } #ifdef LIBRETRO postProcessor.render(glsm_get_current_framebuffer()); @@ -675,7 +676,7 @@ void OpenGLRenderer::RenderFramebuffer(const FramebufferInfo& info) renderLastFrame(); #endif - DrawOSD(false); + drawOSD(); frameRendered = true; renderVideoRouting(); restoreCurrentFramebuffer(); @@ -715,7 +716,7 @@ void writeFramebufferToVRAM() glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glcache.Disable(GL_BLEND); - drawQuad(gl.ofbo.framebuffer->getTexture(), false); + gl.quad->draw(gl.ofbo.framebuffer->getTexture(), false); } else { @@ -800,7 +801,7 @@ bool OpenGLRenderer::renderLastFrame() vertices = sverts; } glcache.Disable(GL_BLEND); - drawQuad(framebuffer->getTexture(), config::Rotate90, true, vertices); + gl.quad->draw(framebuffer->getTexture(), config::Rotate90, true, vertices); } else { @@ -859,7 +860,7 @@ bool OpenGLRenderer::GetLastFrame(std::vector& data, int& width, int& height }; vertices = &rvertices[0][0]; } - drawQuad(framebuffer->getTexture(), config::Rotate90, false, vertices); + gl.quad->draw(framebuffer->getTexture(), config::Rotate90, false, vertices); data.resize(width * height * 3); glPixelStorei(GL_PACK_ALIGNMENT, 1); @@ -987,7 +988,7 @@ static void drawVmuTexture(u8 vmuIndex, int width, int height) }; glcache.Enable(GL_BLEND); glcache.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - drawQuad(vmuTextureId[vmuIndex], false, false, vertices, color); + gl.quad->draw(vmuTextureId[vmuIndex], false, false, vertices, color); } static void updateLightGunTexture() @@ -1040,7 +1041,7 @@ static void drawGunCrosshair(u8 port, int width, int height) }; glcache.Enable(GL_BLEND); glcache.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - drawQuad(lightgunTextureId, false, false, vertices, color); + gl.quad->draw(lightgunTextureId, false, false, vertices, color); } void drawVmusAndCrosshairs(int width, int height) diff --git a/core/rend/gles/gles.cpp b/core/rend/gles/gles.cpp index c6807ca9d..ad9f4f8d7 100644 --- a/core/rend/gles/gles.cpp +++ b/core/rend/gles/gles.cpp @@ -1,5 +1,6 @@ #include "glcache.h" #include "gles.h" +#include "quad.h" #include "hw/pvr/ta.h" #ifndef LIBRETRO #include "ui/gui.h" @@ -7,7 +8,6 @@ #include "rend/gles/postprocess.h" #include "vmu_xhair.h" #endif -#include "rend/osd.h" #include "rend/TexCache.h" #include "rend/transform_matrix.h" #include "wsi/gl_context.h" @@ -395,42 +395,7 @@ void main() } )"; -const char* OSD_VertexShader = R"( -uniform highp vec4 scale; - -in highp vec4 in_pos; -in lowp vec4 in_base; -in mediump vec2 in_uv; - -out lowp vec4 vtx_base; -out mediump vec2 vtx_uv; - -void main() -{ - vtx_base = in_base; - vtx_uv = in_uv; - highp vec4 vpos = in_pos; - - vpos.w = 1.0; - vpos.z = vpos.w; - vpos.xy = vpos.xy * scale.xy - scale.zw; - gl_Position = vpos; -} -)"; - -const char* OSD_Shader = R"( -in lowp vec4 vtx_base; -in mediump vec2 vtx_uv; - -uniform sampler2D tex; -void main() -{ - gl_FragColor = vtx_base * texture(tex, vtx_uv); -} -)"; - void os_VideoRoutingTermGL(); -static void gl_free_osd_resources(); GLCache glcache; gl_ctx gl; @@ -499,7 +464,7 @@ void termGLCommon() #ifdef VIDEO_ROUTING os_VideoRoutingTermGL(); #endif - termQuad(); + gl.quad.reset(); // palette, fog glcache.DeleteTextures(1, &fogTextureId); @@ -509,7 +474,6 @@ void termGLCommon() // RTT gl.rtt.framebuffer.reset(); - gl_free_osd_resources(); gl.ofbo.framebuffer.reset(); glcache.DeleteTextures(1, &gl.dcfb.tex); gl.dcfb.tex = 0; @@ -890,66 +854,6 @@ bool CompilePipelineShader(PipelineShader* s) return true; } -void OSDVertexArray::defineVtxAttribs() -{ - glEnableVertexAttribArray(VERTEX_POS_ARRAY); - glVertexAttribPointer(VERTEX_POS_ARRAY, 2, GL_FLOAT, GL_FALSE, sizeof(OSDVertex), (void*)offsetof(OSDVertex, x)); - - glEnableVertexAttribArray(VERTEX_COL_BASE_ARRAY); - glVertexAttribPointer(VERTEX_COL_BASE_ARRAY, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(OSDVertex), (void*)offsetof(OSDVertex, r)); - - glEnableVertexAttribArray(VERTEX_UV_ARRAY); - glVertexAttribPointer(VERTEX_UV_ARRAY, 2, GL_FLOAT, GL_FALSE, sizeof(OSDVertex), (void*)offsetof(OSDVertex, u)); - - glDisableVertexAttribArray(VERTEX_COL_OFFS_ARRAY); -} - -#ifdef __ANDROID__ -static void gl_load_osd_resources() -{ - OpenGlSource vertexSource; - vertexSource.addSource(VertexCompatShader) - .addSource(OSD_VertexShader); - OpenGlSource fragmentSource; - fragmentSource.addSource(PixelCompatShader) - .addSource(OSD_Shader); - - gl.OSD_SHADER.program = gl_CompileAndLink(vertexSource.generate().c_str(), fragmentSource.generate().c_str()); - gl.OSD_SHADER.scale = glGetUniformLocation(gl.OSD_SHADER.program, "scale"); - glUniform1i(glGetUniformLocation(gl.OSD_SHADER.program, "tex"), 0); //bind osd texture to slot 0 - - if (gl.OSD_SHADER.osd_tex == 0) - { - int width, height; - u8 *image_data = loadOSDButtons(width, height); - //Now generate the OpenGL texture object - gl.OSD_SHADER.osd_tex = glcache.GenTexture(); - glcache.BindTexture(GL_TEXTURE_2D, gl.OSD_SHADER.osd_tex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (GLvoid *)image_data); - glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - - delete[] image_data; - } - gl.OSD_SHADER.geometry = std::make_unique(GL_ARRAY_BUFFER); -} -#endif - -static void gl_free_osd_resources() -{ - if (gl.OSD_SHADER.program != 0) - { - glcache.DeleteProgram(gl.OSD_SHADER.program); - gl.OSD_SHADER.program = 0; - } - - if (gl.OSD_SHADER.osd_tex != 0) { - glcache.DeleteTextures(1, &gl.OSD_SHADER.osd_tex); - gl.OSD_SHADER.osd_tex = 0; - } - gl.OSD_SHADER.geometry.reset(); - gl.OSD_SHADER.vao.term(); -} - static void create_modvol_shader() { if (gl.modvol_shader.program != 0) @@ -1000,7 +904,7 @@ static void gl_create_resources() gl.vbo.modvols = std::make_unique(GL_ARRAY_BUFFER); gl.vbo.idxs = std::make_unique(GL_ELEMENT_ARRAY_BUFFER); - initQuad(); + gl.quad = std::make_unique(); } GLuint gl_CompileShader(const char* shader,GLuint type); @@ -1119,68 +1023,12 @@ static void updatePaletteTexture(GLenum texture_slot) glActiveTexture(GL_TEXTURE0); } -void OpenGLRenderer::DrawOSD(bool clear_screen) +void OpenGLRenderer::drawOSD() { drawVmusAndCrosshairs(width, height); - #ifndef LIBRETRO gui_display_osd(); -#ifdef __ANDROID__ - if (gl.OSD_SHADER.osd_tex == 0) - gl_load_osd_resources(); - if (gl.OSD_SHADER.osd_tex != 0) - { - glcache.Disable(GL_SCISSOR_TEST); - glViewport(0, 0, settings.display.width, settings.display.height); - - if (clear_screen) - { - glcache.ClearColor(0.7f, 0.7f, 0.7f, 1.f); - glClear(GL_COLOR_BUFFER_BIT); - renderLastFrame(); - glViewport(0, 0, settings.display.width, settings.display.height); - } - - glcache.UseProgram(gl.OSD_SHADER.program); - - float scale_h = settings.display.height / 480.f; - float offs_x = (settings.display.width - scale_h * 640.f) / 2.f; - float scale[4]; - scale[0] = 2.f / (settings.display.width / scale_h); - scale[1]= -2.f / 480.f; - scale[2]= 1.f - 2.f * offs_x / settings.display.width; - scale[3]= -1.f; - glUniform4fv(gl.OSD_SHADER.scale, 1, scale); - - glActiveTexture(GL_TEXTURE0); - glcache.BindTexture(GL_TEXTURE_2D, gl.OSD_SHADER.osd_tex); - - glBindFramebuffer(GL_FRAMEBUFFER, gl.ofbo.origFbo); - - const std::vector& osdVertices = GetOSDVertices(); - gl.OSD_SHADER.geometry->update(osdVertices.data(), osdVertices.size() * sizeof(OSDVertex)); - gl.OSD_SHADER.vao.bind(gl.OSD_SHADER.geometry.get()); - - glcache.Enable(GL_BLEND); - glcache.Disable(GL_DEPTH_TEST); - glcache.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - glcache.DepthMask(false); - glcache.DepthFunc(GL_ALWAYS); - - glcache.Disable(GL_CULL_FACE); - int dfa = osdVertices.size() / 4; - - for (int i = 0; i < dfa; i++) - glDrawArrays(GL_TRIANGLE_STRIP, i * 4, 4); - - glCheck(); - if (clear_screen) - imguiDriver->setFrameRendered(); - } #endif -#endif - GlVertexArray::unbind(); } void OpenGLRenderer::Process(TA_context* ctx) @@ -1501,10 +1349,9 @@ bool OpenGLRenderer::Render() if (!config::EmulateFramebuffer) { frameRendered = true; - DrawOSD(false); + drawOSD(); renderVideoRouting(); } - restoreCurrentFramebuffer(); return true; diff --git a/core/rend/gles/gles.h b/core/rend/gles/gles.h index ce1bd6423..0a2134c1b 100755 --- a/core/rend/gles/gles.h +++ b/core/rend/gles/gles.h @@ -223,11 +223,7 @@ class ModvolVertexArray final : public GlVertexArray void defineVtxAttribs() override; }; -class OSDVertexArray final : public GlVertexArray -{ -protected: - void defineVtxAttribs() override; -}; +class GlQuadDrawer; struct gl_ctx { @@ -254,15 +250,6 @@ struct gl_ctx std::unordered_map shaders; - struct - { - GLuint program; - GLint scale; - OSDVertexArray vao; - std::unique_ptr geometry; - GLuint osd_tex; - } OSD_SHADER; - struct { MainVertexArray mainVAO; @@ -308,6 +295,7 @@ struct gl_ctx std::unique_ptr framebuffer; } videorouting; + std::unique_ptr quad; const char *gl_version; const char *glsl_version_header; int gl_major; @@ -521,8 +509,6 @@ struct OpenGLRenderer : Renderer } bool GetLastFrame(std::vector& data, int& width, int& height) override; - void DrawOSD(bool clear_screen) override; - BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override; bool Present() override @@ -558,6 +544,7 @@ struct OpenGLRenderer : Renderer bool renderLastFrame(); void renderVideoRouting(); + void drawOSD(); private: bool renderFrame(int width, int height); @@ -569,10 +556,6 @@ struct OpenGLRenderer : Renderer void initVideoRoutingFrameBuffer(); }; -void initQuad(); -void termQuad(); -void drawQuad(GLuint texId, bool rotate = false, bool swapY = false, const float *coords = nullptr, const float *color = nullptr); - extern const char* ShaderCompatSource; extern const char *VertexCompatShader; extern const char *PixelCompatShader; diff --git a/core/rend/gles/postprocess.cpp b/core/rend/gles/postprocess.cpp index f0c2a2357..3e5e023b1 100644 --- a/core/rend/gles/postprocess.cpp +++ b/core/rend/gles/postprocess.cpp @@ -20,6 +20,7 @@ #include "postprocess.h" #ifdef LIBRETRO +#include "quad.h" #include PostProcessor postProcessor; @@ -303,7 +304,7 @@ void PostProcessor::render(GLuint output_fbo) vertices[1] = vertices[11] = -1.f - gl.ofbo.shiftY * 2.f / framebuffer->getHeight(); vertices[6] = vertices[16] = vertices[1] + 2; glcache.Disable(GL_BLEND); - drawQuad(framebuffer->getTexture(), false, false, vertices); + gl.quad->draw(framebuffer->getTexture(), false, false, vertices); } else { diff --git a/core/rend/gles/quad.cpp b/core/rend/gles/quad.cpp index 8c6047e97..d331b703c 100644 --- a/core/rend/gles/quad.cpp +++ b/core/rend/gles/quad.cpp @@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ -#include "gles.h" +#include "quad.h" static const char* VertexShader = R"( in highp vec3 in_pos; @@ -46,68 +46,34 @@ void main() } )"; -class QuadVertexArray final : public GlVertexArray +GlQuadDrawer::GlQuadDrawer() { -protected: - void defineVtxAttribs() override + OpenGlSource fragmentShader; + fragmentShader.addSource(PixelCompatShader) + .addSource(FragmentShader); + OpenGlSource vertexShader; + vertexShader.addConstant("ROTATE", 0) + .addSource(VertexCompatShader) + .addSource(VertexShader); + + const std::string fragmentGlsl = fragmentShader.generate(); + shader = gl_CompileAndLink(vertexShader.generate().c_str(), fragmentGlsl.c_str()); + GLint tex = glGetUniformLocation(shader, "tex"); + glUniform1i(tex, 0); // texture 0 + tintUniform = glGetUniformLocation(shader, "tint"); + + vertexShader.setConstant("ROTATE", 1); + rot90shader = gl_CompileAndLink(vertexShader.generate().c_str(), fragmentGlsl.c_str()); + tex = glGetUniformLocation(rot90shader, "tex"); + glUniform1i(tex, 0); // texture 0 + rot90TintUniform = glGetUniformLocation(rot90shader, "tint"); + + quadIndexBuffer = std::make_unique(GL_ELEMENT_ARRAY_BUFFER, GL_STATIC_DRAW); + static const GLushort indices[] = { 0, 1, 2, 1, 3 }; + quadIndexBuffer->update(indices, sizeof(indices)); + + quadBuffer = std::make_unique(GL_ARRAY_BUFFER, GL_STATIC_DRAW); { - glEnableVertexAttribArray(VERTEX_POS_ARRAY); - glVertexAttribPointer(VERTEX_POS_ARRAY, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 5, (void*)0); - glEnableVertexAttribArray(VERTEX_UV_ARRAY); - glVertexAttribPointer(VERTEX_UV_ARRAY, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 5, (void*)(sizeof(float) * 3)); - glDisableVertexAttribArray(VERTEX_COL_BASE_ARRAY); - glDisableVertexAttribArray(VERTEX_COL_OFFS_ARRAY); - glDisableVertexAttribArray(VERTEX_COL_BASE1_ARRAY); - glDisableVertexAttribArray(VERTEX_COL_OFFS1_ARRAY); - glDisableVertexAttribArray(VERTEX_UV1_ARRAY); - } -}; - -static GLuint shader; -static GLint tintUniform; -static GLuint rot90shader; -static GLint rot90TintUniform; -static QuadVertexArray quadVertexArray; -static QuadVertexArray quadVertexArraySwapY; -static std::unique_ptr quadBuffer; -static std::unique_ptr quadBufferSwapY; -static std::unique_ptr quadIndexBuffer; -static std::unique_ptr quadBufferCustom; -static QuadVertexArray quadVertexArrayCustom; - -void initQuad() -{ - if (shader == 0) - { - OpenGlSource fragmentShader; - fragmentShader.addSource(PixelCompatShader) - .addSource(FragmentShader); - OpenGlSource vertexShader; - vertexShader.addConstant("ROTATE", 0) - .addSource(VertexCompatShader) - .addSource(VertexShader); - - const std::string fragmentGlsl = fragmentShader.generate(); - shader = gl_CompileAndLink(vertexShader.generate().c_str(), fragmentGlsl.c_str()); - GLint tex = glGetUniformLocation(shader, "tex"); - glUniform1i(tex, 0); // texture 0 - tintUniform = glGetUniformLocation(shader, "tint"); - - vertexShader.setConstant("ROTATE", 1); - rot90shader = gl_CompileAndLink(vertexShader.generate().c_str(), fragmentGlsl.c_str()); - tex = glGetUniformLocation(rot90shader, "tex"); - glUniform1i(tex, 0); // texture 0 - rot90TintUniform = glGetUniformLocation(rot90shader, "tint"); - } - if (quadIndexBuffer == nullptr) - { - quadIndexBuffer = std::make_unique(GL_ELEMENT_ARRAY_BUFFER, GL_STATIC_DRAW); - static const GLushort indices[] = { 0, 1, 2, 1, 3 }; - quadIndexBuffer->update(indices, sizeof(indices)); - } - if (quadBuffer == nullptr) - { - quadBuffer = std::make_unique(GL_ARRAY_BUFFER, GL_STATIC_DRAW); float vertices[4][5] = { { -1.f, 1.f, 1.f, 0.f, 1.f }, { -1.f, -1.f, 1.f, 0.f, 0.f }, @@ -116,9 +82,8 @@ void initQuad() }; quadBuffer->update(vertices, sizeof(vertices)); } - if (quadBufferSwapY == nullptr) + quadBufferSwapY = std::make_unique(GL_ARRAY_BUFFER, GL_STATIC_DRAW); { - quadBufferSwapY = std::make_unique(GL_ARRAY_BUFFER, GL_STATIC_DRAW); float vertices[4][5] = { { -1.f, 1.f, 1.f, 0.f, 0.f }, { -1.f, -1.f, 1.f, 0.f, 1.f }, @@ -127,12 +92,11 @@ void initQuad() }; quadBufferSwapY->update(vertices, sizeof(vertices)); } - if (quadBufferCustom == nullptr) - quadBufferCustom = std::make_unique(GL_ARRAY_BUFFER); + quadBufferCustom = std::make_unique(GL_ARRAY_BUFFER); glCheck(); } -void termQuad() +GlQuadDrawer::~GlQuadDrawer() { quadIndexBuffer.reset(); quadBuffer.reset(); @@ -149,8 +113,7 @@ void termQuad() rot90shader = 0; } -// coords is an optional array of 20 floats (4 vertices with x,y,z,u,v each) -void drawQuad(GLuint texId, bool rotate, bool swapY, const float *coords, const float *color) +void GlQuadDrawer::draw(GLuint texId, bool rotate, bool swapY, const float *coords, const float *color) { glcache.Disable(GL_SCISSOR_TEST); glcache.Disable(GL_DEPTH_TEST); diff --git a/core/rend/gles/quad.h b/core/rend/gles/quad.h new file mode 100644 index 000000000..d2c78cf3e --- /dev/null +++ b/core/rend/gles/quad.h @@ -0,0 +1,60 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#pragma once +#include "gles.h" + +class GlQuadDrawer +{ +public: + GlQuadDrawer(); + ~GlQuadDrawer(); + + // coords is an optional array of 20 floats (4 vertices with x,y,z,u,v each) + void draw(GLuint texId, bool rotate = false, bool swapY = false, const float *coords = nullptr, const float *color = nullptr); + +private: + class VertexArray final : public GlVertexArray + { + protected: + void defineVtxAttribs() override + { + glEnableVertexAttribArray(VERTEX_POS_ARRAY); + glVertexAttribPointer(VERTEX_POS_ARRAY, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 5, (void*)0); + glEnableVertexAttribArray(VERTEX_UV_ARRAY); + glVertexAttribPointer(VERTEX_UV_ARRAY, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 5, (void*)(sizeof(float) * 3)); + glDisableVertexAttribArray(VERTEX_COL_BASE_ARRAY); + glDisableVertexAttribArray(VERTEX_COL_OFFS_ARRAY); + glDisableVertexAttribArray(VERTEX_COL_BASE1_ARRAY); + glDisableVertexAttribArray(VERTEX_COL_OFFS1_ARRAY); + glDisableVertexAttribArray(VERTEX_UV1_ARRAY); + } + }; + + GLuint shader; + GLint tintUniform; + GLuint rot90shader; + GLint rot90TintUniform; + VertexArray quadVertexArray; + VertexArray quadVertexArraySwapY; + std::unique_ptr quadBuffer; + std::unique_ptr quadBufferSwapY; + std::unique_ptr quadIndexBuffer; + std::unique_ptr quadBufferCustom; + VertexArray quadVertexArrayCustom; +}; diff --git a/core/rend/osd.cpp b/core/rend/osd.cpp index b10c5493c..a88d8d7d6 100644 --- a/core/rend/osd.cpp +++ b/core/rend/osd.cpp @@ -15,166 +15,10 @@ along with reicast. If not, see . */ #include "osd.h" -#include "types.h" -#include "input/gamepad.h" -#include "input/gamepad_device.h" #include "stdclass.h" #ifdef LIBRETRO #include "vmu_xhair.h" #endif -#include "oslib/resources.h" - -#include - -#ifndef LIBRETRO - -#define OSD_TEX_W 512 -#define OSD_TEX_H 256 - -#ifdef __ANDROID__ -extern float vjoy_pos[15][8]; -#else - -static float vjoy_pos[15][8]= -{ - {24+0,24+64,64,64}, //LEFT - {24+64,24+0,64,64}, //UP - {24+128,24+64,64,64}, //RIGHT - {24+64,24+128,64,64}, //DOWN - - {440+0,280+64,64,64}, //X - {440+64,280+0,64,64}, //Y - {440+128,280+64,64,64}, //B - {440+64,280+128,64,64}, //A - - {320-32,360+32,64,64}, //Start - - {440,200,90,64}, //LT - {542,200,90,64}, //RT - - {-24,128+224,128,128}, //ANALOG_RING - {96,320,64,64}, //ANALOG_POINT - {320-32,24,64,64}, // FFORWARD - {1} // VJOY_VISIBLE -}; -#endif // !__ANDROID__ - -static const float vjoy_sz[2][15] = { - { 64,64,64,64, 64,64,64,64, 64, 90,90, 128, 64, 64 }, - { 64,64,64,64, 64,64,64,64, 64, 64,64, 128, 64, 64 }, -}; - -static std::vector osdVertices; - -void HideOSD() -{ - vjoy_pos[VJOY_VISIBLE][0] = 0; -} - -static void DrawButton(const float xy[8], u32 state) -{ - OSDVertex vtx; - - vtx.r = vtx.g = vtx.b = (0x7F - 0x40 * state / 255) * vjoy_pos[VJOY_VISIBLE][0]; - vtx.a = (100 - config::VirtualGamepadTransparency) * 0xff / 100 * vjoy_pos[VJOY_VISIBLE][4]; - vjoy_pos[VJOY_VISIBLE][4] += (vjoy_pos[VJOY_VISIBLE][0] - vjoy_pos[VJOY_VISIBLE][4]) / 2; - - vtx.x = xy[0]; vtx.y = xy[1]; - vtx.u = xy[4]; vtx.v = xy[5]; - osdVertices.push_back(vtx); - - vtx.x = xy[0] + xy[2]; vtx.y = xy[1]; - vtx.u = xy[6]; vtx.v = xy[5]; - osdVertices.push_back(vtx); - - vtx.x = xy[0]; vtx.y = xy[1] + xy[3]; - vtx.u = xy[4]; vtx.v = xy[7]; - osdVertices.push_back(vtx); - - vtx.x = xy[0] + xy[2]; vtx.y = xy[1] + xy[3]; - vtx.u = xy[6]; vtx.v = xy[7]; - osdVertices.push_back(vtx); -} - -static void DrawButton2(const float xy[8], bool state) -{ - DrawButton(xy, state ? 0 : 255); -} - -const std::vector& GetOSDVertices() -{ - osdVertices.reserve(std::size(vjoy_pos) * 4); - osdVertices.clear(); - DrawButton2(vjoy_pos[0], kcode[0] & DC_DPAD_LEFT); - DrawButton2(vjoy_pos[1], kcode[0] & DC_DPAD_UP); - DrawButton2(vjoy_pos[2], kcode[0] & DC_DPAD_RIGHT); - DrawButton2(vjoy_pos[3], kcode[0] & DC_DPAD_DOWN); - - DrawButton2(vjoy_pos[4], kcode[0] & (settings.platform.isConsole() ? DC_BTN_X : DC_BTN_C)); - DrawButton2(vjoy_pos[5], kcode[0] & (settings.platform.isConsole() ? DC_BTN_Y : DC_BTN_X)); - DrawButton2(vjoy_pos[6], kcode[0] & DC_BTN_B); - DrawButton2(vjoy_pos[7], kcode[0] & DC_BTN_A); - - DrawButton2(vjoy_pos[8], kcode[0] & DC_BTN_START); - - DrawButton(vjoy_pos[9], lt[0] >> 8); - - DrawButton(vjoy_pos[10], rt[0] >> 8); - - DrawButton2(vjoy_pos[11], true); - DrawButton2(vjoy_pos[12], false); - - DrawButton2(vjoy_pos[13], false); - - return osdVertices; -} - -static void setVjoyUV() -{ - float u = 0; - float v = 0; - - for (int i = 0; i < VJOY_VISIBLE; i++) - { - //umin, vmin, umax, vmax - vjoy_pos[i][4] = (u + 1) / OSD_TEX_W; - vjoy_pos[i][5] = 1.f - (v + 1) / OSD_TEX_H; - - vjoy_pos[i][6] = (u + vjoy_sz[0][i] - 1) / OSD_TEX_W; - vjoy_pos[i][7] = 1.f - (v + vjoy_sz[1][i] - 1) / OSD_TEX_H; - - u += vjoy_sz[0][i]; - if (u >= OSD_TEX_W) - { - u -= OSD_TEX_W; - v += vjoy_sz[1][i]; - } - } -} - -static OnLoad setVjoyUVOnLoad(&setVjoyUV); - -u8 *loadOSDButtons(int &width, int &height) -{ - int n; - stbi_set_flip_vertically_on_load(1); - - FILE *file = nowide::fopen(get_readonly_data_path("buttons.png").c_str(), "rb"); - u8 *image_data = nullptr; - if (file != nullptr) - { - image_data = stbi_load_from_file(file, &width, &height, &n, STBI_rgb_alpha); - std::fclose(file); - } - if (image_data == nullptr) - { - size_t size; - std::unique_ptr data = resource::load("picture/buttons.png", size); - image_data = stbi_load_from_memory(data.get(), size, &width, &height, &n, STBI_rgb_alpha); - } - return image_data; -} -#endif // LIBRETRO u32 vmu_lcd_data[8][48 * 32]; bool vmu_lcd_status[8]; diff --git a/core/rend/osd.h b/core/rend/osd.h index b3a819d6d..60a202797 100644 --- a/core/rend/osd.h +++ b/core/rend/osd.h @@ -18,21 +18,6 @@ #include "types.h" #include "cfg/option.h" -#include - -#define VJOY_VISIBLE 14 - -struct OSDVertex -{ - float x, y; - float u, v; - u8 r, g, b, a; -}; - -const std::vector& GetOSDVertices(); - -u8 *loadOSDButtons(int &width, int &height); -void HideOSD(); // VMUs extern u32 vmu_lcd_data[8][48 * 32]; diff --git a/core/rend/vulkan/pipeline.cpp b/core/rend/vulkan/pipeline.cpp index 90ce604be..e2330571c 100644 --- a/core/rend/vulkan/pipeline.cpp +++ b/core/rend/vulkan/pipeline.cpp @@ -20,7 +20,6 @@ */ #include "pipeline.h" #include "hw/pvr/Renderer_if.h" -#include "rend/osd.h" void PipelineManager::CreateModVolPipeline(ModVolMode mode, int cullMode, bool naomi2) { @@ -436,79 +435,3 @@ void PipelineManager::CreatePipeline(u32 listType, bool sortTriangles, const Pol pipelines[hash(listType, sortTriangles, &pp, gpuPalette, dithering)] = GetContext()->GetDevice().createGraphicsPipelineUnique(GetContext()->GetPipelineCache(), graphicsPipelineCreateInfo).value; } - -void OSDPipeline::CreatePipeline() -{ - // Vertex input state - static const vk::VertexInputBindingDescription vertexInputBindingDescription(0, sizeof(OSDVertex)); - static const std::array vertexInputAttributeDescriptions = { - vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32Sfloat, offsetof(OSDVertex, x)), // pos - vk::VertexInputAttributeDescription(1, 0, vk::Format::eR8G8B8A8Unorm, offsetof(OSDVertex, r)), // color - vk::VertexInputAttributeDescription(2, 0, vk::Format::eR32G32Sfloat, offsetof(OSDVertex, u)), // tex coord - }; - vk::PipelineVertexInputStateCreateInfo vertexInputStateCreateInfo( - vk::PipelineVertexInputStateCreateFlags(), - vertexInputBindingDescription, - vertexInputAttributeDescriptions); - - // Input assembly state - vk::PipelineInputAssemblyStateCreateInfo pipelineInputAssemblyStateCreateInfo(vk::PipelineInputAssemblyStateCreateFlags(), vk::PrimitiveTopology::eTriangleStrip); - - // Viewport and scissor states - vk::PipelineViewportStateCreateInfo pipelineViewportStateCreateInfo(vk::PipelineViewportStateCreateFlags(), 1, nullptr, 1, nullptr); - - // Rasterization and multisample states - vk::PipelineRasterizationStateCreateInfo pipelineRasterizationStateCreateInfo; - pipelineRasterizationStateCreateInfo.lineWidth = 1.0; - vk::PipelineMultisampleStateCreateInfo pipelineMultisampleStateCreateInfo; - - // Depth and stencil - vk::PipelineDepthStencilStateCreateInfo pipelineDepthStencilStateCreateInfo; - - // Color flags and blending - vk::PipelineColorBlendAttachmentState pipelineColorBlendAttachmentState( - true, // blendEnable - vk::BlendFactor::eSrcAlpha, // srcColorBlendFactor - vk::BlendFactor::eOneMinusSrcAlpha, // dstColorBlendFactor - vk::BlendOp::eAdd, // colorBlendOp - vk::BlendFactor::eSrcAlpha, // srcAlphaBlendFactor - vk::BlendFactor::eOneMinusSrcAlpha, // dstAlphaBlendFactor - vk::BlendOp::eAdd, // alphaBlendOp - vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG - | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA - ); - vk::PipelineColorBlendStateCreateInfo pipelineColorBlendStateCreateInfo - ( - vk::PipelineColorBlendStateCreateFlags(), // flags - false, // logicOpEnable - vk::LogicOp::eNoOp, // logicOp - pipelineColorBlendAttachmentState, // attachments - { { 1.0f, 1.0f, 1.0f, 1.0f } } // blendConstants - ); - - std::array dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; - vk::PipelineDynamicStateCreateInfo pipelineDynamicStateCreateInfo(vk::PipelineDynamicStateCreateFlags(), dynamicStates); - - std::array stages = { - vk::PipelineShaderStageCreateInfo(vk::PipelineShaderStageCreateFlags(), vk::ShaderStageFlagBits::eVertex, shaderManager->GetOSDVertexShader(), "main"), - vk::PipelineShaderStageCreateInfo(vk::PipelineShaderStageCreateFlags(), vk::ShaderStageFlagBits::eFragment, shaderManager->GetOSDFragmentShader(), "main"), - }; - vk::GraphicsPipelineCreateInfo graphicsPipelineCreateInfo - ( - vk::PipelineCreateFlags(), // flags - stages, // stages - &vertexInputStateCreateInfo, // pVertexInputState - &pipelineInputAssemblyStateCreateInfo, // pInputAssemblyState - nullptr, // pTessellationState - &pipelineViewportStateCreateInfo, // pViewportState - &pipelineRasterizationStateCreateInfo, // pRasterizationState - &pipelineMultisampleStateCreateInfo, // pMultisampleState - &pipelineDepthStencilStateCreateInfo, // pDepthStencilState - &pipelineColorBlendStateCreateInfo, // pColorBlendState - &pipelineDynamicStateCreateInfo, // pDynamicState - *pipelineLayout, // layout - renderPass // renderPass - ); - - pipeline = GetContext()->GetDevice().createGraphicsPipelineUnique(GetContext()->GetPipelineCache(), graphicsPipelineCreateInfo).value; -} diff --git a/core/rend/vulkan/pipeline.h b/core/rend/vulkan/pipeline.h index 5e5e32fb0..d3dfaa8a2 100644 --- a/core/rend/vulkan/pipeline.h +++ b/core/rend/vulkan/pipeline.h @@ -398,74 +398,3 @@ class RttPipelineManager : public PipelineManager vk::UniqueRenderPass rttRenderPass; bool renderToTextureBuffer = false; }; - -class OSDPipeline -{ -public: - void Init(ShaderManager *shaderManager, vk::ImageView imageView, vk::RenderPass renderPass) - { - this->shaderManager = shaderManager; - if (!pipelineLayout) - { - vk::DescriptorSetLayoutBinding binding(0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment); // texture - descSetLayout = GetContext()->GetDevice().createDescriptorSetLayoutUnique( - vk::DescriptorSetLayoutCreateInfo(vk::DescriptorSetLayoutCreateFlags(), binding)); - pipelineLayout = GetContext()->GetDevice().createPipelineLayoutUnique( - vk::PipelineLayoutCreateInfo(vk::PipelineLayoutCreateFlags(), descSetLayout.get())); - } - if (!sampler) - { - sampler = GetContext()->GetDevice().createSamplerUnique( - vk::SamplerCreateInfo(vk::SamplerCreateFlags(), vk::Filter::eLinear, vk::Filter::eLinear, - vk::SamplerMipmapMode::eLinear, vk::SamplerAddressMode::eClampToEdge, vk::SamplerAddressMode::eClampToEdge, - vk::SamplerAddressMode::eClampToEdge, 0.0f, false, 16.0f, false, - vk::CompareOp::eNever, 0.0f, vk::LodClampNone, vk::BorderColor::eFloatOpaqueBlack)); - } - if (this->renderPass != renderPass) - { - this->renderPass = renderPass; - pipeline.reset(); - } - if (!descriptorSet) - { - descriptorSet = std::move(GetContext()->GetDevice().allocateDescriptorSetsUnique( - vk::DescriptorSetAllocateInfo(GetContext()->GetDescriptorPool(), descSetLayout.get())).front()); - } - vk::DescriptorImageInfo imageInfo(*sampler, imageView, vk::ImageLayout::eShaderReadOnlyOptimal); - vk::WriteDescriptorSet writeDescriptorSet(*descriptorSet, 0, 0, vk::DescriptorType::eCombinedImageSampler, imageInfo); - GetContext()->GetDevice().updateDescriptorSets(writeDescriptorSet, nullptr); - } - - void Term() - { - descriptorSet.reset(); - pipeline.reset(); - sampler.reset(); - pipelineLayout.reset(); - descSetLayout.reset(); - } - - vk::Pipeline GetPipeline() - { - if (!pipeline) - CreatePipeline(); - return *pipeline; - } - - void BindDescriptorSets(vk::CommandBuffer cmdBuffer) const - { - cmdBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, descriptorSet.get(), nullptr); - } - -private: - VulkanContext *GetContext() const { return VulkanContext::Instance(); } - void CreatePipeline(); - - vk::RenderPass renderPass; - vk::UniquePipeline pipeline; - vk::UniqueSampler sampler; - vk::UniqueDescriptorSet descriptorSet; - vk::UniquePipelineLayout pipelineLayout; - vk::UniqueDescriptorSetLayout descSetLayout; - ShaderManager *shaderManager = nullptr; -}; diff --git a/core/rend/vulkan/shaders.cpp b/core/rend/vulkan/shaders.cpp index 3420f0e25..4f21f74f2 100644 --- a/core/rend/vulkan/shaders.cpp +++ b/core/rend/vulkan/shaders.cpp @@ -394,33 +394,6 @@ void main() } )"; -static const char OSDVertexShaderSource[] = R"( -layout (location = 0) in vec4 inPos; -layout (location = 1) in vec4 inColor; -layout (location = 2) in vec2 inUV; -layout (location = 0) out lowp vec4 outColor; -layout (location = 1) out mediump vec2 outUV; - -void main() -{ - outColor = inColor; - outUV = inUV; - gl_Position = inPos; -} -)"; - -static const char OSDFragmentShaderSource[] = R"( -layout (binding = 0) uniform sampler2D tex; -layout (location = 0) in lowp vec4 inColor; -layout (location = 1) in mediump vec2 inUV; -layout (location = 0) out vec4 FragColor; - -void main() -{ - FragColor = inColor * texture(tex, inUV); -} -)"; - extern const char N2LightShaderSource[] = R"( layout (std140, set = 1, binding = 2) uniform N2VertexShaderUniforms @@ -825,13 +798,3 @@ vk::UniqueShaderModule ShaderManager::compileQuadFragmentShader(bool ignoreTexAl .addSource(QuadFragmentShaderSource); return ShaderCompiler::Compile(vk::ShaderStageFlagBits::eFragment,src.generate()); } - -vk::UniqueShaderModule ShaderManager::compileOSDVertexShader() -{ - return ShaderCompiler::Compile(vk::ShaderStageFlagBits::eVertex, VulkanSource().addSource(OSDVertexShaderSource).generate()); -} - -vk::UniqueShaderModule ShaderManager::compileOSDFragmentShader() -{ - return ShaderCompiler::Compile(vk::ShaderStageFlagBits::eFragment, VulkanSource().addSource(OSDFragmentShaderSource).generate()); -} diff --git a/core/rend/vulkan/shaders.h b/core/rend/vulkan/shaders.h index 426081381..6ec5e019d 100644 --- a/core/rend/vulkan/shaders.h +++ b/core/rend/vulkan/shaders.h @@ -147,18 +147,6 @@ class ShaderManager return *quadFragmentShader; } } - vk::ShaderModule GetOSDVertexShader() - { - if (!osdVertexShader) - osdVertexShader = compileOSDVertexShader(); - return *osdVertexShader; - } - vk::ShaderModule GetOSDFragmentShader() - { - if (!osdFragmentShader) - osdFragmentShader = compileOSDFragmentShader(); - return *osdFragmentShader; - } void term() { @@ -171,8 +159,6 @@ class ShaderManager quadRotateVertexShader.reset(); quadFragmentShader.reset(); quadNoAlphaFragmentShader.reset(); - osdVertexShader.reset(); - osdFragmentShader.reset(); } private: @@ -192,8 +178,6 @@ class ShaderManager vk::UniqueShaderModule compileModVolFragmentShader(bool divPosZ); vk::UniqueShaderModule compileQuadVertexShader(bool rotate); vk::UniqueShaderModule compileQuadFragmentShader(bool ignoreTexAlpha); - vk::UniqueShaderModule compileOSDVertexShader(); - vk::UniqueShaderModule compileOSDFragmentShader(); std::map vertexShaders; std::map fragmentShaders; @@ -203,6 +187,4 @@ class ShaderManager vk::UniqueShaderModule quadRotateVertexShader; vk::UniqueShaderModule quadFragmentShader; vk::UniqueShaderModule quadNoAlphaFragmentShader; - vk::UniqueShaderModule osdVertexShader; - vk::UniqueShaderModule osdFragmentShader; }; diff --git a/core/rend/vulkan/vulkan_context.cpp b/core/rend/vulkan/vulkan_context.cpp index 304bd0c0b..557d6bb11 100644 --- a/core/rend/vulkan/vulkan_context.cpp +++ b/core/rend/vulkan/vulkan_context.cpp @@ -1059,7 +1059,6 @@ void VulkanContext::PresentFrame(vk::Image image, vk::ImageView imageView, const DrawOverlay(settings.display.uiScale, config::FloatVMUs, true); imguiDriver->renderDrawData(ImGui::GetDrawData(), false); - renderer->DrawOSD(false); EndFrame(overlayCmdBuffer); static_cast(renderer)->RenderVideoRouting(); diff --git a/core/rend/vulkan/vulkan_renderer.cpp b/core/rend/vulkan/vulkan_renderer.cpp index db4294db1..404f01993 100644 --- a/core/rend/vulkan/vulkan_renderer.cpp +++ b/core/rend/vulkan/vulkan_renderer.cpp @@ -22,38 +22,12 @@ #include "vulkan_renderer.h" #include "drawer.h" #include "hw/pvr/ta.h" -#include "rend/osd.h" #include "rend/transform_matrix.h" bool BaseVulkanRenderer::BaseInit(vk::RenderPass renderPass, int subpass) { texCommandPool.Init(); fbCommandPool.Init(); - -#if defined(__ANDROID__) && !defined(LIBRETRO) - if (!vjoyTexture) - { - int w, h; - u8 *image_data = loadOSDButtons(w, h); - texCommandPool.BeginFrame(); - vjoyTexture = std::make_unique(); - vjoyTexture->tex_type = TextureType::_8888; - vk::CommandBuffer cmdBuffer = texCommandPool.Allocate(); - cmdBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); - vjoyTexture->SetCommandBuffer(cmdBuffer); - vjoyTexture->UploadToGPU(w, h, image_data, false); - vjoyTexture->SetCommandBuffer(nullptr); - cmdBuffer.end(); - texCommandPool.EndFrame(); - delete [] image_data; - osdPipeline.Init(&shaderManager, vjoyTexture->GetImageView(), GetContext()->GetRenderPass()); - } - if (!osdBuffer) - { - osdBuffer = std::make_unique(sizeof(OSDVertex) * VJOY_VISIBLE * 4, - vk::BufferUsageFlagBits::eVertexBuffer); - } -#endif quadPipeline = std::make_unique(false, false); quadPipeline->Init(&shaderManager, renderPass, subpass); framebufferDrawer = std::make_unique(); @@ -71,9 +45,6 @@ void BaseVulkanRenderer::Term() #endif framebufferDrawer.reset(); quadPipeline.reset(); - osdBuffer.reset(); - osdPipeline.Term(); - vjoyTexture.reset(); textureCache.Clear(); fogTexture = nullptr; paletteTexture = nullptr; @@ -138,58 +109,6 @@ void BaseVulkanRenderer::ReInitOSD() { texCommandPool.Init(); fbCommandPool.Init(); -#if defined(__ANDROID__) && !defined(LIBRETRO) - osdPipeline.Init(&shaderManager, vjoyTexture->GetImageView(), GetContext()->GetRenderPass()); -#endif -} - -void BaseVulkanRenderer::DrawOSD(bool clear_screen) -{ -#ifndef LIBRETRO - if (!vjoyTexture) - return; - try { - if (clear_screen) - { - GetContext()->NewFrame(); - GetContext()->BeginRenderPass(); - GetContext()->PresentLastFrame(); - } - const float dc2s_scale_h = settings.display.height / 480.0f; - const float sidebarWidth = (settings.display.width - dc2s_scale_h * 640.0f) / 2; - - std::vector osdVertices = GetOSDVertices(); - const float x1 = 2.0f / (settings.display.width / dc2s_scale_h); - const float y1 = 2.0f / 480; - const float x2 = 1 - 2 * sidebarWidth / settings.display.width; - const float y2 = 1; - for (OSDVertex& vtx : osdVertices) - { - vtx.x = vtx.x * x1 - x2; - vtx.y = vtx.y * y1 - y2; - } - - const vk::CommandBuffer cmdBuffer = GetContext()->GetCurrentCommandBuffer(); - - static const float scopeColor[4] = { 0.0f, 1.0f, 0.0f, 1.0f }; - CommandBufferDebugScope _(cmdBuffer, "DrawOSD", scopeColor); - - cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, osdPipeline.GetPipeline()); - - osdPipeline.BindDescriptorSets(cmdBuffer); - const vk::Viewport viewport(0, 0, (float)settings.display.width, (float)settings.display.height, 0, 1.f); - cmdBuffer.setViewport(0, viewport); - const vk::Rect2D scissor({ 0, 0 }, { (u32)settings.display.width, (u32)settings.display.height }); - cmdBuffer.setScissor(0, scissor); - osdBuffer->upload((u32)(osdVertices.size() * sizeof(OSDVertex)), osdVertices.data()); - cmdBuffer.bindVertexBuffers(0, osdBuffer->buffer.get(), {0}); - for (u32 i = 0; i < (u32)osdVertices.size(); i += 4) - cmdBuffer.draw(4, 1, i, 0); - if (clear_screen) - GetContext()->EndFrame(); - } catch (const InvalidVulkanContext&) { - } -#endif } void BaseVulkanRenderer::RenderFramebuffer(const FramebufferInfo& info) diff --git a/core/rend/vulkan/vulkan_renderer.h b/core/rend/vulkan/vulkan_renderer.h index f22f1230d..0780d5799 100644 --- a/core/rend/vulkan/vulkan_renderer.h +++ b/core/rend/vulkan/vulkan_renderer.h @@ -38,7 +38,6 @@ class BaseVulkanRenderer : public Renderer BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override; void Process(TA_context* ctx) override; void ReInitOSD(); - void DrawOSD(bool clear_screen) override; void RenderFramebuffer(const FramebufferInfo& info) override; void RenderVideoRouting(); @@ -67,9 +66,6 @@ class BaseVulkanRenderer : public Renderer CommandPool texCommandPool; std::vector> framebufferTextures; int framebufferTexIndex = 0; - OSDPipeline osdPipeline; - std::unique_ptr vjoyTexture; - std::unique_ptr osdBuffer; TextureCache textureCache; vk::Extent2D viewport; vk::CommandBuffer texCommandBuffer; diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index c0e13180a..e6367e716 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -56,8 +56,8 @@ #include "sdl/sdl.h" #endif -#ifdef __ANDROID__ #include "gui_android.h" +#ifdef __ANDROID__ #if HOST_CPU == CPU_ARM64 && USE_VULKAN #include "rend/vulkan/adreno.h" #endif @@ -516,7 +516,7 @@ void gui_open_settings() { if (achievements::canPause()) { - HideOSD(); + vgamepad::hide(); try { emu.stop(); gui_setState(GuiState::Commands); @@ -532,6 +532,7 @@ void gui_open_settings() } else if (gui_state == GuiState::VJoyEdit) { + vgamepad::stopEditing(false); gui_setState(GuiState::VJoyEditCommands); } else if (gui_state == GuiState::Loading) @@ -1960,7 +1961,7 @@ static void gui_settings_controls(bool& maple_devices_changed) { if (ImGui::Button("Edit Layout")) { - vjoy_start_editing(); + vgamepad::startEditing(); gui_setState(GuiState::VJoyEdit); } } @@ -3504,7 +3505,7 @@ void gui_display_ui() FC_PROFILE_SCOPE; const LockGuard lock(guiMutex); - if (gui_state == GuiState::Closed || gui_state == GuiState::VJoyEdit) + if (gui_state == GuiState::Closed) return; if (gui_state == GuiState::Main) { @@ -3541,11 +3542,10 @@ void gui_display_ui() gui_display_onboarding(); break; case GuiState::VJoyEdit: + vgamepad::draw(); break; case GuiState::VJoyEditCommands: -#ifdef __ANDROID__ - gui_display_vjoy_commands(); -#endif + vgamepad::displayCommands(); break; case GuiState::SelectDisk: gui_display_content(); @@ -3604,8 +3604,6 @@ static std::string getFPSNotification() void gui_draw_osd() { - if (gui_state == GuiState::VJoyEdit) - return; gui_newFrame(); ImGui::NewFrame(); @@ -3640,14 +3638,13 @@ void gui_draw_osd() } if (!settings.raHardcoreMode) lua::overlay(); + vgamepad::draw(); ImGui::Render(); uiThreadRunner.execTasks(); } void gui_display_osd() { - if (gui_state == GuiState::VJoyEdit) - return; gui_draw_osd(); gui_endFrame(gui_is_open()); } diff --git a/core/ui/gui.h b/core/ui/gui.h index 80be4f5e6..a8026910a 100644 --- a/core/ui/gui.h +++ b/core/ui/gui.h @@ -74,7 +74,7 @@ void gui_setState(GuiState newState); static inline bool gui_is_open() { - return gui_state != GuiState::Closed && gui_state != GuiState::VJoyEdit; + return gui_state != GuiState::Closed; } static inline bool gui_is_content_browser() { diff --git a/core/ui/gui_android.cpp b/core/ui/gui_android.cpp index e727b38bb..58508b39c 100644 --- a/core/ui/gui_android.cpp +++ b/core/ui/gui_android.cpp @@ -20,16 +20,35 @@ #include "gui_android.h" #include "gui.h" - -#include "types.h" #include "stdclass.h" #include "imgui.h" #include "gui_util.h" +#include "rend/osd.h" +#include "imgui_driver.h" +#include "input/gamepad.h" +#include "input/gamepad_device.h" +#include "oslib/resources.h" +#include + +namespace vgamepad +{ -void vjoy_reset_editing(); -void vjoy_stop_editing(bool canceled); +struct Control +{ + float x; + float y; + float w; + float h; + float u0; + float v0; + float u1; + float v1; +}; +static Control Controls[_Count]; +static bool Visible = true; +static float AlphaTrans; -void gui_display_vjoy_commands() +void displayCommands() { centerNextWindow(); @@ -38,23 +57,174 @@ void gui_display_vjoy_commands() if (ImGui::Button("Save", ScaledVec2(150, 50))) { - vjoy_stop_editing(false); + stopEditing(false); gui_setState(GuiState::Settings); } ImGui::SameLine(); if (ImGui::Button("Reset", ScaledVec2(150, 50))) { - vjoy_reset_editing(); + resetEditing(); + startEditing(); gui_setState(GuiState::VJoyEdit); } ImGui::SameLine(); if (ImGui::Button("Cancel", ScaledVec2(150, 50))) { - vjoy_stop_editing(true); + stopEditing(true); gui_setState(GuiState::Settings); } ImGui::End(); } +static u8 *loadOSDButtons(int &width, int &height) +{ + int n; + stbi_set_flip_vertically_on_load(1); + + FILE *file = nowide::fopen(get_readonly_data_path("buttons.png").c_str(), "rb"); + u8 *image_data = nullptr; + if (file != nullptr) + { + image_data = stbi_load_from_file(file, &width, &height, &n, STBI_rgb_alpha); + std::fclose(file); + } + if (image_data == nullptr) + { + size_t size; + std::unique_ptr data = resource::load("picture/buttons.png", size); + image_data = stbi_load_from_memory(data.get(), size, &width, &height, &n, STBI_rgb_alpha); + } + return image_data; +} + +class ImguiVGamepadTexture : public ImguiTexture +{ +public: + ImTextureID getId() override + { + ImTextureID id = imguiDriver->getTexture(PATH); + if (id == ImTextureID()) + { + int width, height; + u8 *imgData = loadOSDButtons(width, height); + if (imgData != nullptr) + { + try { + id = imguiDriver->updateTextureAndAspectRatio(PATH, imgData, width, height, nearestSampling); + } catch (...) { + // vulkan can throw during resizing + } + free(imgData); + } + } + return id; + } + +private: + static constexpr char const *PATH = "picture/buttons.png"; +}; + +constexpr float vjoy_sz[2][14] = { + // L U R D X Y B A St LT RT Ana Stck FF + { 64,64,64,64, 64,64,64,64, 64, 90,90, 128, 64, 64 }, + { 64,64,64,64, 64,64,64,64, 64, 64,64, 128, 64, 64 }, +}; + +constexpr float OSD_TEX_W = 512.f; +constexpr float OSD_TEX_H = 256.f; + +static void setUV() +{ + float u = 0; + float v = 0; + int i = 0; + + for (auto& control : Controls) + { + control.u0 = (u + 1) / OSD_TEX_W; + control.v0 = 1.f - (v + 1) / OSD_TEX_H; + control.u1 = (u + vjoy_sz[0][i] - 1) / OSD_TEX_W; + control.v1 = 1.f - (v + vjoy_sz[1][i] - 1) / OSD_TEX_H; + + u += vjoy_sz[0][i]; + if (u >= OSD_TEX_W) { + u -= OSD_TEX_W; + v += vjoy_sz[1][i]; + } + i++; + } +} +static OnLoad _(&setUV); + +void show() { + Visible = true; +} + +void hide() { + Visible = false; +} + +void setPosition(ControlId id, float x, float y, float w, float h) +{ + verify(id >= 0 && id < _Count); + auto& control = Controls[id]; + control.x = x; + control.y = y; + control.w = w; + control.h = h; +} + +static void drawButtonDim(ImDrawList *drawList, const Control& control, int state) +{ + ImVec2 pos(control.x, control.y); + ImVec2 size(control.w, control.h); + ImVec2 uv0(control.u0, control.v0); + ImVec2 uv1(control.u1, control.v1); + float scale_h = settings.display.height / 480.f; + float offs_x = (settings.display.width - scale_h * 640.f) / 2.f; + pos *= scale_h; + size *= scale_h; + pos.x += offs_x; + + float col = (0.5f - 0.25f * state / 255) * (float)Visible; + float alpha = (100.f - config::VirtualGamepadTransparency) / 100.f * AlphaTrans; + AlphaTrans += ((float)Visible - AlphaTrans) / 2; + ImVec4 color(col, col, col, alpha); + + ImguiVGamepadTexture tex; + tex.draw(drawList, pos, size, uv0, uv1, color); +} + +static void drawButton(ImDrawList *drawList, const Control& control, bool state) { + drawButtonDim(drawList, control, state ? 0 : 255); +} + +void draw() +{ + ImDrawList *drawList = ImGui::GetForegroundDrawList(); + drawButton(drawList, Controls[Left], kcode[0] & DC_DPAD_LEFT); + drawButton(drawList, Controls[Up], kcode[0] & DC_DPAD_UP); + drawButton(drawList, Controls[Right], kcode[0] & DC_DPAD_RIGHT); + drawButton(drawList, Controls[Down], kcode[0] & DC_DPAD_DOWN); + + drawButton(drawList, Controls[X], kcode[0] & (settings.platform.isConsole() ? DC_BTN_X : DC_BTN_C)); + drawButton(drawList, Controls[Y], kcode[0] & (settings.platform.isConsole() ? DC_BTN_Y : DC_BTN_X)); + drawButton(drawList, Controls[B], kcode[0] & DC_BTN_B); + drawButton(drawList, Controls[A], kcode[0] & DC_BTN_A); + + drawButton(drawList, Controls[Start], kcode[0] & DC_BTN_START); + + drawButtonDim(drawList, Controls[LeftTrigger], lt[0] >> 8); + + drawButtonDim(drawList, Controls[RightTrigger], rt[0] >> 8); + + drawButton(drawList, Controls[AnalogArea], true); + drawButton(drawList, Controls[AnalogStick], false); + + drawButton(drawList, Controls[FastForward], false); +} + +} // namespace vgamepad + #endif // __ANDROID__ diff --git a/core/ui/gui_android.h b/core/ui/gui_android.h index 68e5cc074..ead3478cb 100644 --- a/core/ui/gui_android.h +++ b/core/ui/gui_android.h @@ -17,6 +17,51 @@ along with reicast. If not, see . */ #pragma once +#include "types.h" -void gui_display_vjoy_commands(); -void vjoy_start_editing(); +namespace vgamepad +{ +enum ControlId +{ + Left, + Up, + Right, + Down, + X, + Y, + B, + A, + Start, + LeftTrigger, + RightTrigger, + AnalogArea, + AnalogStick, + FastForward, + + _Count +}; + +#ifdef __ANDROID__ + +void setPosition(ControlId id, float x, float y, float w, float h); +void show(); +void hide(); +void draw(); +void startEditing(); +void stopEditing(bool canceled); +void resetEditing(); +void displayCommands(); + +#else + +void setPosition(ControlId id, float x, float y, float w, float h) {} +void show() {} +void hide() {} +void draw() {} +void startEditing() {} +void stopEditing(bool canceled) {} +void resetEditing() {} +void displayCommands() {} + +#endif +} // namespace vgamepad diff --git a/core/ui/gui_util.cpp b/core/ui/gui_util.cpp index 83562dd96..ec60d6a67 100644 --- a/core/ui/gui_util.cpp +++ b/core/ui/gui_util.cpp @@ -751,6 +751,16 @@ void ImguiTexture::draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& s drawList->AddImage(id, pos, pos + size, uv0, uv1, col); } +void ImguiTexture::draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, + const ImVec2& uv0, const ImVec2& uv1, const ImVec4& color) +{ + ImTextureID id = getId(); + if (id == ImTextureID{}) + return; + u32 col = ImGui::ColorConvertFloat4ToU32(color); + drawList->AddImage(id, pos, pos + size, uv0, uv1, col); +} + bool ImguiTexture::button(const char* str_id, const ImVec2& image_size, const std::string& title, const ImVec4& bg_col, const ImVec4& tint_col) { diff --git a/core/ui/gui_util.h b/core/ui/gui_util.h index 7fec8ebae..baadb4466 100644 --- a/core/ui/gui_util.h +++ b/core/ui/gui_util.h @@ -202,6 +202,8 @@ class ImguiTexture void draw(const ImVec2& size, const ImVec4& tint_col = ImVec4(1, 1, 1, 1), const ImVec4& border_col = ImVec4(0, 0, 0, 0)); void draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha = 1.f); + void draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, + const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& color = ImVec4(1, 1, 1, 1)); bool button(const char* str_id, const ImVec2& image_size, const std::string& title = {}, const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); diff --git a/core/ui/mainui.cpp b/core/ui/mainui.cpp index 209afaae9..4b1ad55b5 100644 --- a/core/ui/mainui.cpp +++ b/core/ui/mainui.cpp @@ -41,12 +41,9 @@ bool mainui_rend_frame() os_DoEvents(); os_UpdateInputState(); - if (gui_is_open() || gui_state == GuiState::VJoyEdit) + if (gui_is_open()) { gui_display_ui(); - // TODO refactor android vjoy out of renderer - if (gui_state == GuiState::VJoyEdit && renderer != nullptr) - renderer->DrawOSD(true); #ifndef TARGET_IPHONE std::this_thread::sleep_for(std::chrono::milliseconds(16)); #endif diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java index 37d676ad6..476694958 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java @@ -331,14 +331,13 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyDown(int keyCode, KeyEvent event) { if (event.getRepeatCount() == 0) { if (keyCode == KeyEvent.KEYCODE_BACK) { - if (!JNIdc.guiIsOpen()) { - showMenu(); - return true; - } - else if (JNIdc.guiIsContentBrowser()) { + if (JNIdc.guiIsContentBrowser()) { finish(); - return true; } + else { + showMenu(); + } + return true; } if (InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), keyCode, true)) return true; diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java index 1cb336993..9369b936f 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java @@ -64,7 +64,7 @@ public boolean isSurfaceReady() { // Called from native code private void VJoyStartEditing() { vjoy_d_cached = VJoy.readCustomVjoyValues(getApplicationContext()); - JNIdc.show_osd(); + JNIdc.showVirtualGamepad(); mView.setEditVjoyMode(true); } // Called from native code diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java index df5133fb5..45aecceea 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java @@ -31,8 +31,6 @@ public final class JNIdc public static native boolean guiIsContentBrowser(); public static native void guiSetInsets(int left, int right, int top, int bottom); - public static void show_osd() { - JNIdc.vjoy(14, 1, 0, 0, 0); - } - public static native void hideOsd(); + public static native void showVirtualGamepad(); + public static native void hideVirtualGamepad(); } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java index 1b23cf0f0..aaebc99dd 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java @@ -20,10 +20,10 @@ public class VirtualJoystickDelegate { private ScaleGestureDetector scaleGestureDetector; private Handler handler = new Handler(); - private Runnable hideOsdRunnable = new Runnable() { + private Runnable hideVGamepadRunnable = new Runnable() { @Override public void run() { - JNIdc.hideOsd(); + JNIdc.hideVirtualGamepad(); } }; @@ -169,10 +169,10 @@ public boolean onTouchEvent(MotionEvent event, int width, int height) if ((event.getSource() & InputDevice.SOURCE_TOUCHSCREEN) != InputDevice.SOURCE_TOUCHSCREEN) // Ignore real mice, trackballs, etc. return false; - JNIdc.show_osd(); - this.handler.removeCallbacks(hideOsdRunnable); + JNIdc.showVirtualGamepad(); + this.handler.removeCallbacks(hideVGamepadRunnable); if (!editVjoyMode) - this.handler.postDelayed(hideOsdRunnable, 10000); + this.handler.postDelayed(hideVGamepadRunnable, 10000); scaleGestureDetector.onTouchEvent(event); @@ -186,7 +186,7 @@ public boolean onTouchEvent(MotionEvent event, int width, int height) int aid = event.getActionMasked(); int pid = event.getActionIndex(); - if (!JNIdc.guiIsOpen()) { + if (!JNIdc.guiIsOpen() || editVjoyMode) { if (editVjoyMode && selectedVjoyElement != VJoy.ELEM_NONE && aid == MotionEvent.ACTION_MOVE && !scaleGestureDetector.isInProgress()) { float x = (event.getX() - tx) / scl; float y = (event.getY() - ty) / scl; @@ -360,8 +360,8 @@ else if (event.getPointerId(event.getActionIndex())==rt_id) int joyy = get_anal(11, 1); InputDeviceManager.getInstance().virtualGamepadEvent(rv, joyx, joyy, left_trigger, right_trigger, fastForward); // Only register the mouse event if no virtual gamepad button is down - if ((!editVjoyMode && rv == 0xFFFFFFFF && left_trigger == 0 && right_trigger == 0 && joyx == 0 && joyy == 0 && !fastForward) - || JNIdc.guiIsOpen()) + if (!editVjoyMode && ((rv == 0xFFFFFFFF && left_trigger == 0 && right_trigger == 0 && joyx == 0 && joyy == 0 && !fastForward) + || JNIdc.guiIsOpen())) InputDeviceManager.getInstance().mouseEvent(mouse_pos[0], mouse_pos[1], mouse_btns); return(true); } @@ -370,7 +370,7 @@ public void setEditVjoyMode(boolean editVjoyMode) { this.editVjoyMode = editVjoyMode; selectedVjoyElement = -1; if (editVjoyMode) - this.handler.removeCallbacks(hideOsdRunnable); + this.handler.removeCallbacks(hideVGamepadRunnable); resetEditMode(); } diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index 0772ed84b..d9f4bdcbb 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -1,12 +1,12 @@ #include "types.h" #include "hw/maple/maple_cfg.h" -#include "rend/osd.h" #include "hw/maple/maple_devs.h" #include "hw/maple/maple_if.h" #include "hw/naomi/naomi_cart.h" #include "audio/audiostream.h" #include "imgread/common.h" #include "ui/gui.h" +#include "ui/gui_android.h" #include "rend/osd.h" #include "cfg/cfg.h" #include "log/LogManager.h" @@ -57,8 +57,6 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_screenChar std::shared_ptr mouse; std::shared_ptr keyboard; -float vjoy_pos[15][8]; - static bool game_started; //stuff for saving prefs @@ -356,18 +354,17 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_rendinitNa extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_vjoy(JNIEnv * env, jobject obj,int id,float x, float y, float w, float h) { - if (id < std::size(vjoy_pos)) - { - vjoy_pos[id][0] = x; - vjoy_pos[id][1] = y; - vjoy_pos[id][2] = w; - vjoy_pos[id][3] = h; - } + vgamepad::setPosition(static_cast(id), x, y, w, h); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_showVirtualGamepad(JNIEnv * env, jobject obj) +{ + vgamepad::show(); } -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_hideOsd(JNIEnv * env, jobject obj) +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_hideVirtualGamepad(JNIEnv * env, jobject obj) { - HideOSD(); + vgamepad::hide(); } extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_getControllers(JNIEnv *env, jobject obj, jintArray controllers, jobjectArray peripherals) @@ -601,21 +598,21 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_BaseGLActivity_regis } } -void vjoy_start_editing() +namespace vgamepad { + +void startEditing() { jni::env()->CallVoidMethod(g_activity, VJoyStartEditingMID); } - -void vjoy_reset_editing() -{ +void resetEditing() { jni::env()->CallVoidMethod(g_activity, VJoyResetEditingMID); } - -void vjoy_stop_editing(bool canceled) -{ +void stopEditing(bool canceled) { jni::env()->CallVoidMethod(g_activity, VJoyStopEditingMID, canceled); } +} + void enableNetworkBroadcast(bool enable) { JNIEnv *env = jni::env(); diff --git a/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h b/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h index c637e3c44..2208b741c 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h +++ b/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h @@ -250,7 +250,7 @@ class AndroidGamepadDevice : public GamepadDevice void virtual_gamepad_event(int kcode, int joyx, int joyy, int lt, int rt, bool fastForward) { // No virtual gamepad when the GUI is open: touch events only - if (gui_is_open()) + if (gui_is_open() && gui_state != GuiState::VJoyEdit) { kcode = 0xffffffff; joyx = joyy = rt = lt = 0; From d0584b86529a989d77b8615faa33a5c450ad03f5 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 23 Nov 2024 13:47:19 +0100 Subject: [PATCH 27/81] vk: defer deleting framebuffers in ScreenDrawer and lr context Defer deleting framebuffers and attachments in ScreenDrawer and lr context when resizing. Fixes crash when starting 240pSuite on the lr core. --- core/rend/vulkan/drawer.cpp | 11 ++++++++--- core/rend/vulkan/vk_context_lr.cpp | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/rend/vulkan/drawer.cpp b/core/rend/vulkan/drawer.cpp index fd6220bc6..02fd05337 100644 --- a/core/rend/vulkan/drawer.cpp +++ b/core/rend/vulkan/drawer.cpp @@ -639,9 +639,14 @@ void ScreenDrawer::Init(SamplerManager *samplerManager, ShaderManager *shaderMan this->shaderManager = shaderManager; if (this->viewport != viewport) { - framebuffers.clear(); - colorAttachments.clear(); - depthAttachment.reset(); + if (!framebuffers.empty()) { + verify(commandPool != nullptr); + commandPool->addToFlight(new Deleter(std::move(framebuffers))); + } + if (!colorAttachments.empty()) + commandPool->addToFlight(new Deleter(std::move(colorAttachments))); + if (depthAttachment) + commandPool->addToFlight(new Deleter(depthAttachment.release())); transitionNeeded.clear(); clearNeeded.clear(); } diff --git a/core/rend/vulkan/vk_context_lr.cpp b/core/rend/vulkan/vk_context_lr.cpp index 2fd8e1b0f..c02c6b773 100644 --- a/core/rend/vulkan/vk_context_lr.cpp +++ b/core/rend/vulkan/vk_context_lr.cpp @@ -433,8 +433,8 @@ void VulkanContext::beginFrame(vk::Extent2D extent) vk::Extent2D caExtent = colorAttachments[currentImage]->getExtent(); if (extent != caExtent) { - colorAttachments[currentImage].reset(); - framebuffers[currentImage].reset(); + addToFlight(new Deleter(std::move(colorAttachments[currentImage]))); + addToFlight(new Deleter(std::move(framebuffers[currentImage]))); } } commandPool.BeginFrame(); From a9fa33c17a0a31622a77ddf35256ca9f8d2ea920 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 26 Nov 2024 10:03:33 +0100 Subject: [PATCH 28/81] gl: test glBlitFramebuffer compliance and disable its use if failing --- core/rend/gles/gldraw.cpp | 4 +- core/rend/gles/gles.cpp | 13 +++++++ core/rend/gles/gles.h | 1 + core/rend/gles/gltex.cpp | 67 ++++++++++++++++++++++++++++++++++ core/rend/gles/postprocess.cpp | 2 +- 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/core/rend/gles/gldraw.cpp b/core/rend/gles/gldraw.cpp index ded6e531d..0cd253cc7 100644 --- a/core/rend/gles/gldraw.cpp +++ b/core/rend/gles/gldraw.cpp @@ -705,7 +705,7 @@ void writeFramebufferToVRAM() if (gl.fbscaling.framebuffer == nullptr) gl.fbscaling.framebuffer = std::make_unique(scaledW, scaledH); - if (gl.gl_major < 3) + if (gl.bogusBlitFramebuffer) { gl.fbscaling.framebuffer->bind(); glViewport(0, 0, scaledW, scaledH); @@ -777,7 +777,7 @@ bool OpenGLRenderer::renderLastFrame() else dx = (int)roundf(settings.display.width * (1 - renderAR / screenAR) / 2.f); - if (gl.gl_major < 3 || config::Rotate90) + if (gl.bogusBlitFramebuffer || config::Rotate90) { glViewport(dx, dy, settings.display.width - dx * 2, settings.display.height - dy * 2); glBindFramebuffer(GL_FRAMEBUFFER, gl.ofbo.origFbo); diff --git a/core/rend/gles/gles.cpp b/core/rend/gles/gles.cpp index ad9f4f8d7..eabcdcca1 100644 --- a/core/rend/gles/gles.cpp +++ b/core/rend/gles/gles.cpp @@ -498,6 +498,8 @@ static void gles_term() gl_delete_shaders(); } +bool testBlitFramebuffer(); + void findGLVersion() { gl.index_type = GL_UNSIGNED_INT; @@ -602,6 +604,17 @@ void findGLVersion() NOTICE_LOG(RENDERER, "Vendor '%s' Renderer '%s' Version '%s'", vendor, renderer, glGetString(GL_VERSION)); while (glGetError() != GL_NO_ERROR) ; + gl.bogusBlitFramebuffer = true; // not supported in GL/GLES 2 +#ifndef GLES2 + if (gl.gl_major >= 3) + { + gl.bogusBlitFramebuffer = !testBlitFramebuffer(); + if (gl.bogusBlitFramebuffer) + WARN_LOG(RENDERER, "glBlitFramebuffer is bogus. Using quad drawer instead"); + else + NOTICE_LOG(RENDERER, "glBlitFramebuffer test successful"); + } +#endif } struct ShaderUniforms_t ShaderUniforms; diff --git a/core/rend/gles/gles.h b/core/rend/gles/gles.h index 0a2134c1b..3a887feea 100755 --- a/core/rend/gles/gles.h +++ b/core/rend/gles/gles.h @@ -311,6 +311,7 @@ struct gl_ctx bool border_clamp_supported; bool prim_restart_supported; bool prim_restart_fixed_supported; + bool bogusBlitFramebuffer; size_t get_index_size() { return index_type == GL_UNSIGNED_INT ? sizeof(u32) : sizeof(u16); } }; diff --git a/core/rend/gles/gltex.cpp b/core/rend/gles/gltex.cpp index e71e8992f..b7c10e182 100644 --- a/core/rend/gles/gltex.cpp +++ b/core/rend/gles/gltex.cpp @@ -459,3 +459,70 @@ GlFramebuffer::~GlFramebuffer() glcache.DeleteTextures(1, &texture); glDeleteRenderbuffers(1, &colorBuffer); } + +bool testBlitFramebuffer() +{ +#ifdef GLES2 + return false; +#else + GLint ofbo = 0; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint *)&ofbo); + + GLuint texture = glcache.GenTexture(); + glcache.BindTexture(GL_TEXTURE_2D, texture); + + u32 data[32 * 32]; + // Lower half is red + for (int i = 0; i < 16 * 32; i++) + data[i] = 0xFF0000FF; + // Upper half is green + for (int i = 16 * 32; i < 32 * 32; i++) + data[i] = 0xFF00FF00; // green + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 32, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + GlFramebuffer src(32, 32, false, texture); + + GlFramebuffer dest(32, 64, false, true); + + src.bind(GL_READ_FRAMEBUFFER); + GLenum error = glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); + if (error != GL_FRAMEBUFFER_COMPLETE) { + WARN_LOG(RENDERER, "testBlitFramebuffer: Source framebuffer error %x", error); + return false; + } + dest.bind(GL_DRAW_FRAMEBUFFER); + error = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER); + if (error != GL_FRAMEBUFFER_COMPLETE) { + WARN_LOG(RENDERER, "testBlitFramebuffer: Destination framebuffer error %x", error); + return false; + } + + glcache.Disable(GL_SCISSOR_TEST); + glcache.ClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + // Apple A8X chokes on negative coordinates + // Many mobile GPUs don't support dstY0 > dstY1 and the resulting image is flipped vertically + glBlitFramebuffer(0, -1, 32, 31, 0, 64, 32, 0, GL_COLOR_BUFFER_BIT, GL_NEAREST); + + u32 outdata[32 * 64]; + dest.bind(GL_READ_FRAMEBUFFER); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(0, 0, 32, 64, GL_RGBA, GL_UNSIGNED_BYTE, outdata); + glBindFramebuffer(GL_FRAMEBUFFER, ofbo); + error = glGetError(); + if (error != GL_NO_ERROR) { + WARN_LOG(RENDERER, "testBlitFramebuffer: OpenGL error %x", error); + return false; + } + // Now lower half should be green (except last line due to srcY0 == -1) + if (outdata[32 * 2] != 0xFF00FF00) { // green + WARN_LOG(RENDERER, "testBlitFramebuffer: Expected 0xFF00FF00 but was %08x", outdata[0]); + return false; + } + // And upper half should be red + if (outdata[31 * 64 - 1] != 0xFF0000FF) { // red + WARN_LOG(RENDERER, "testBlitFramebuffer: Expected 0xFF0000FF but was %08x", outdata[32 * 64 - 1]); + return false; + } + return true; +#endif +} diff --git a/core/rend/gles/postprocess.cpp b/core/rend/gles/postprocess.cpp index 3e5e023b1..d6029d324 100644 --- a/core/rend/gles/postprocess.cpp +++ b/core/rend/gles/postprocess.cpp @@ -285,7 +285,7 @@ void PostProcessor::render(GLuint output_fbo) if (!config::PowerVR2Filter) { // Just handle shifting and Y flipping - if (gl.gl_major < 3) + if (gl.bogusBlitFramebuffer) { glBindFramebuffer(GL_FRAMEBUFFER, output_fbo); glViewport(0, 0, framebuffer->getWidth(), framebuffer->getHeight()); From 516982c66cbc40013d8e7551bb0557ae3ca98ddc Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 26 Nov 2024 10:46:02 +0100 Subject: [PATCH 29/81] ios: allow virtual gamepad layout customization Use same display code as android. Support custom buttons.png. Allow layout customization as in android. Save layout in emu.cfg. Allow transparency setting. Issue #990 --- CMakeLists.txt | 7 +- core/cfg/cfg.cpp | 9 + core/cfg/cfg.h | 2 + core/cfg/ini.cpp | 21 ++ core/cfg/ini.h | 3 + core/cfg/option.h | 10 +- core/ui/gui.cpp | 14 +- core/ui/gui_android.cpp | 349 ++++++++++++++++-- core/ui/gui_android.h | 41 +- .../flycast/src/main/jni/src/Android.cpp | 3 + .../emulator/EditPadViewController.h | 31 ++ .../emulator/EditPadViewController.mm | 131 +++++++ .../emulator/EditPadViewController.xib | 46 +++ .../emulator/FlycastViewController.mm | 16 + .../ABXYPad.imageset/ABXYPad.png | Bin 19907 -> 0 bytes .../ABXYPad.imageset/ABXYPad@2x.png | Bin 60578 -> 0 bytes .../ABXYPad.imageset/Contents.json | 22 -- .../DPad.imageset/Contents.json | 22 -- .../Images.xcassets/DPad.imageset/DPad.png | Bin 6122 -> 0 bytes .../Images.xcassets/DPad.imageset/DPad@2x.png | Bin 14267 -> 0 bytes .../JoystickBackground.imageset/Contents.json | 22 -- .../JoystickBackground.png | Bin 8439 -> 0 bytes .../JoystickBackground@2x.png | Bin 14384 -> 0 bytes .../JoystickButton.imageset/Contents.json | 22 -- .../JoystickButton.png | Bin 4434 -> 0 bytes .../JoystickButton@2x.png | Bin 6398 -> 0 bytes .../LTrigger.imageset/Contents.json | 22 -- .../LTrigger.imageset/LTrigger.png | Bin 3226 -> 0 bytes .../LTrigger.imageset/LTrigger@2x.png | Bin 4056 -> 0 bytes .../RTrigger.imageset/Contents.json | 22 -- .../RTrigger.imageset/RTrigger.png | Bin 3488 -> 0 bytes .../RTrigger.imageset/RTrigger@2x.png | Bin 4302 -> 0 bytes .../Start.imageset/Contents.json | 22 -- .../Images.xcassets/Start.imageset/Start.png | Bin 3914 -> 0 bytes .../Start.imageset/Start@2x.png | Bin 6510 -> 0 bytes .../emulator-ios/emulator/PadViewController.h | 5 - .../emulator/PadViewController.mm | 139 ++++--- .../emulator/PadViewController.xib | 199 ---------- .../apple/emulator-ios/emulator/ios_gamepad.h | 75 ++-- 39 files changed, 746 insertions(+), 509 deletions(-) create mode 100644 shell/apple/emulator-ios/emulator/EditPadViewController.h create mode 100644 shell/apple/emulator-ios/emulator/EditPadViewController.mm create mode 100644 shell/apple/emulator-ios/emulator/EditPadViewController.xib delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/ABXYPad.imageset/ABXYPad.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/ABXYPad.imageset/ABXYPad@2x.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/ABXYPad.imageset/Contents.json delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/DPad.imageset/Contents.json delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/DPad.imageset/DPad.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/DPad.imageset/DPad@2x.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/JoystickBackground.imageset/Contents.json delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/JoystickBackground.imageset/JoystickBackground.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/JoystickBackground.imageset/JoystickBackground@2x.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/JoystickButton.imageset/Contents.json delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/JoystickButton.imageset/JoystickButton.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/JoystickButton.imageset/JoystickButton@2x.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/LTrigger.imageset/Contents.json delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/LTrigger.imageset/LTrigger.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/LTrigger.imageset/LTrigger@2x.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/RTrigger.imageset/Contents.json delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/RTrigger.imageset/RTrigger.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/RTrigger.imageset/RTrigger@2x.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/Start.imageset/Contents.json delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/Start.imageset/Start.png delete mode 100644 shell/apple/emulator-ios/emulator/Images.xcassets/Start.imageset/Start@2x.png diff --git a/CMakeLists.txt b/CMakeLists.txt index f12f63ba6..3ff5a4493 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1029,7 +1029,7 @@ if(NOT LIBRETRO) fonts/Roboto-Medium.ttf.zip fonts/Roboto-Regular.ttf.zip fonts/fa-solid-900.ttf.zip) - if(ANDROID) + if(ANDROID OR IOS) cmrc_add_resources(flycast-resources WHENCE resources resources/picture/buttons.png) @@ -1641,6 +1641,8 @@ if(NOT LIBRETRO) shell/apple/emulator-ios/emulator/FlycastViewController.mm shell/apple/emulator-ios/emulator/PadViewController.h shell/apple/emulator-ios/emulator/PadViewController.mm + shell/apple/emulator-ios/emulator/EditPadViewController.h + shell/apple/emulator-ios/emulator/EditPadViewController.mm shell/apple/emulator-ios/emulator/EmulatorView.h shell/apple/emulator-ios/emulator/EmulatorView.mm shell/apple/emulator-ios/emulator/main.m @@ -1655,7 +1657,8 @@ if(NOT LIBRETRO) shell/apple/emulator-ios/emulator/Images.xcassets shell/apple/emulator-ios/emulator/FlycastStoryboard.storyboard shell/apple/emulator-ios/emulator/LaunchScreen.storyboard - shell/apple/emulator-ios/emulator/PadViewController.xib) + shell/apple/emulator-ios/emulator/PadViewController.xib + shell/apple/emulator-ios/emulator/EditPadViewController.xib) target_sources(${PROJECT_NAME} PRIVATE ${IOS_RESOURCES}) source_group("Resources" FILES ${IOS_RESOURCES}) set_source_files_properties(${IOS_RESOURCES} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") diff --git a/core/cfg/cfg.cpp b/core/cfg/cfg.cpp index 4a464b845..021a51418 100644 --- a/core/cfg/cfg.cpp +++ b/core/cfg/cfg.cpp @@ -130,3 +130,12 @@ void cfgSetAutoSave(bool autoSave) if (autoSave) saveConfigFile(); } + +void cfgSaveFloat(const std::string& section, const std::string& key, float value) +{ + cfgdb.set_float(section, key, value); +} +float cfgLoadFloat(const std::string& section, const std::string& key, float def) +{ + return cfgdb.get_float(section, key, def); +} diff --git a/core/cfg/cfg.h b/core/cfg/cfg.h index f13279e54..d90a307f7 100644 --- a/core/cfg/cfg.h +++ b/core/cfg/cfg.h @@ -11,6 +11,8 @@ std::string cfgLoadStr(const std::string& section, const std::string& key, const void cfgSaveStr(const std::string& section, const std::string& key, const std::string& value); void cfgSaveBool(const std::string& section, const std::string& key, bool value); bool cfgLoadBool(const std::string& section, const std::string& key, bool def); +void cfgSaveFloat(const std::string& section, const std::string& key, float value); +float cfgLoadFloat(const std::string& section, const std::string& key, float def); void cfgSetVirtual(const std::string& section, const std::string& key, const std::string& value); bool cfgIsVirtual(const std::string& section, const std::string& key); diff --git a/core/cfg/ini.cpp b/core/cfg/ini.cpp index c5879572c..a5ea7aad0 100644 --- a/core/cfg/ini.cpp +++ b/core/cfg/ini.cpp @@ -47,6 +47,11 @@ bool ConfigEntry::get_bool() } } +float ConfigEntry::get_float() +{ + return atof(this->value.c_str()); +} + /* ConfigSection */ bool ConfigSection::has_entry(const std::string& name) @@ -192,6 +197,15 @@ bool ConfigFile::get_bool(const std::string& section_name, const std::string& en } } +float ConfigFile::get_float(const std::string& section_name, const std::string& entry_name, float default_value) +{ + ConfigEntry* entry = get_entry(section_name, entry_name); + if (entry == nullptr) + return default_value; + else + return entry->get_float(); +} + void ConfigFile::set(const std::string& section_name, const std::string& entry_name, const std::string& value, bool is_virtual) { ConfigSection* section = this->get_section(section_name, is_virtual); @@ -222,6 +236,13 @@ void ConfigFile::set_bool(const std::string& section_name, const std::string& en this->set(section_name, entry_name, str_value, is_virtual); } +void ConfigFile::set_float(const std::string& section_name, const std::string& entry_name, float value, bool is_virtual) +{ + std::stringstream str_value; + str_value << value; + this->set(section_name, entry_name, str_value.str(), is_virtual); +} + void ConfigFile::parse(FILE* file) { if (file == nullptr) diff --git a/core/cfg/ini.h b/core/cfg/ini.h index ab3edd3f8..e4bd8a532 100644 --- a/core/cfg/ini.h +++ b/core/cfg/ini.h @@ -13,6 +13,7 @@ struct ConfigEntry { int get_int(); bool get_bool(); int64_t get_int64(); + float get_float(); }; struct ConfigSection { @@ -45,11 +46,13 @@ struct ConfigFile { int get_int(const std::string& section_name, const std::string& entry_name, int default_value = 0); int64_t get_int64(const std::string& section_name, const std::string& entry_name, int64_t default_value = 0); bool get_bool(const std::string& section_name, const std::string& entry_name, bool default_value = false); + float get_float(const std::string& section_name, const std::string& entry_name, float default_value = 0.f); /* setting values */ void set(const std::string& section_name, const std::string& entry_name, const std::string& value, bool is_virtual = false); void set_int(const std::string& section_name, const std::string& entry_name, int value, bool is_virtual = false); void set_int64(const std::string& section_name, const std::string& entry_name, int64_t value, bool is_virtual = false); void set_bool(const std::string& section_name, const std::string& entry_name, bool value, bool is_virtual = false); + void set_float(const std::string& section_name, const std::string& entry_name, float value, bool is_virtual = false); void delete_section(const std::string& section_name); void delete_entry(const std::string& section_name, const std::string& entry_name); diff --git a/core/cfg/option.h b/core/cfg/option.h index 1e520ecd0..c3ba52786 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -202,11 +202,7 @@ class Option : public BaseOption { std::enable_if_t, T> doLoad(const std::string& section, const std::string& name) const { - std::string strValue = cfgLoadStr(section, name, ""); - if (strValue.empty()) - return value; - else - return (float)atof(strValue.c_str()); + return cfgLoadFloat(section, name, value); } template @@ -301,9 +297,7 @@ class Option : public BaseOption { std::enable_if_t> doSave(const std::string& section, const std::string& name) const { - char buf[64]; - snprintf(buf, sizeof(buf), "%f", value); - cfgSaveStr(section, name, buf); + cfgSaveFloat(section, name, value); } template diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index e6367e716..57015c632 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -328,6 +328,7 @@ void gui_initFonts() largeFont = io.Fonts->AddFontFromMemoryTTF(data.release(), dataSize, largeFontSize, nullptr, ranges); NOTICE_LOG(RENDERER, "Screen DPI is %.0f, size %d x %d. Scaling by %.2f", settings.display.dpi, settings.display.width, settings.display.height, settings.display.uiScale); + vgamepad::applyUiScale(); } void gui_keyboard_input(u16 wc) @@ -532,7 +533,9 @@ void gui_open_settings() } else if (gui_state == GuiState::VJoyEdit) { - vgamepad::stopEditing(false); + vgamepad::pauseEditing(); + // iOS: force a touch up event to make up for the one eaten by the tap gesture recognizer + mouseButtons &= ~1; gui_setState(GuiState::VJoyEditCommands); } else if (gui_state == GuiState::Loading) @@ -1446,8 +1449,10 @@ static void gamepadSettingsPopup(const std::shared_ptr& gamepad) ImGui::NewLine(); if (gamepad->is_virtual_gamepad()) { +#ifdef __ANDROID__ header("Haptic"); OptionSlider("Power", config::VirtualGamepadVibration, 0, 100, "Haptic feedback power", "%d%%"); +#endif header("View"); OptionSlider("Transparency", config::VirtualGamepadTransparency, 0, 100, "Virtual gamepad buttons transparency", "%d%%"); } @@ -1956,7 +1961,7 @@ static void gui_settings_controls(bool& maple_devices_changed) controller_mapping_popup(gamepad); -#ifdef __ANDROID__ +#if defined(__ANDROID__) || defined(TARGET_IPHONE) if (gamepad->is_virtual_gamepad()) { if (ImGui::Button("Edit Layout")) @@ -1967,10 +1972,7 @@ static void gui_settings_controls(bool& maple_devices_changed) } #endif if (gamepad->is_rumble_enabled() || gamepad->has_analog_stick() -#ifdef __ANDROID__ - || gamepad->is_virtual_gamepad() -#endif - ) + || gamepad->is_virtual_gamepad()) { ImGui::SameLine(0, uiScaled(16)); if (ImGui::Button("Settings")) diff --git a/core/ui/gui_android.cpp b/core/ui/gui_android.cpp index 58508b39c..4c9ba44af 100644 --- a/core/ui/gui_android.cpp +++ b/core/ui/gui_android.cpp @@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with reicast. If not, see . */ -#ifdef __ANDROID__ +#if defined(__ANDROID__) || defined(TARGET_IPHONE) #include "gui_android.h" #include "gui.h" @@ -28,32 +28,38 @@ #include "input/gamepad.h" #include "input/gamepad_device.h" #include "oslib/resources.h" +#include "cfg/cfg.h" +#include "input/gamepad.h" #include namespace vgamepad { +static void loadLayout(); + struct Control { - float x; - float y; - float w; - float h; - float u0; - float v0; - float u1; - float v1; + Control() = default; + Control(float x, float y, float w = 64.f, float h = 64.f) + : pos(x, y), size(w, h), uv0(0, 0), uv1(1, 1) {} + + ImVec2 pos; + ImVec2 size; + ImVec2 uv0; + ImVec2 uv1; }; static Control Controls[_Count]; static bool Visible = true; -static float AlphaTrans; +static float AlphaTrans = 1.f; +static ImVec2 StickPos; // analog stick position [-1, 1] +constexpr char const *BTN_PATH = "picture/buttons.png"; void displayCommands() { + draw(); centerNextWindow(); - ImGui::Begin("Virtual Joystick", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse - | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar); + ImGui::Begin("##vgamepad", NULL, ImGuiWindowFlags_NoDecoration); if (ImGui::Button("Save", ScaledVec2(150, 50))) { @@ -83,6 +89,9 @@ static u8 *loadOSDButtons(int &width, int &height) stbi_set_flip_vertically_on_load(1); FILE *file = nowide::fopen(get_readonly_data_path("buttons.png").c_str(), "rb"); + if (file == nullptr) + // also try the home folder (android) + file = nowide::fopen(get_readonly_config_path("buttons.png").c_str(), "rb"); u8 *image_data = nullptr; if (file != nullptr) { @@ -92,8 +101,8 @@ static u8 *loadOSDButtons(int &width, int &height) if (image_data == nullptr) { size_t size; - std::unique_ptr data = resource::load("picture/buttons.png", size); - image_data = stbi_load_from_memory(data.get(), size, &width, &height, &n, STBI_rgb_alpha); + std::unique_ptr data = resource::load(BTN_PATH, size); + image_data = stbi_load_from_memory(data.get(), (int)size, &width, &height, &n, STBI_rgb_alpha); } return image_data; } @@ -103,7 +112,7 @@ class ImguiVGamepadTexture : public ImguiTexture public: ImTextureID getId() override { - ImTextureID id = imguiDriver->getTexture(PATH); + ImTextureID id = imguiDriver->getTexture(BTN_PATH); if (id == ImTextureID()) { int width, height; @@ -111,7 +120,7 @@ class ImguiVGamepadTexture : public ImguiTexture if (imgData != nullptr) { try { - id = imguiDriver->updateTextureAndAspectRatio(PATH, imgData, width, height, nearestSampling); + id = imguiDriver->updateTextureAndAspectRatio(BTN_PATH, imgData, width, height, nearestSampling); } catch (...) { // vulkan can throw during resizing } @@ -120,9 +129,6 @@ class ImguiVGamepadTexture : public ImguiTexture } return id; } - -private: - static constexpr char const *PATH = "picture/buttons.png"; }; constexpr float vjoy_sz[2][14] = { @@ -142,10 +148,10 @@ static void setUV() for (auto& control : Controls) { - control.u0 = (u + 1) / OSD_TEX_W; - control.v0 = 1.f - (v + 1) / OSD_TEX_H; - control.u1 = (u + vjoy_sz[0][i] - 1) / OSD_TEX_W; - control.v1 = 1.f - (v + vjoy_sz[1][i] - 1) / OSD_TEX_H; + control.uv0.x = (u + 1) / OSD_TEX_W; + control.uv0.y = 1.f - (v + 1) / OSD_TEX_H; + control.uv1.x = (u + vjoy_sz[0][i] - 1) / OSD_TEX_W; + control.uv1.y = 1.f - (v + vjoy_sz[1][i] - 1) / OSD_TEX_H; u += vjoy_sz[0][i]; if (u >= OSD_TEX_W) { @@ -167,33 +173,74 @@ void hide() { void setPosition(ControlId id, float x, float y, float w, float h) { - verify(id >= 0 && id < _Count); + verify(id >= 0 && id < _VisibleCount); auto& control = Controls[id]; - control.x = x; - control.y = y; - control.w = w; - control.h = h; + control.pos.x = x; + control.pos.y = y; + if (w != 0) + control.size.x = w; + if (h != 0) + control.size.y = h; +} + +ControlId hitTest(float x, float y) +{ + for (const auto& control : Controls) + if (x >= control.pos.x && x < control.pos.x + control.size.x + && y >= control.pos.y && y < control.pos.y + control.size.y) + return static_cast(&control - &Controls[0]); + return None; +} + +u32 controlToDcKey(ControlId control) +{ + switch (control) + { + case Left: return DC_DPAD_LEFT; + case Up: return DC_DPAD_UP; + case Right: return DC_DPAD_RIGHT; + case Down: return DC_DPAD_DOWN; + case X: return DC_BTN_X; + case Y: return DC_BTN_Y; + case B: return DC_BTN_B; + case A: return DC_BTN_A; + case Start: return DC_BTN_START; + case LeftTrigger: return DC_AXIS_LT; + case RightTrigger: return DC_AXIS_RT; + case FastForward: return EMU_BTN_FFORWARD; + case LeftUp: return DC_DPAD_LEFT | DC_DPAD_UP; + case RightUp: return DC_DPAD_RIGHT | DC_DPAD_UP; + case LeftDown: return DC_DPAD_LEFT | DC_DPAD_DOWN; + case RightDown: return DC_DPAD_RIGHT | DC_DPAD_DOWN; + default: return 0; + } +} + +void setAnalogStick(float x, float y) { + StickPos.x = x; + StickPos.y = y; +} + +float getControlWidth(ControlId control) { + return Controls[control].size.x; } static void drawButtonDim(ImDrawList *drawList, const Control& control, int state) { - ImVec2 pos(control.x, control.y); - ImVec2 size(control.w, control.h); - ImVec2 uv0(control.u0, control.v0); - ImVec2 uv1(control.u1, control.v1); float scale_h = settings.display.height / 480.f; float offs_x = (settings.display.width - scale_h * 640.f) / 2.f; - pos *= scale_h; - size *= scale_h; + ImVec2 pos = control.pos * scale_h; + ImVec2 size = control.size * scale_h; pos.x += offs_x; + if (static_cast(&control - &Controls[0]) == AnalogStick) + pos += StickPos * size; - float col = (0.5f - 0.25f * state / 255) * (float)Visible; + float col = (0.5f - 0.25f * state / 255) * AlphaTrans; float alpha = (100.f - config::VirtualGamepadTransparency) / 100.f * AlphaTrans; - AlphaTrans += ((float)Visible - AlphaTrans) / 2; ImVec4 color(col, col, col, alpha); ImguiVGamepadTexture tex; - tex.draw(drawList, pos, size, uv0, uv1, color); + tex.draw(drawList, pos, size, control.uv0, control.uv1, color); } static void drawButton(ImDrawList *drawList, const Control& control, bool state) { @@ -202,7 +249,17 @@ static void drawButton(ImDrawList *drawList, const Control& control, bool state) void draw() { - ImDrawList *drawList = ImGui::GetForegroundDrawList(); +#ifndef __ANDROID__ + if (Controls[Left].pos.x == 0.f) + { + loadLayout(); + if (Controls[Left].pos.x == 0.f) + // mark done + Controls[Left].pos.x = 1e-12f; + } +#endif + + ImDrawList *drawList = ImGui::GetBackgroundDrawList(); drawButton(drawList, Controls[Left], kcode[0] & DC_DPAD_LEFT); drawButton(drawList, Controls[Up], kcode[0] & DC_DPAD_UP); drawButton(drawList, Controls[Right], kcode[0] & DC_DPAD_RIGHT); @@ -223,8 +280,224 @@ void draw() drawButton(drawList, Controls[AnalogStick], false); drawButton(drawList, Controls[FastForward], false); + AlphaTrans += ((float)Visible - AlphaTrans) / 2; +} + +static float getUIScale() { + // scale is 1.1@ for a 320 dpi screen of height 750 + return 1.1f * 750.f / settings.display.height * settings.display.dpi / 320.f; +} + +struct LayoutElement +{ + const std::string name; + const float dx, dy; // default pos in dc coords, relative to sides (or middle if 0) + const float dw, dh; // default size in dc coords + + float x, y; // normalized coordinates [0, 1] + float w, h; // normalized coordinates [0, 1], scaled with uiScale + float scale; // user scale + + void load() + { + x = cfgLoadFloat(SECTION, name + "_x", x); + y = cfgLoadFloat(SECTION, name + "_y", y); + scale = cfgLoadFloat(SECTION, name + "_scale", scale); + } + void save() const + { + cfgSaveFloat(SECTION, name + "_x", x); + cfgSaveFloat(SECTION, name + "_y", y); + cfgSaveFloat(SECTION, name + "_scale", scale); + } + + bool hitTest(float nx, float ny) const { + return nx >= x && nx < x + w * scale + && ny >= y && ny < y + h * scale; + } + + void applyUiScale() + { + const float dcw = 480.f * (float)settings.display.width / settings.display.height; + const float uiscale = getUIScale(); + w = dw / dcw * uiscale; + h = dh / 480.f * uiscale; + } + + void reset() + { + scale = 1.f; + const float dcw = 480.f * (float)settings.display.width / settings.display.height; + const float uiscale = getUIScale(); + if (dx == 0) + x = 0.5f - w / 2; + else if (dx > 0) + x = dx / dcw * uiscale; + else + x = 1.f - w + dx / dcw * uiscale; + if (dy == 0) + y = 0.5f - h / 2; + else if (dy > 0) + y = dy / 480.f * uiscale; + else + y = 1.f - h + dy / 480.f * uiscale; + } + + static constexpr char const *SECTION = "vgamepad"; +}; +static LayoutElement Layout[] { + { "dpad", 32.f, -24.f, 192.f, 192.f }, + { "buttons", -24.f, -24.f, 192.f, 192.f }, + { "start", 0.f, -24.f, 64.f, 64.f }, + { "LT", -134.f,-240.f, 90.f, 64.f }, + { "RT", -32.f,-240.f, 90.f, 64.f }, + { "analog", 40.f,-320.f, 128.f, 128.f }, + { "fforward", -24.f, 24.f, 64.f, 64.f }, +}; + +static void applyLayout() +{ + const float dcw = 480.f * (float)settings.display.width / settings.display.height; + const float dx = (dcw - 640.f) / 2; + const float uiscale = getUIScale(); + float x, y, scale; + + // DPad + x = Layout[Elem_DPad].x * dcw - dx; + y = Layout[Elem_DPad].y * 480.f; + scale = Layout[Elem_DPad].scale * uiscale; + Controls[Left].pos = { x + 0.f * scale, y + 64.f * scale }; + Controls[Up].pos = { x + 64.f * scale, y + 0.f * scale }; + Controls[Right].pos = { x + 128.f * scale, y + 64.f * scale }; + Controls[Down].pos = { x + 64.f * scale, y + 128.f * scale }; + for (int control = Left; control <= Down; control++) + Controls[control].size = { 64.f * scale, 64.f * scale }; + + Controls[LeftUp].pos = { x + 0.f * scale, y + 0.f * scale }; + Controls[LeftDown].pos = { x + 0.f * scale, y + 128.f * scale }; + Controls[RightUp].pos = { x + 128.f * scale, y + 0.f * scale }; + Controls[RightDown].pos = { x + 128.f * scale, y + 128.f * scale }; + for (int control = LeftUp; control <= RightDown; control++) + Controls[control].size = { 64.f * scale, 64.f * scale }; + + // Buttons + x = Layout[Elem_Buttons].x * dcw - dx; + y = Layout[Elem_Buttons].y * 480.f; + scale = Layout[Elem_Buttons].scale * uiscale; + Controls[X].pos = { x + 0.f * scale, y + 64.f * scale }; + Controls[Y].pos = { x + 64.f * scale, y + 0.f * scale }; + Controls[B].pos = { x + 128.f * scale, y + 64.f * scale }; + Controls[A].pos = { x + 64.f * scale, y + 128.f * scale }; + for (int control = X; control <= A; control++) + Controls[control].size = { 64.f * scale, 64.f * scale }; + + // Start + scale = Layout[Elem_Start].scale * uiscale; + Controls[Start].pos = { Layout[Elem_Start].x * dcw - dx, Layout[Elem_Start].y * 480.f }; + Controls[Start].size = { Layout[Elem_Start].dw * scale, Layout[Elem_Start].dh * scale }; + + // Left trigger + scale = Layout[Elem_LT].scale * uiscale; + Controls[LeftTrigger].pos = { Layout[Elem_LT].x * dcw - dx, Layout[Elem_LT].y * 480.f }; + Controls[LeftTrigger].size = { Layout[Elem_LT].dw * scale, Layout[Elem_LT].dh * scale }; + + // Right trigger + scale = Layout[Elem_RT].scale * uiscale; + Controls[RightTrigger].pos = { Layout[Elem_RT].x * dcw - dx, Layout[Elem_RT].y * 480.f }; + Controls[RightTrigger].size = { Layout[Elem_RT].dw * scale, Layout[Elem_RT].dh * scale }; + + // Analog + x = Layout[Elem_Analog].x * dcw - dx; + y = Layout[Elem_Analog].y * 480.f; + scale = Layout[Elem_Analog].scale * uiscale; + Controls[AnalogArea].pos = { x, y }; + Controls[AnalogArea].size = { Layout[Elem_Analog].dw * scale, Layout[Elem_Analog].dh * scale }; + Controls[AnalogStick].pos = { x + 32.f * scale, y + 32.f * scale }; + Controls[AnalogStick].size = { 64.f * scale, 64.f * scale }; + + // Fast forward + scale = Layout[Elem_FForward].scale * uiscale; + Controls[FastForward].pos = { Layout[Elem_FForward].x * dcw - dx, Layout[Elem_FForward].y * 480.f }; + Controls[FastForward].size = { Layout[Elem_FForward].dw * scale, Layout[Elem_FForward].dh * scale }; +} + +void applyUiScale() { + for (auto& element : Layout) + element.applyUiScale(); +} + +static void loadLayout() +{ + for (auto& element : Layout) { + element.reset(); + element.load(); + } + applyLayout(); +} + +static void saveLayout() +{ + cfgSetAutoSave(false); + for (auto& element : Layout) + element.save(); + cfgSetAutoSave(false); +} + +static void resetLayout() +{ + for (auto& element : Layout) + element.reset(); + applyLayout(); +} + +Element layoutHitTest(float x, float y) +{ + for (const auto& element : Layout) + if (element.hitTest(x, y)) + return static_cast(&element - &Layout[0]); + return Elem_None; +} + +void translateElement(Element element, float dx, float dy) +{ + LayoutElement& e = Layout[element]; + e.x += dx; + e.y += dy; + applyLayout(); +} + +void scaleElement(Element element, float factor) +{ + LayoutElement& e = Layout[element]; + float dx = e.w * e.scale * (factor - 1.f) / 2.f; + float dy = e.h * e.scale * (factor - 1.f) / 2.f; + e.scale *= factor; + // keep centered + translateElement(element, -dx, -dy); +} + +#ifndef __ANDROID__ + +void startEditing() { + show(); +} + +void pauseEditing() { +} + +void stopEditing(bool canceled) +{ + if (canceled) + loadLayout(); + else + saveLayout(); +} + +void resetEditing() { + resetLayout(); } +#endif } // namespace vgamepad #endif // __ANDROID__ diff --git a/core/ui/gui_android.h b/core/ui/gui_android.h index ead3478cb..a84bcd098 100644 --- a/core/ui/gui_android.h +++ b/core/ui/gui_android.h @@ -23,6 +23,7 @@ namespace vgamepad { enum ControlId { + None = -1, Left, Up, Right, @@ -38,30 +39,58 @@ enum ControlId AnalogStick, FastForward, - _Count + LeftUp, + RightUp, + LeftDown, + RightDown, + + _Count, + _VisibleCount = FastForward + 1, }; -#ifdef __ANDROID__ +enum Element +{ + Elem_None = -1, + Elem_DPad, + Elem_Buttons, + Elem_Start, + Elem_LT, + Elem_RT, + Elem_Analog, + Elem_FForward, +}; + +#if defined(__ANDROID__) || defined(TARGET_IPHONE) -void setPosition(ControlId id, float x, float y, float w, float h); +void setPosition(ControlId id, float x, float y, float w = 0.f, float h = 0.f); // Legacy android void show(); void hide(); void draw(); void startEditing(); +void pauseEditing(); void stopEditing(bool canceled); void resetEditing(); void displayCommands(); +ControlId hitTest(float x, float y); +u32 controlToDcKey(ControlId control); +void setAnalogStick(float x, float y); +float getControlWidth(ControlId); + +void applyUiScale(); +Element layoutHitTest(float x, float y); +void translateElement(Element element, float dx, float dy); +void scaleElement(Element element, float factor); + #else -void setPosition(ControlId id, float x, float y, float w, float h) {} void show() {} void hide() {} void draw() {} void startEditing() {} -void stopEditing(bool canceled) {} -void resetEditing() {} +void pauseEditing() {} void displayCommands() {} +void applyUiScale() {} #endif } // namespace vgamepad diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index d9f4bdcbb..3547f48b8 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -604,6 +604,9 @@ namespace vgamepad void startEditing() { jni::env()->CallVoidMethod(g_activity, VJoyStartEditingMID); } +void pauseEditing() { + stopEditing(false); +} void resetEditing() { jni::env()->CallVoidMethod(g_activity, VJoyResetEditingMID); } diff --git a/shell/apple/emulator-ios/emulator/EditPadViewController.h b/shell/apple/emulator-ios/emulator/EditPadViewController.h new file mode 100644 index 000000000..b347b8eba --- /dev/null +++ b/shell/apple/emulator-ios/emulator/EditPadViewController.h @@ -0,0 +1,31 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#pragma once +#import + +@interface EditPadViewController : UIViewController + +- (void) showController:(UIView *)parentView; +- (void) hideController; +- (BOOL) isControllerVisible; +- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer; +- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer; +- (IBAction)handleTap:(UITapGestureRecognizer *)recognizer; + +@end diff --git a/shell/apple/emulator-ios/emulator/EditPadViewController.mm b/shell/apple/emulator-ios/emulator/EditPadViewController.mm new file mode 100644 index 000000000..811e5a5f7 --- /dev/null +++ b/shell/apple/emulator-ios/emulator/EditPadViewController.mm @@ -0,0 +1,131 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#import "EditPadViewController.h" +#include "types.h" +#include "ui/gui.h" +#include "ui/gui_android.h" +#include "cfg/cfg.h" + +@interface EditPadViewController () { + vgamepad::Element currentControl; +} + +@end + +@implementation EditPadViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + currentControl = vgamepad::Elem_None; +} + +- (void)showController:(UIView *)parentView +{ + if (!cfgLoadBool("help", "EditPadTip", false)) + { + UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Help Tip" + message:@"Double tap to exit." + preferredStyle:UIAlertControllerStyleAlert]; + [self presentViewController:alert animated:YES completion:nil]; + UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + cfgSaveBool("help", "EditPadTip", true); + }]; + [alert addAction:defaultAction]; + } + [parentView addSubview:self.view]; +} + +- (void)hideController +{ + [self.view removeFromSuperview]; +} + +- (BOOL)isControllerVisible { + return self.view.window != nil; +} + +static void normalize(CGPoint& pos, const CGSize& size) { + pos.x /= size.width; + pos.y /= size.height; +} + +- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer +{ + CGPoint loc = [recognizer locationInView:self.view]; + normalize(loc, self.view.bounds.size); + switch (recognizer.state) + { + case UIGestureRecognizerStateBegan: + currentControl = vgamepad::layoutHitTest(loc.x, loc.y); + break; + case UIGestureRecognizerStateEnded: + currentControl = vgamepad::Elem_None; + break; + case UIGestureRecognizerStateChanged: + if (currentControl != vgamepad::Elem_None) + { + CGPoint translation = [recognizer translationInView:self.view]; + [recognizer setTranslation:CGPointMake(0, 0) inView:self.view]; + normalize(translation, self.view.bounds.size); + vgamepad::translateElement(currentControl, translation.x, translation.y); + } + break; + case UIGestureRecognizerStateCancelled: + currentControl = vgamepad::Elem_None; + break; + default: + break; + } +} + +- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer +{ + CGPoint loc = [recognizer locationInView:self.view]; + normalize(loc, self.view.bounds.size); + switch (recognizer.state) + { + case UIGestureRecognizerStateBegan: + currentControl = vgamepad::layoutHitTest(loc.x, loc.y); + break; + case UIGestureRecognizerStateChanged: + if (currentControl != vgamepad::Elem_None) + vgamepad::scaleElement(currentControl, recognizer.scale); + break; + case UIGestureRecognizerStateEnded: + currentControl = vgamepad::Elem_None; + break; + case UIGestureRecognizerStateCancelled: + currentControl = vgamepad::Elem_None; + break; + default: + break; + } + recognizer.scale = 1; +} + +- (IBAction)handleTap:(UITapGestureRecognizer *)recognizer +{ + if (recognizer.state == UIGestureRecognizerStateRecognized) + gui_open_settings(); +} + +@end + diff --git a/shell/apple/emulator-ios/emulator/EditPadViewController.xib b/shell/apple/emulator-ios/emulator/EditPadViewController.xib new file mode 100644 index 000000000..224cf0ac4 --- /dev/null +++ b/shell/apple/emulator-ios/emulator/EditPadViewController.xib @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shell/apple/emulator-ios/emulator/FlycastViewController.mm b/shell/apple/emulator-ios/emulator/FlycastViewController.mm index dc76c26b1..6bfdf8377 100644 --- a/shell/apple/emulator-ios/emulator/FlycastViewController.mm +++ b/shell/apple/emulator-ios/emulator/FlycastViewController.mm @@ -29,6 +29,7 @@ #import #import "PadViewController.h" +#import "EditPadViewController.h" #import "EmulatorView.h" #include "types.h" @@ -149,6 +150,7 @@ @interface FlycastViewController () 004&%004{+008|`004nN004b?008NW002DY000@xb3BE2000U( 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

-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 diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/ABXYPad.imageset/Contents.json b/shell/apple/emulator-ios/emulator/Images.xcassets/ABXYPad.imageset/Contents.json deleted file mode 100644 index 7c836a94b..000000000 --- a/shell/apple/emulator-ios/emulator/Images.xcassets/ABXYPad.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "ABXYPad.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "ABXYPad@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/DPad.imageset/Contents.json b/shell/apple/emulator-ios/emulator/Images.xcassets/DPad.imageset/Contents.json deleted file mode 100644 index 2c0bf7313..000000000 --- a/shell/apple/emulator-ios/emulator/Images.xcassets/DPad.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "DPad.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "DPad@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/DPad.imageset/DPad.png b/shell/apple/emulator-ios/emulator/Images.xcassets/DPad.imageset/DPad.png deleted file mode 100644 index 5bb8caa5b3a47fff8f07d9460483fba3b7651621..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6122 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 x07|Htw5KRKD5fbSd@iRU*?cC-=Md5 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 diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/JoystickBackground.imageset/Contents.json b/shell/apple/emulator-ios/emulator/Images.xcassets/JoystickBackground.imageset/Contents.json deleted file mode 100644 index c0251f4cd..000000000 --- a/shell/apple/emulator-ios/emulator/Images.xcassets/JoystickBackground.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "JoystickBackground.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "JoystickBackground@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/JoystickBackground.imageset/JoystickBackground.png b/shell/apple/emulator-ios/emulator/Images.xcassets/JoystickBackground.imageset/JoystickBackground.png deleted file mode 100644 index 2a251547c4f76c08cafc3519d779ee8ed435736b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8439 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'sd331u>Lttb_k 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 M07*qoM6N<$g7P6X+5i9m diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/LTrigger.imageset/Contents.json b/shell/apple/emulator-ios/emulator/Images.xcassets/LTrigger.imageset/Contents.json deleted file mode 100644 index b29609a2f..000000000 --- a/shell/apple/emulator-ios/emulator/Images.xcassets/LTrigger.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "LTrigger.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "LTrigger@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/LTrigger.imageset/LTrigger.png b/shell/apple/emulator-ios/emulator/Images.xcassets/LTrigger.imageset/LTrigger.png deleted file mode 100644 index f6ece71923d7ba3b67b0073714ab3a758a5a4ea7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3226 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 M07*qoM6N<$f^#ejT>t<8 diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/LTrigger.imageset/LTrigger@2x.png b/shell/apple/emulator-ios/emulator/Images.xcassets/LTrigger.imageset/LTrigger@2x.png deleted file mode 100644 index 8693da029113049df39cb11d208a82b0a5fc1ca6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4056 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 NOu*&_wR(4-{1;ltpQQi* diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/RTrigger.imageset/Contents.json b/shell/apple/emulator-ios/emulator/Images.xcassets/RTrigger.imageset/Contents.json deleted file mode 100644 index 49f44043a..000000000 --- a/shell/apple/emulator-ios/emulator/Images.xcassets/RTrigger.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "RTrigger.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "RTrigger@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/RTrigger.imageset/RTrigger.png b/shell/apple/emulator-ios/emulator/Images.xcassets/RTrigger.imageset/RTrigger.png deleted file mode 100644 index f81f1418854a227c9bc66b15c86e99a9a430c1e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3488 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 diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/Start.imageset/Contents.json b/shell/apple/emulator-ios/emulator/Images.xcassets/Start.imageset/Contents.json deleted file mode 100644 index 8d88e6bc9..000000000 --- a/shell/apple/emulator-ios/emulator/Images.xcassets/Start.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Start.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "Start@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/Start.imageset/Start.png b/shell/apple/emulator-ios/emulator/Images.xcassets/Start.imageset/Start.png deleted file mode 100644 index e3ea5cc58c92868b73a9a317e41a89fef931b2e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3914 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 YKRvZO@-%`S6#xJL07*qoM6N<$f<0k=xBvhE diff --git a/shell/apple/emulator-ios/emulator/Images.xcassets/Start.imageset/Start@2x.png b/shell/apple/emulator-ios/emulator/Images.xcassets/Start.imageset/Start@2x.png deleted file mode 100644 index 099c7371cd728cbd2c0afde61007ad2ebd426e67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6510 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 virtualGamepad; NSMutableDictionary *touchToButton; + NSTimer *hideTimer; } @end @@ -58,11 +60,13 @@ - (void)showController:(UIView *)parentView [alert addAction:defaultAction]; } [parentView addSubview:self.view]; + [self startHideTimer]; } - (void)hideController { [self resetTouch]; + [hideTimer invalidate]; [self.view removeFromSuperview]; } @@ -70,35 +74,60 @@ - (BOOL)isControllerVisible { return self.view.window != nil; } +-(void)startHideTimer +{ + [hideTimer invalidate]; + hideTimer = [NSTimer scheduledTimerWithTimeInterval:10 + target:self + selector:@selector(hideTimer) + userInfo:nil + repeats:NO]; + vgamepad::show(); +} + +-(void)hideTimer { + vgamepad::hide(); +} + - (void)resetTouch { joyTouch = nil; - self.joyXConstraint.constant = 0; - self.joyYConstraint.constant = 0; - virtualGamepad->gamepad_axis_input(IOS_AXIS_LX, 0); - virtualGamepad->gamepad_axis_input(IOS_AXIS_LY, 0); + virtualGamepad->gamepad_axis_input(DC_AXIS_LEFT, 0); + virtualGamepad->gamepad_axis_input(DC_AXIS_UP, 0); + vgamepad::setAnalogStick(0, 0); +} + +static CGPoint translateCoords(const CGPoint& pos, const CGSize& size) +{ + CGFloat hscale = 480.0 / size.height; + CGPoint p; + p.y = pos.y * hscale; + p.x = (pos.x - (size.width - 640.0 / hscale) / 2.0) * hscale; + return p; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; { - for (UITouch *touch in touches) { - if (joyTouch == nil) { - CGPoint loc = [touch locationInView:self.joystickBackground]; - if ([self.joystickBackground pointInside:loc withEvent:event]) { - joyTouch = touch; - joyBias = loc; - virtualGamepad->gamepad_axis_input(IOS_AXIS_LX, 0); - virtualGamepad->gamepad_axis_input(IOS_AXIS_LY, 0); - continue; - } - } + [self startHideTimer]; + for (UITouch *touch in touches) + { CGPoint point = [touch locationInView:self.view]; - UIView *touchedView = [self.view hitTest:point withEvent:nil]; + point = translateCoords(point, self.view.bounds.size); + vgamepad::ControlId control = vgamepad::hitTest(point.x, point.y); + if (joyTouch == nil && control == vgamepad::AnalogArea) + { + [self resetTouch]; + joyTouch = touch; + joyBias = point; + continue; + } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; - if (touchedView.tag != 0 && touchToButton[key] == nil) { - touchToButton[key] = touchedView; + if (control != vgamepad::None && control != vgamepad::AnalogArea + && touchToButton[key] == nil) + { + touchToButton[key] = [NSNumber numberWithInt:control]; // button down - virtualGamepad->gamepad_btn_input((u32)touchedView.tag, true); + virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey(control), true); } } [super touchesBegan:touches withEvent:event]; @@ -106,17 +135,18 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; { - for (UITouch *touch in touches) { + for (UITouch *touch in touches) + { if (touch == joyTouch) { [self resetTouch]; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; - UIView *button = touchToButton[key]; - if (button != nil) { + NSNumber *control = touchToButton[key]; + if (control != nil) { [touchToButton removeObjectForKey:key]; // button up - virtualGamepad->gamepad_btn_input((u32)button.tag, false); + virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey((vgamepad::ControlId)control.intValue), false); } } [super touchesEnded:touches withEvent:event]; @@ -124,36 +154,43 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; { - for (UITouch *touch in touches) { - if (touch == joyTouch) { - CGPoint pos = [touch locationInView:[self joystickBackground]]; - pos.x -= joyBias.x; - pos.y -= joyBias.y; - pos.x = std::max(std::min(25.0, pos.x), -25.0); - pos.y = std::max(std::min(25.0, pos.y), -25.0); - self.joyXConstraint.constant = pos.x; - self.joyYConstraint.constant = pos.y; - virtualGamepad->gamepad_axis_input(IOS_AXIS_LX, (s8)std::round(pos.x * 32767.0 / 25.0)); - virtualGamepad->gamepad_axis_input(IOS_AXIS_LY, (s8)std::round(pos.y * 32767.0 / 25.0)); + [self startHideTimer]; + for (UITouch *touch in touches) + { + CGPoint point = [touch locationInView:self.view]; + point = translateCoords(point, self.view.bounds.size); + if (touch == joyTouch) + { + point.x -= joyBias.x; + point.y -= joyBias.y; + double sz = vgamepad::getControlWidth(vgamepad::AnalogStick); + point.x = std::max(std::min(1.0, point.x / sz), -1.0); + point.y = std::max(std::min(1.0, point.y / sz), -1.0); + vgamepad::setAnalogStick(point.x, point.y); + point.x *= 32767.0; + point.y *= 32767.0; + if (point.x >= 0) + virtualGamepad->gamepad_axis_input(DC_AXIS_RIGHT, (int)std::round(point.x)); + else + virtualGamepad->gamepad_axis_input(DC_AXIS_LEFT, -(int)std::round(point.x)); + if (point.y >= 0) + virtualGamepad->gamepad_axis_input(DC_AXIS_DOWN, (int)std::round(point.y)); + else + virtualGamepad->gamepad_axis_input(DC_AXIS_UP, -(int)std::round(point.y)); continue; } - CGPoint point = [touch locationInView:self.view]; - UIView *touchedView = [self.view hitTest:point withEvent:nil]; + vgamepad::ControlId control = vgamepad::hitTest(point.x, point.y); NSValue *key = [NSValue valueWithPointer:(const void *)touch]; - UIView *button = touchToButton[key]; - if (button != nil && touchedView.tag != button.tag) { + NSNumber *prevControl = touchToButton[key]; + if (prevControl.intValue == control) + continue; + if (prevControl != nil && prevControl.intValue != vgamepad::None && prevControl.intValue != vgamepad::AnalogArea) { // button up - virtualGamepad->gamepad_btn_input((u32)button.tag, false); - touchToButton[key] = touchedView; - // button down - virtualGamepad->gamepad_btn_input((u32)touchedView.tag, true); - } - else if (button == nil && touchedView.tag != 0) - { - touchToButton[key] = touchedView; - // button down - virtualGamepad->gamepad_btn_input((u32)touchedView.tag, true); + virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey((vgamepad::ControlId)prevControl.intValue), false); } + // button down + virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey(control), true); + touchToButton[key] = [NSNumber numberWithInt:control]; } [super touchesMoved:touches withEvent:event]; } @@ -166,11 +203,11 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; - UIView *button = touchToButton[key]; - if (button != nil) { + NSNumber *control = touchToButton[key]; + if (control != nil) { [touchToButton removeObjectForKey:key]; // button up - virtualGamepad->gamepad_btn_input((u32)button.tag, false); + virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey((vgamepad::ControlId)control.intValue), false); } } [super touchesCancelled:touches withEvent:event]; diff --git a/shell/apple/emulator-ios/emulator/PadViewController.xib b/shell/apple/emulator-ios/emulator/PadViewController.xib index 94a774c7a..6ccc8b085 100644 --- a/shell/apple/emulator-ios/emulator/PadViewController.xib +++ b/shell/apple/emulator-ios/emulator/PadViewController.xib @@ -10,214 +10,15 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/shell/apple/emulator-ios/emulator/ios_gamepad.h b/shell/apple/emulator-ios/emulator/ios_gamepad.h index 3d768c713..214744c3e 100644 --- a/shell/apple/emulator-ios/emulator/ios_gamepad.h +++ b/shell/apple/emulator-ios/emulator/ios_gamepad.h @@ -49,11 +49,6 @@ enum IOSButton { IOS_BTN_PADDLE3, IOS_BTN_PADDLE4, IOS_BTN_TOUCHPAD, - - IOS_BTN_UP_RIGHT, - IOS_BTN_UP_LEFT, - IOS_BTN_DOWN_LEFT, - IOS_BTN_DOWN_RIGHT, }; enum IOSAxis { @@ -491,7 +486,7 @@ class IOSVirtualGamepad : public GamepadDevice IOSVirtualGamepad() : GamepadDevice(0, "iOS", false) { _name = "Virtual Gamepad"; _unique_id = "ios-virtual-gamepad"; - input_mapper = getDefaultMapping(); + input_mapper = std::make_shared(); //hasAnalogStick = true; // TODO has an analog stick but input mapping isn't persisted } @@ -504,31 +499,31 @@ class IOSVirtualGamepad : public GamepadDevice bool gamepad_btn_input(u32 code, bool pressed) override { if (pressed) - buttonState |= 1 << code; + buttonState |= code; else - buttonState &= ~(1 << code); + buttonState &= ~code; switch (code) { - case IOS_BTN_L2: - gamepad_axis_input(IOS_AXIS_L2, pressed ? 0x7fff : 0); + case DC_AXIS_LT: + gamepad_axis_input(DC_AXIS_LT, pressed ? 0x7fff : 0); if (settings.platform.isArcade()) - GamepadDevice::gamepad_btn_input(IOS_BTN_L1, pressed); // Z, btn5 + GamepadDevice::gamepad_btn_input(DC_BTN_Z, pressed); // btn5 return true; - case IOS_BTN_R2: + case DC_AXIS_RT: if (!pressed && maple_port() >= 0 && maple_port() <= 3) kcode[maple_port()] |= DC_DPAD2_UP | DC_BTN_D | DC_DPAD2_DOWN; - gamepad_axis_input(IOS_AXIS_R2, pressed ? 0x7fff : 0); + gamepad_axis_input(DC_AXIS_RT, pressed ? 0x7fff : 0); if (settings.platform.isArcade()) - GamepadDevice::gamepad_btn_input(IOS_BTN_Y, pressed); // Y, btn4 + GamepadDevice::gamepad_btn_input(DC_BTN_Y, pressed); // btn4 return true; default: - if ((buttonState & ((1 << IOS_BTN_UP) | (1 << IOS_BTN_DOWN))) == ((1 << IOS_BTN_UP) | (1 << IOS_BTN_DOWN)) - || (buttonState & ((1 << IOS_BTN_LEFT) | (1 << IOS_BTN_RIGHT))) == ((1 << IOS_BTN_LEFT) | (1 << IOS_BTN_RIGHT))) + if ((buttonState & (DC_DPAD_UP | DC_DPAD_DOWN)) == (DC_DPAD_UP | DC_DPAD_DOWN) + || (buttonState & (DC_DPAD_LEFT | DC_DPAD_RIGHT)) == (DC_DPAD_LEFT | DC_DPAD_RIGHT)) { - GamepadDevice::gamepad_btn_input(IOS_BTN_UP, false); - GamepadDevice::gamepad_btn_input(IOS_BTN_DOWN, false); - GamepadDevice::gamepad_btn_input(IOS_BTN_LEFT, false); - GamepadDevice::gamepad_btn_input(IOS_BTN_RIGHT, false); + GamepadDevice::gamepad_btn_input(DC_DPAD_UP, false); + GamepadDevice::gamepad_btn_input(DC_DPAD_DOWN, false); + GamepadDevice::gamepad_btn_input(DC_DPAD_LEFT, false); + GamepadDevice::gamepad_btn_input(DC_DPAD_RIGHT, false); buttonState = 0; gui_open_settings(); return true; @@ -536,18 +531,18 @@ class IOSVirtualGamepad : public GamepadDevice if (settings.platform.isArcade() && maple_port() >= 0 && maple_port() <= 3) { u32& keycode = kcode[maple_port()]; - if ((buttonState & (1 << IOS_BTN_R2)) != 0) + if ((buttonState & DC_AXIS_RT) != 0) { switch (code) { - case IOS_BTN_A: + case DC_BTN_A: // RT + A -> D (coin) keycode = pressed ? keycode & ~DC_BTN_D : keycode | DC_BTN_D; break; - case IOS_BTN_B: + case DC_BTN_B: // RT + B -> Service keycode = pressed ? keycode & ~DC_DPAD2_UP : keycode | DC_DPAD2_UP; break; - case IOS_BTN_X: + case DC_BTN_X: // RT + X -> Test keycode = pressed ? keycode & ~DC_DPAD2_DOWN : keycode | DC_DPAD2_DOWN; break; @@ -556,28 +551,28 @@ class IOSVirtualGamepad : public GamepadDevice } } // arcade mapping: X -> btn2, Y -> btn3 - if (code == IOS_BTN_X) - code = IOS_BTN_R1; // C, btn2 - if (code == IOS_BTN_Y) - code = IOS_BTN_X; // btn3 + if (code == DC_BTN_X) + code = DC_BTN_C; // btn2 + if (code == DC_BTN_Y) + code = DC_BTN_X; // btn3 } switch (code) { - case IOS_BTN_UP_RIGHT: - GamepadDevice::gamepad_btn_input(IOS_BTN_UP, pressed); - code = IOS_BTN_RIGHT; + case DC_DPAD_UP | DC_DPAD_RIGHT: + GamepadDevice::gamepad_btn_input(DC_DPAD_UP, pressed); + code = DC_DPAD_RIGHT; break; - case IOS_BTN_DOWN_RIGHT: - GamepadDevice::gamepad_btn_input(IOS_BTN_DOWN, pressed); - code = IOS_BTN_RIGHT; + case DC_DPAD_DOWN | DC_DPAD_RIGHT: + GamepadDevice::gamepad_btn_input(DC_DPAD_DOWN, pressed); + code = DC_DPAD_RIGHT; break; - case IOS_BTN_DOWN_LEFT: - GamepadDevice::gamepad_btn_input(IOS_BTN_DOWN, pressed); - code = IOS_BTN_LEFT; + case DC_DPAD_DOWN | DC_DPAD_LEFT: + GamepadDevice::gamepad_btn_input(DC_DPAD_DOWN, pressed); + code = DC_DPAD_LEFT; break; - case IOS_BTN_UP_LEFT: - GamepadDevice::gamepad_btn_input(IOS_BTN_UP, pressed); - code = IOS_BTN_LEFT; + case DC_DPAD_UP | DC_DPAD_LEFT: + GamepadDevice::gamepad_btn_input(DC_DPAD_UP, pressed); + code = DC_DPAD_LEFT; break; default: break; From 792aa38d34fd033d527ac11c0b406f4cfb8e40f3 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 26 Nov 2024 16:06:13 +0100 Subject: [PATCH 30/81] input: implement ramp up/down for analog axes mapped to buttons Full analog axes now have a ramp up/down of 100 ms when the mapped button is pressed/released. Works also for keyboards. Issue #1017 --- core/input/gamepad_device.cpp | 76 +++++++++++++++++++++++++++++++++++ core/input/gamepad_device.h | 22 +++++----- core/oslib/oslib.cpp | 9 +++-- 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/core/input/gamepad_device.cpp b/core/input/gamepad_device.cpp index 7ca658414..5ddc72c6b 100644 --- a/core/input/gamepad_device.cpp +++ b/core/input/gamepad_device.cpp @@ -584,6 +584,82 @@ void GamepadDevice::SaveMaplePorts() } } +s16 (&GamepadDevice::getTargetArray(DigAnalog axis))[4] +{ + switch (axis) + { + case DIGANA_LEFT: + case DIGANA_RIGHT: + return joyx; + case DIGANA_UP:; + case DIGANA_DOWN: + return joyy; + case DIGANA2_LEFT: + case DIGANA2_RIGHT: + return joyrx; + case DIGANA2_UP: + case DIGANA2_DOWN: + return joyry; + case DIGANA3_LEFT: + case DIGANA3_RIGHT: + return joy3x; + case DIGANA3_UP: + case DIGANA3_DOWN: + return joy3y; + default: + die("unknown axis"); + } +} + +void GamepadDevice::rampAnalog() +{ + if (lastAnalogUpdate == 0) + // also used as a flag that no analog ramping is needed on this device (yet) + return; + + const u64 now = getTimeMs(); + const int delta = std::round(static_cast(now - lastAnalogUpdate) * AnalogRamp); + lastAnalogUpdate = now; + for (unsigned port = 0; port < std::size(digitalToAnalogState); port++) + { + for (int axis = 0; axis < 12; axis += 2) // 3 sticks with 2 axes each + { + DigAnalog negDir = static_cast(1 << axis); + if ((rampAnalogState[port] & negDir) == 0) + // axis not active + continue; + DigAnalog posDir = static_cast(1 << (axis + 1)); + const int socd = digitalToAnalogState[port] & (negDir | posDir); + s16& axisValue = getTargetArray(negDir)[port]; + if (socd != 0 && socd != (negDir | posDir)) + { + // One axis is pressed => ramp up + if (socd == posDir) + axisValue = std::min(32767, axisValue + delta); + else + axisValue = std::max(-32768, axisValue - delta); + } + else + { + // No axis is pressed (or both) => ramp down + if (axisValue > 0) + axisValue = std::max(0, axisValue - delta); + else if (axisValue < 0) + axisValue = std::min(0, axisValue + delta); + else + rampAnalogState[port] &= ~negDir; + } + } + } +} + +void GamepadDevice::RampAnalog() +{ + std::lock_guard _(_gamepads_mutex); + for (auto& gamepad : _gamepads) + gamepad->rampAnalog(); +} + #ifdef TEST_AUTOMATION #include "cfg/option.h" static bool replay_inited; diff --git a/core/input/gamepad_device.h b/core/input/gamepad_device.h index aef633870..6e94b70ad 100644 --- a/core/input/gamepad_device.h +++ b/core/input/gamepad_device.h @@ -20,6 +20,7 @@ #pragma once #include "types.h" #include "mapping.h" +#include "stdclass.h" #include #include @@ -98,6 +99,7 @@ class GamepadDevice static int GetGamepadCount(); static std::shared_ptr GetGamepad(int index); static void SaveMaplePorts(); + static void RampAnalog(); static void load_system_mappings(); bool find_mapping(int system = settings.platform.system); @@ -163,16 +165,14 @@ class GamepadDevice digitalToAnalogState[port] |= axis; else digitalToAnalogState[port] &= ~axis; - const u32 socd = digitalToAnalogState[port] & (NegDir | PosDir); - if (socd == 0 || socd == (NegDir | PosDir)) - joystick = 0; - else if (socd == NegDir) - joystick = -32768; - else - joystick = 32767; - + rampAnalogState[port] |= NegDir; + if (lastAnalogUpdate == 0) + lastAnalogUpdate = getTimeMs(); } + s16 (&getTargetArray(DigAnalog axis))[4]; + void rampAnalog(); + std::string _api_name; int _maple_port; bool _detecting_button = false; @@ -184,7 +184,11 @@ class GamepadDevice std::map lastAxisValue[4]; bool perGameMapping = false; bool instanceMapping = false; - + + u64 lastAnalogUpdate = 0; + u32 rampAnalogState[4] {}; + static constexpr float AnalogRamp = 32767.f / 100.f; // 100 ms ramp time + static std::vector> _gamepads; static std::mutex _gamepads_mutex; }; diff --git a/core/oslib/oslib.cpp b/core/oslib/oslib.cpp index fa7a3793e..67bc3393b 100644 --- a/core/oslib/oslib.cpp +++ b/core/oslib/oslib.cpp @@ -40,6 +40,7 @@ #include #endif #include "profiler/fc_profiler.h" +#include "input/gamepad_device.h" namespace hostfs { @@ -370,12 +371,12 @@ void os_UpdateInputState() { FC_PROFILE_SCOPE; + // FIXME threading (android) this will be called on the render thread, events are on the main app thread + GamepadDevice::RampAnalog(); #if defined(USE_SDL) input_sdl_handle(); -#else - #if defined(USE_EVDEV) - input_evdev_handle(); - #endif +#elif defined(USE_EVDEV) + input_evdev_handle(); #endif } From 6bf38e50854490f9797d18a6ee60e63e401e0c15 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 26 Nov 2024 19:29:40 +0100 Subject: [PATCH 31/81] input: fix potential race condition with analog ramp up/down Fix a potential race condition on android causing missed inputs with analog axes bound to buttons. --- core/input/gamepad_device.cpp | 27 ++++++++++----------------- core/input/gamepad_device.h | 4 ++++ core/oslib/oslib.cpp | 1 - 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/core/input/gamepad_device.cpp b/core/input/gamepad_device.cpp index 5ddc72c6b..5dcb38b56 100644 --- a/core/input/gamepad_device.cpp +++ b/core/input/gamepad_device.cpp @@ -447,24 +447,18 @@ bool GamepadDevice::find_mapping(int system /* = settings.platform.system */) return false; } -int GamepadDevice::GetGamepadCount() -{ - _gamepads_mutex.lock(); - int count = _gamepads.size(); - _gamepads_mutex.unlock(); - return count; +int GamepadDevice::GetGamepadCount() { + Lock _(_gamepads_mutex); + return _gamepads.size(); } std::shared_ptr GamepadDevice::GetGamepad(int index) { - _gamepads_mutex.lock(); - std::shared_ptr dev; + Lock _(_gamepads_mutex); if (index >= 0 && index < (int)_gamepads.size()) - dev = _gamepads[index]; + return _gamepads[index]; else - dev = NULL; - _gamepads_mutex.unlock(); - return dev; + return nullptr; } void GamepadDevice::save_mapping(int system /* = settings.platform.system */) @@ -557,21 +551,19 @@ void GamepadDevice::Register(const std::shared_ptr& gamepad) setbuf(record_input, NULL); } #endif - _gamepads_mutex.lock(); + Lock _(_gamepads_mutex); _gamepads.push_back(gamepad); - _gamepads_mutex.unlock(); MapleConfigMap::UpdateVibration = updateVibration; } void GamepadDevice::Unregister(const std::shared_ptr& gamepad) { - _gamepads_mutex.lock(); + Lock _(_gamepads_mutex); for (auto it = _gamepads.begin(); it != _gamepads.end(); it++) if (*it == gamepad) { _gamepads.erase(it); break; } - _gamepads_mutex.unlock(); } void GamepadDevice::SaveMaplePorts() @@ -613,6 +605,7 @@ s16 (&GamepadDevice::getTargetArray(DigAnalog axis))[4] void GamepadDevice::rampAnalog() { + Lock _(rampMutex); if (lastAnalogUpdate == 0) // also used as a flag that no analog ramping is needed on this device (yet) return; @@ -655,7 +648,7 @@ void GamepadDevice::rampAnalog() void GamepadDevice::RampAnalog() { - std::lock_guard _(_gamepads_mutex); + Lock _(_gamepads_mutex); for (auto& gamepad : _gamepads) gamepad->rampAnalog(); } diff --git a/core/input/gamepad_device.h b/core/input/gamepad_device.h index 6e94b70ad..2799b7abf 100644 --- a/core/input/gamepad_device.h +++ b/core/input/gamepad_device.h @@ -160,6 +160,7 @@ class GamepadDevice { if (port < 0) return; + Lock _(rampMutex); DigAnalog axis = key == DcNegDir ? NegDir : PosDir; if (pressed) digitalToAnalogState[port] |= axis; @@ -188,9 +189,12 @@ class GamepadDevice u64 lastAnalogUpdate = 0; u32 rampAnalogState[4] {}; static constexpr float AnalogRamp = 32767.f / 100.f; // 100 ms ramp time + std::mutex rampMutex; static std::vector> _gamepads; static std::mutex _gamepads_mutex; + + using Lock = std::lock_guard; }; #ifdef TEST_AUTOMATION diff --git a/core/oslib/oslib.cpp b/core/oslib/oslib.cpp index 67bc3393b..c0df0eba8 100644 --- a/core/oslib/oslib.cpp +++ b/core/oslib/oslib.cpp @@ -371,7 +371,6 @@ void os_UpdateInputState() { FC_PROFILE_SCOPE; - // FIXME threading (android) this will be called on the render thread, events are on the main app thread GamepadDevice::RampAnalog(); #if defined(USE_SDL) input_sdl_handle(); From b42387e929ec7e1538c2e84f22e4dda66e46c942 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 28 Nov 2024 16:41:30 +0100 Subject: [PATCH 32/81] naomi: vf4 and mazan input descriptors --- core/hw/naomi/naomi_roms.cpp | 31 ++++++++++++++++--------------- core/hw/naomi/naomi_roms_input.h | 12 ++++++++++++ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/core/hw/naomi/naomi_roms.cpp b/core/hw/naomi/naomi_roms.cpp index c6315a1a8..e1ce88de2 100644 --- a/core/hw/naomi/naomi_roms.cpp +++ b/core/hw/naomi/naomi_roms.cpp @@ -2475,8 +2475,7 @@ const Game Games[] = { "maz1ma8.4d", 0x8000000, 0x1000000, 0xd46c9f40 }, }, nullptr, - // ENTER, START BUTTON - nullptr, + &mazan_inputs, // no free play with eeprom }, // Mazan: Flash of the Blade (US, MAZ3 Ver.A) @@ -2501,7 +2500,7 @@ const Game Games[] = { "maz1ma8.4d", 0x8000000, 0x1000000, 0xd46c9f40 }, }, nullptr, - nullptr, + &mazan_inputs, // no free play with eeprom }, // Mazan: Flash of the Blade (Japan, MAZ1 Ver.A) @@ -2526,7 +2525,7 @@ const Game Games[] = { "maz1ma8.4d", 0x8000000, 0x1000000, 0xd46c9f40 }, }, nullptr, - nullptr, + &mazan_inputs, // no free play with eeprom }, // Mushiking The King Of Beetles 2004 Second (Japan) @@ -7273,7 +7272,9 @@ const Game Games[] = { "mpr-23782.ic9", 0x8800000, 0x1000000, 0x4f72e901 }, { "mpr-23783.ic10", 0x9800000, 0x1000000, 0xc8d4f6f9 }, { "mpr-23784.ic11", 0xa800000, 0x1000000, 0xf74f2fee }, - } + }, + nullptr, + &vf4_inputs, }, { "kingrt66", @@ -7396,7 +7397,7 @@ const Game Games[] = { "copy", 0x400000, 0xc00000, 0x00000000, Copy, 0x1000000 }, }, nullptr, - nullptr, + &vf4_inputs, vf4evo_eeprom_dump, }, { @@ -7765,7 +7766,7 @@ const Game Games[] = { "317-0314-com.pic", 0, 0x4000, 0xfa0b6c70 }, }, "gds-0012", - nullptr, + &vf4_inputs, vf4_eeprom_dump }, { @@ -7781,7 +7782,7 @@ const Game Games[] = { "317-0314-com.pic", 0, 0x4000, 0xfa0b6c70 }, }, "gds-0012b", - nullptr, + &vf4_inputs, vf4_eeprom_dump }, { @@ -7797,7 +7798,7 @@ const Game Games[] = { "317-0314-com.pic", 0, 0x4000, 0xfa0b6c70 }, }, "gds-0012c", - nullptr, + &vf4_inputs, vf4_eeprom_dump }, { @@ -7857,7 +7858,7 @@ const Game Games[] = { "317-0338-jpn.pic", 0, 0x4000, 0xb177ba7d }, }, "gds-0024c", - nullptr, + &vf4_inputs, vf4evo_eeprom_dump, }, { @@ -7873,7 +7874,7 @@ const Game Games[] = { "317-0338-jpn.pic", 0, 0x4000, 0xb177ba7d }, }, "gds-0024b", - nullptr, + &vf4_inputs, vf4evo_eeprom_dump, }, { @@ -7889,7 +7890,7 @@ const Game Games[] = { "317-0338-jpn.pic", 0, 0x4000, 0xb177ba7d }, }, "gds-0024a", - nullptr, + &vf4_inputs, vf4evo_eeprom_dump, }, { @@ -8070,7 +8071,7 @@ const Game Games[] = { "317-0387-com.pic", 0, 0x4000, 0x8728aeaa }, }, "gds-0036f", - nullptr, + &vf4_inputs, vf4tuned_eeprom_dump, }, { @@ -8086,7 +8087,7 @@ const Game Games[] = { "317-0387-com.pic", 0, 0x4000, 0x8728aeaa }, }, "gds-0036d", - nullptr, + &vf4_inputs, vf4tuned_eeprom_dump, }, { @@ -8102,7 +8103,7 @@ const Game Games[] = { "317-0387-com.pic", 0, 0x4000, 0x8728aeaa }, }, "gds-0036a", - nullptr, + &vf4_inputs, vf4tuned_eeprom_dump, }, { diff --git a/core/hw/naomi/naomi_roms_input.h b/core/hw/naomi/naomi_roms_input.h index 1e2245281..1c07768e2 100644 --- a/core/hw/naomi/naomi_roms_input.h +++ b/core/hw/naomi/naomi_roms_input.h @@ -261,6 +261,16 @@ static InputDescriptors ninjaslt_inputs = { }, }; +static InputDescriptors mazan_inputs = { + { + { NAOMI_BTN0_KEY, "TRIGGER" }, + { NAOMI_UP_KEY, "SELECT UP" }, + { NAOMI_DOWN_KEY, "SELECT DOWN" }, + NAO_START_DESC + NAO_BASE_BTN_DESC + }, +}; + static InputDescriptors vonot_inputs = { { { NAOMI_UP_KEY, "L UP" }, @@ -484,6 +494,8 @@ static InputDescriptors shootout_inputs = { } }; +static InputDescriptors vf4_inputs = INPUT_3_BUTTONS("PUNCH", "KICK", "GUARD"); + // // AtomisWave games // From 59c2343cad6d356aea7e73591434840939a23aad Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 28 Nov 2024 16:46:17 +0100 Subject: [PATCH 33/81] android: add mime-type argument to addStorage() --- core/oslib/storage.cpp | 6 +++--- core/oslib/storage.h | 4 ++-- .../src/main/java/com/flycast/emulator/AndroidStorage.java | 7 +++++-- .../flycast/src/main/jni/src/android_storage.h | 7 ++++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/core/oslib/storage.cpp b/core/oslib/storage.cpp index 7fc2db3a1..97d21f1ce 100644 --- a/core/oslib/storage.cpp +++ b/core/oslib/storage.cpp @@ -42,7 +42,7 @@ CustomStorage& customStorage() FileInfo getFileInfo(const std::string& path) override { die("Not implemented"); } bool exists(const std::string& path) override { die("Not implemented"); } bool addStorage(bool isDirectory, bool writeAccess, const std::string& description, - void (*callback)(bool cancelled, std::string selectedPath)) override { + void (*callback)(bool cancelled, std::string selectedPath), const std::string& mimeType) override { die("Not implemented"); } }; @@ -394,9 +394,9 @@ AllStorage& storage() } bool addStorage(bool isDirectory, bool writeAccess, const std::string& description, - void (*callback)(bool cancelled, std::string selectedPath)) + void (*callback)(bool cancelled, std::string selectedPath), const std::string& mimeType) { - return customStorage().addStorage(isDirectory, writeAccess, description, callback); + return customStorage().addStorage(isDirectory, writeAccess, description, callback, mimeType); } } diff --git a/core/oslib/storage.h b/core/oslib/storage.h index a53e2457f..47281dccc 100644 --- a/core/oslib/storage.h +++ b/core/oslib/storage.h @@ -65,7 +65,7 @@ class CustomStorage : public Storage { public: virtual bool addStorage(bool isDirectory, bool writeAccess, const std::string& description, - void (*callback)(bool cancelled, std::string selectedPath)) = 0; + void (*callback)(bool cancelled, std::string selectedPath), const std::string& mimeType) = 0; }; class AllStorage : public Storage @@ -84,7 +84,7 @@ class AllStorage : public Storage AllStorage& storage(); bool addStorage(bool isDirectory, bool writeAccess, const std::string& description, - void (*callback)(bool cancelled, std::string selectedPath)); + void (*callback)(bool cancelled, std::string selectedPath), const std::string& mimeType = {}); // iterate depth-first over the files contained in a folder hierarchy class DirectoryTree diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java index 0cf835384..5533e4482 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java @@ -293,14 +293,17 @@ public String mkdir(String parent, String name) throws FileNotFoundException return dir.getAbsolutePath(); } - public boolean addStorage(boolean isDirectory, boolean writeAccess, String description) + public boolean addStorage(boolean isDirectory, boolean writeAccess, String description, String mimeType) { if (isDirectory && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return false; Intent intent = new Intent(isDirectory ? Intent.ACTION_OPEN_DOCUMENT_TREE : Intent.ACTION_OPEN_DOCUMENT); if (!isDirectory) { intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("application/*"); + if (!mimeType.isEmpty()) + intent.setType(mimeType); + else + intent.setType("application/*"); } intent = Intent.createChooser(intent, description); storageIntentPerms = Intent.FLAG_GRANT_READ_URI_PERMISSION | (writeAccess ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION : 0); diff --git a/shell/android-studio/flycast/src/main/jni/src/android_storage.h b/shell/android-studio/flycast/src/main/jni/src/android_storage.h index 5476f391f..14a9ce3b2 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_storage.h +++ b/shell/android-studio/flycast/src/main/jni/src/android_storage.h @@ -38,7 +38,7 @@ class AndroidStorage : public CustomStorage jgetSubPath = env->GetMethodID(clazz, "getSubPath", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); jgetFileInfo = env->GetMethodID(clazz, "getFileInfo", "(Ljava/lang/String;)Lcom/flycast/emulator/FileInfo;"); jexists = env->GetMethodID(clazz, "exists", "(Ljava/lang/String;)Z"); - jaddStorage = env->GetMethodID(clazz, "addStorage", "(ZZLjava/lang/String;)Z"); + jaddStorage = env->GetMethodID(clazz, "addStorage", "(ZZLjava/lang/String;Ljava/lang/String;)Z"); jsaveScreenshot = env->GetMethodID(clazz, "saveScreenshot", "(Ljava/lang/String;[B)V"); jimportHomeDirectory = env->GetMethodID(clazz, "importHomeDirectory", "()V"); jexportHomeDirectory = env->GetMethodID(clazz, "exportHomeDirectory", "()V"); @@ -136,12 +136,13 @@ class AndroidStorage : public CustomStorage } bool addStorage(bool isDirectory, bool writeAccess, const std::string& description, - void (*callback)(bool cancelled, std::string selectedPath)) override + void (*callback)(bool cancelled, std::string selectedPath), const std::string& mimeType) override { if (!config::UseSafFilePicker && !jni::env()->CallBooleanMethod(jstorage, jrequiresSafFilePicker)) return false; jni::String jdesc(description); - bool ret = jni::env()->CallBooleanMethod(jstorage, jaddStorage, isDirectory, writeAccess, (jstring)jdesc); + jni::String jmimeType(mimeType); + bool ret = jni::env()->CallBooleanMethod(jstorage, jaddStorage, isDirectory, writeAccess, (jstring)jdesc, (jstring)jmimeType); checkException(); if (ret) addStorageCallback = callback; From 5e9c168fc9998438f5728ef434662a652d162fc7 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 28 Nov 2024 17:01:39 +0100 Subject: [PATCH 34/81] android: add BooleanArray to jni util. refactor using templates --- .../flycast/src/main/jni/src/jni_util.h | 186 ++++++++++++------ 1 file changed, 123 insertions(+), 63 deletions(-) diff --git a/shell/android-studio/flycast/src/main/jni/src/jni_util.h b/shell/android-studio/flycast/src/main/jni/src/jni_util.h index 30f7d2436..2fc0b992a 100644 --- a/shell/android-studio/flycast/src/main/jni/src/jni_util.h +++ b/shell/android-studio/flycast/src/main/jni/src/jni_util.h @@ -249,99 +249,143 @@ class ObjectArray : public Array } }; -class ByteArray : public Array +class ByteArray; +class IntArray; +class ShortArray; +class BooleanArray; + +namespace detail { -public: - using jtype = jbyteArray; +// Use a traits type and specializations to define types needed in the base CRTP template. +template struct JniArrayTraits; +template <> struct JniArrayTraits { + using jtype = jbyteArray; + using ctype = u8; + using vtype = ctype; + static constexpr char const * JNISignature = "[B"; +}; +template <> struct JniArrayTraits { + using jtype = jintArray; + using ctype = int; + using vtype = ctype; + static constexpr char const * JNISignature = "[I"; +}; +template <> struct JniArrayTraits { + using jtype = jshortArray; + using ctype = short; + using vtype = ctype; + static constexpr char const * JNISignature = "[S"; +}; +template <> struct JniArrayTraits { + using jtype = jbooleanArray; + using ctype = bool; + using vtype = u8; // avoid std::vector abomination + static constexpr char const * JNISignature = "[Z"; +}; +} - ByteArray(jobject array = nullptr, bool ownRef = true, bool globalRef = false) : Array(array, ownRef, globalRef) { } - ByteArray(ByteArray &&other) : Array(std::move(other)) {} - explicit ByteArray(size_t size) : ByteArray() { - object = env()->NewByteArray(size); - } +template +class PrimitiveArray : public Array +{ + using ctype = typename detail::JniArrayTraits::ctype; + using vtype = typename detail::JniArrayTraits::vtype; - ByteArray& operator=(const ByteArray& other) { - return (ByteArray&)Object::operator=(other); - } +public: + using jtype = typename detail::JniArrayTraits::jtype; - operator jbyteArray() const { return (jbyteArray)object; } + PrimitiveArray(jobject array = nullptr, bool ownRef = true, bool globalRef = false) : Array(array, ownRef, globalRef) { } + PrimitiveArray(PrimitiveArray &&other) : Array(std::move(other)) {} - void getData(u8 *dst, size_t first = 0, size_t len = 0) const { + operator jtype() const { return static_cast(object); } + + void getData(ctype *dst, size_t first = 0, size_t len = 0) const + { if (len == 0) len = size(); if (len != 0) - env()->GetByteArrayRegion((jbyteArray)object, first, len, (jbyte *)dst); + static_cast(this)->getJavaArrayRegion(object, first, len, dst); } - void setData(const u8 *src, size_t first = 0, size_t len = 0) { + void setData(const ctype *src, size_t first = 0, size_t len = 0) + { if (len == 0) len = size(); - env()->SetByteArrayRegion((jbyteArray)object, first, len, (const jbyte *)src); + static_cast(this)->setJavaArrayRegion(object, first, len, src); } - operator std::vector() const + operator std::vector() const { - std::vector v; + std::vector v; v.resize(size()); - getData(v.data()); + getData(static_cast(v.data())); return v; } static Class getClass() { - return Class(env()->FindClass("[B")); + return Class(env()->FindClass(detail::JniArrayTraits::JNISignature)); } }; -class IntArray : public Array +class ByteArray : public PrimitiveArray { + using super = PrimitiveArray; + friend super; + public: - using jtype = jintArray; + ByteArray(jobject array = nullptr, bool ownRef = true, bool globalRef = false) : super(array, ownRef, globalRef) { } + ByteArray(ByteArray &&other) : super(std::move(other)) {} + explicit ByteArray(size_t size) : ByteArray() { + object = env()->NewByteArray(size); + } - IntArray(jobject array = nullptr, bool ownRef = true, bool globalRef = false) : Array(array, ownRef, globalRef) { } - IntArray(IntArray &&other) : Array(std::move(other)) {} - explicit IntArray(size_t size) : IntArray() { - object = env()->NewIntArray(size); + ByteArray& operator=(const ByteArray& other) { + return (ByteArray&)Object::operator=(other); } - IntArray& operator=(const IntArray& other) { - return (IntArray&)Object::operator=(other); +protected: + void getJavaArrayRegion(jobject object, size_t first, size_t len, u8 *dst) const { + env()->GetByteArrayRegion((jbyteArray)object, first, len, (jbyte *)dst); } - operator jintArray() const { return (jintArray)object; } + void setJavaArrayRegion(jobject object, size_t first, size_t len, const u8 *dst) { + env()->SetByteArrayRegion((jbyteArray)object, first, len, (const jbyte *)dst); + } +}; - void getData(int *dst, size_t first = 0, size_t len = 0) const { - if (len == 0) - len = size(); - if (len != 0) - env()->GetIntArrayRegion((jintArray)object, first, len, (jint *)dst); +class IntArray : public PrimitiveArray +{ + using super = PrimitiveArray; + friend super; + +public: + IntArray(jobject array = nullptr, bool ownRef = true, bool globalRef = false) : super(array, ownRef, globalRef) { } + IntArray(IntArray &&other) : super(std::move(other)) {} + explicit IntArray(size_t size) : IntArray() { + object = env()->NewIntArray(size); } - void setData(const int *src, size_t first = 0, size_t len = 0) { - if (len == 0) - len = size(); - env()->SetIntArrayRegion((jintArray)object, first, len, (const jint *)src); + IntArray& operator=(const IntArray& other) { + return (IntArray&)Object::operator=(other); } - operator std::vector() const - { - std::vector v; - v.resize(size()); - getData(v.data()); - return v; +protected: + void getJavaArrayRegion(jobject object, size_t first, size_t len, int *dst) const { + env()->GetIntArrayRegion((jintArray)object, first, len, (jint *)dst); } - static Class getClass() { - return Class(env()->FindClass("[I")); + void setJavaArrayRegion(jobject object, size_t first, size_t len, const int *dst) { + env()->SetIntArrayRegion((jintArray)object, first, len, (const jint *)dst); } }; -class ShortArray : public Array +class ShortArray : public PrimitiveArray { -public: - using jtype = jshortArray; + using super = PrimitiveArray; + friend super; - ShortArray(jobject array = nullptr, bool ownRef = true, bool globalRef = false) : Array(array, ownRef, globalRef) { } - ShortArray(ShortArray &&other) : Array(std::move(other)) {} +public: + ShortArray(jobject array = nullptr, bool ownRef = true, bool globalRef = false) : super(array, ownRef, globalRef) { } + ShortArray(ShortArray &&other) : super(std::move(other)) {} explicit ShortArray(size_t size) : ShortArray() { object = env()->NewShortArray(size); } @@ -350,23 +394,39 @@ class ShortArray : public Array return (ShortArray&)Object::operator=(other); } - operator jshortArray() const { return (jshortArray)object; } +protected: + void getJavaArrayRegion(jobject object, size_t first, size_t len, short *dst) const { + env()->GetShortArrayRegion((jshortArray)object, first, len, (jshort *)dst); + } - void getData(short *dst, size_t first = 0, size_t len = 0) { - if (len == 0) - len = size(); - if (len != 0) - env()->GetShortArrayRegion((jshortArray)object, first, len, (jshort *)dst); + void setJavaArrayRegion(jobject object, size_t first, size_t len, const short *dst) { + env()->SetShortArrayRegion((jshortArray)object, first, len, (const jshort *)dst); } +}; - void setData(const short *src, size_t first = 0, size_t len = 0) { - if (len == 0) - len = size(); - env()->SetShortArrayRegion((jshortArray)object, first, len, (const jshort *)src); +class BooleanArray : public PrimitiveArray +{ + using super = PrimitiveArray; + friend super; + +public: + BooleanArray(jobject array = nullptr, bool ownRef = true, bool globalRef = false) : super(array, ownRef, globalRef) { } + BooleanArray(BooleanArray &&other) : super(std::move(other)) {} + explicit BooleanArray(size_t size) : BooleanArray() { + object = env()->NewBooleanArray(size); } - static Class getClass() { - return Class(env()->FindClass("[S")); + BooleanArray& operator=(const BooleanArray& other) { + return (BooleanArray&)Object::operator=(other); + } + +protected: + void getJavaArrayRegion(jobject object, size_t first, size_t len, bool *dst) const { + env()->GetBooleanArrayRegion((jbooleanArray)object, first, len, (jboolean *)dst); + } + + void setJavaArrayRegion(jobject object, size_t first, size_t len, const bool *dst) { + env()->SetBooleanArrayRegion((jbooleanArray)object, first, len, (const jboolean *)dst); } }; From 36c2f5137cd0569c7e1d0243288b3947a89433c7 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 28 Nov 2024 17:09:09 +0100 Subject: [PATCH 35/81] ios, android: load custom vgamepad image. disable unused controls User can choose a custom vgamepad image or reset to default. Unused controls are disabled and hidden based on the game (arcade) or maple device (console). --- core/ui/gui.cpp | 48 ++- core/ui/gui_android.cpp | 339 +++++++++++++++--- core/ui/gui_android.h | 13 + .../flycast/emulator/NativeGLActivity.java | 3 + .../flycast/emulator/emu/NativeGLView.java | 3 + .../emulator/emu/VirtualJoystickDelegate.java | 100 +++--- .../flycast/src/main/jni/src/Android.cpp | 10 + 7 files changed, 410 insertions(+), 106 deletions(-) diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 57015c632..331b8baee 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -111,6 +111,7 @@ static void emuEventCallback(Event event, void *) { case Event::Resume: game_started = true; + vgamepad::startGame(); break; case Event::Start: GamepadDevice::load_system_mappings(); @@ -1400,13 +1401,21 @@ static void controller_mapping_popup(const std::shared_ptr& gamep } } +static void gamepadPngFileSelected(bool cancelled, std::string path) +{ + if (!cancelled) + gui_runOnUiThread([path]() { + vgamepad::loadImage(path); + }); +} + static void gamepadSettingsPopup(const std::shared_ptr& gamepad) { centerNextWindow(); ImGui::SetNextWindowSize(min(ImGui::GetIO().DisplaySize, ScaledVec2(450.f, 300.f))); ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0); - if (ImGui::BeginPopupModal("Gamepad Settings", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + if (ImGui::BeginPopupModal("Gamepad Settings", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_DragScrolling)) { if (ImGui::Button("Done", ScaledVec2(100, 30))) { @@ -1449,12 +1458,38 @@ static void gamepadSettingsPopup(const std::shared_ptr& gamepad) ImGui::NewLine(); if (gamepad->is_virtual_gamepad()) { -#ifdef __ANDROID__ - header("Haptic"); - OptionSlider("Power", config::VirtualGamepadVibration, 0, 100, "Haptic feedback power", "%d%%"); -#endif + if (gamepad->is_rumble_enabled()) { + header("Haptic"); + OptionSlider("Power", config::VirtualGamepadVibration, 0, 100, "Haptic feedback power", "%d%%"); + } header("View"); OptionSlider("Transparency", config::VirtualGamepadTransparency, 0, 100, "Virtual gamepad buttons transparency", "%d%%"); + +#if defined(__ANDROID__) || defined(TARGET_IPHONE) + vgamepad::ImguiVGamepadTexture tex; + ImGui::Image(tex.getId(), ScaledVec2(300, 112.5f), ImVec2(0, 1), ImVec2(1, 0.25f)); +#endif + const char *gamepadPngTitle = "Select a PNG file"; + if (ImGui::Button("Choose Image...", ScaledVec2(150, 30))) +#ifdef __ANDROID__ + { + if (!hostfs::addStorage(false, false, gamepadPngTitle, gamepadPngFileSelected, "image/png")) + ImGui::OpenPopup(gamepadPngTitle); + } +#else + { + ImGui::OpenPopup(gamepadPngTitle); + } +#endif + ImGui::SameLine(); + if (ImGui::Button("Use Default", ScaledVec2(150, 30))) + vgamepad::loadImage(""); + + select_file_popup(gamepadPngTitle, [](bool cancelled, std::string selection) + { + gamepadPngFileSelected(cancelled, selection); + return true; + }, true, "png"); } else if (gamepad->is_rumble_enabled()) { @@ -1483,6 +1518,8 @@ static void gamepadSettingsPopup(const std::shared_ptr& gamepad) ShowHelpMarker("Value sent to the game at 100% thumbstick deflection. " "Values greater than 100% will saturate before full deflection of the thumbstick."); } + scrollWhenDraggingOnVoid(); + windowDragScroll(); ImGui::EndPopup(); } } @@ -2996,6 +3033,7 @@ static void gui_display_settings() { maple_ReconnectDevices(); reset_vmus(); + vgamepad::startGame(); } } SaveSettings(); diff --git a/core/ui/gui_android.cpp b/core/ui/gui_android.cpp index 4c9ba44af..1705d8a59 100644 --- a/core/ui/gui_android.cpp +++ b/core/ui/gui_android.cpp @@ -22,14 +22,16 @@ #include "gui.h" #include "stdclass.h" #include "imgui.h" -#include "gui_util.h" #include "rend/osd.h" #include "imgui_driver.h" #include "input/gamepad.h" #include "input/gamepad_device.h" +#include "oslib/storage.h" #include "oslib/resources.h" #include "cfg/cfg.h" #include "input/gamepad.h" +#include "hw/naomi/naomi_cart.h" +#include "hw/maple/maple_devs.h" #include namespace vgamepad @@ -47,12 +49,14 @@ struct Control ImVec2 size; ImVec2 uv0; ImVec2 uv1; + bool disabled = false; }; static Control Controls[_Count]; static bool Visible = true; static float AlphaTrans = 1.f; static ImVec2 StickPos; // analog stick position [-1, 1] constexpr char const *BTN_PATH = "picture/buttons.png"; +constexpr char const *CFG_SECTION = "vgamepad"; void displayCommands() { @@ -83,58 +87,74 @@ void displayCommands() ImGui::End(); } -static u8 *loadOSDButtons(int &width, int &height) +static bool loadOSDButtons(const std::string& path) { - int n; - stbi_set_flip_vertically_on_load(1); - - FILE *file = nowide::fopen(get_readonly_data_path("buttons.png").c_str(), "rb"); + if (path.empty()) + return false; + FILE *file = hostfs::storage().openFile(path, "rb"); if (file == nullptr) - // also try the home folder (android) - file = nowide::fopen(get_readonly_config_path("buttons.png").c_str(), "rb"); - u8 *image_data = nullptr; - if (file != nullptr) - { - image_data = stbi_load_from_file(file, &width, &height, &n, STBI_rgb_alpha); - std::fclose(file); - } + return false; + + stbi_set_flip_vertically_on_load(1); + int width, height, n; + u8 *image_data = stbi_load_from_file(file, &width, &height, &n, STBI_rgb_alpha); + std::fclose(file); if (image_data == nullptr) - { - size_t size; - std::unique_ptr data = resource::load(BTN_PATH, size); - image_data = stbi_load_from_memory(data.get(), (int)size, &width, &height, &n, STBI_rgb_alpha); - } - return image_data; + return false; + try { + imguiDriver->updateTexture(BTN_PATH, image_data, width, height, false); + } catch (...) { + // vulkan can throw during resizing + } + free(image_data); + + return true; } -class ImguiVGamepadTexture : public ImguiTexture +static ImTextureID loadOSDButtons() { -public: - ImTextureID getId() override - { - ImTextureID id = imguiDriver->getTexture(BTN_PATH); - if (id == ImTextureID()) - { - int width, height; - u8 *imgData = loadOSDButtons(width, height); - if (imgData != nullptr) - { - try { - id = imguiDriver->updateTextureAndAspectRatio(BTN_PATH, imgData, width, height, nearestSampling); - } catch (...) { - // vulkan can throw during resizing - } - free(imgData); - } - } + ImTextureID id{}; + // custom image + std::string path = cfgLoadStr(CFG_SECTION, "image", ""); + if (loadOSDButtons(path)) return id; - } -}; + // legacy buttons.png in data folder + if (loadOSDButtons(get_readonly_data_path("buttons.png"))) + return id; + // also try the home folder (android) + if (loadOSDButtons(get_readonly_config_path("buttons.png"))) + return id; + // default in resource + size_t size; + std::unique_ptr data = resource::load(BTN_PATH, size); + stbi_set_flip_vertically_on_load(1); + int width, height, n; + u8 *image_data = stbi_load_from_memory(data.get(), (int)size, &width, &height, &n, STBI_rgb_alpha); + if (image_data != nullptr) + { + try { + id = imguiDriver->updateTexture(BTN_PATH, image_data, width, height, false); + } catch (...) { + // vulkan can throw during resizing + } + free(image_data); + } + return id; +} + +ImTextureID ImguiVGamepadTexture::getId() +{ + ImTextureID id = imguiDriver->getTexture(BTN_PATH); + if (id == ImTextureID()) + id = loadOSDButtons(); -constexpr float vjoy_sz[2][14] = { - // L U R D X Y B A St LT RT Ana Stck FF - { 64,64,64,64, 64,64,64,64, 64, 90,90, 128, 64, 64 }, - { 64,64,64,64, 64,64,64,64, 64, 64,64, 128, 64, 64 }, + return id; +} + +constexpr float vjoy_sz[2][_Count] = { + // L U R D X Y B A St LT RT Ana Stck FF LU RU LD RD + { 64,64,64,64, 64,64,64,64, 64, 90,90, 128, 64, 64, 64,64,64,64 }, + { 64,64,64,64, 64,64,64,64, 64, 64,64, 128, 64, 64, 64,64,64,64 }, }; constexpr float OSD_TEX_W = 512.f; @@ -186,8 +206,9 @@ void setPosition(ControlId id, float x, float y, float w, float h) ControlId hitTest(float x, float y) { for (const auto& control : Controls) - if (x >= control.pos.x && x < control.pos.x + control.size.x - && y >= control.pos.y && y < control.pos.y + control.size.y) + if (!control.disabled + && x >= control.pos.x && x < control.pos.x + control.size.x + && y >= control.pos.y && y < control.pos.y + control.size.y) return static_cast(&control - &Controls[0]); return None; } @@ -227,6 +248,8 @@ float getControlWidth(ControlId control) { static void drawButtonDim(ImDrawList *drawList, const Control& control, int state) { + if (control.disabled) + return; float scale_h = settings.display.height / 480.f; float offs_x = (settings.display.width - scale_h * 640.f) / 2.f; ImVec2 pos = control.pos * scale_h; @@ -284,7 +307,7 @@ void draw() } static float getUIScale() { - // scale is 1.1@ for a 320 dpi screen of height 750 + // scale is 1.1 for a 320 dpi screen of height 750 return 1.1f * 750.f / settings.display.height * settings.display.dpi / 320.f; } @@ -300,15 +323,15 @@ struct LayoutElement void load() { - x = cfgLoadFloat(SECTION, name + "_x", x); - y = cfgLoadFloat(SECTION, name + "_y", y); - scale = cfgLoadFloat(SECTION, name + "_scale", scale); + x = cfgLoadFloat(CFG_SECTION, name + "_x", x); + y = cfgLoadFloat(CFG_SECTION, name + "_y", y); + scale = cfgLoadFloat(CFG_SECTION, name + "_scale", scale); } void save() const { - cfgSaveFloat(SECTION, name + "_x", x); - cfgSaveFloat(SECTION, name + "_y", y); - cfgSaveFloat(SECTION, name + "_scale", scale); + cfgSaveFloat(CFG_SECTION, name + "_x", x); + cfgSaveFloat(CFG_SECTION, name + "_y", y); + cfgSaveFloat(CFG_SECTION, name + "_scale", scale); } bool hitTest(float nx, float ny) const { @@ -342,8 +365,6 @@ struct LayoutElement else y = 1.f - h + dy / 480.f * uiscale; } - - static constexpr char const *SECTION = "vgamepad"; }; static LayoutElement Layout[] { { "dpad", 32.f, -24.f, 192.f, 192.f }, @@ -476,9 +497,210 @@ void scaleElement(Element element, float factor) translateElement(element, -dx, -dy); } +void loadImage(const std::string& path) +{ + if (path.empty()) { + cfgSaveStr(CFG_SECTION, "image", ""); + loadOSDButtons(); + } + else if (loadOSDButtons(path)) { + cfgSaveStr(CFG_SECTION, "image", path); + } +} + +void enableAllControls() +{ + for (auto& control : Controls) + control.disabled = false; +} + +static void disableControl(ControlId ctrlId) +{ + Controls[ctrlId].disabled = true; + switch (ctrlId) + { + case Left: + Controls[LeftUp].disabled = true; + Controls[LeftDown].disabled = true; + break; + case Right: + Controls[RightUp].disabled = true; + Controls[RightDown].disabled = true; + break; + case Up: + Controls[LeftUp].disabled = true; + Controls[RightUp].disabled = true; + break; + case Down: + Controls[LeftDown].disabled = true; + Controls[RightDown].disabled = true; + break; + case AnalogArea: + case AnalogStick: + Controls[AnalogArea].disabled = true; + Controls[AnalogStick].disabled = true; + break; + default: + break; + } +} + +void startGame() +{ + enableAllControls(); + if (settings.platform.isConsole()) + { + switch (config::MapleMainDevices[0]) + { + case MDT_LightGun: + // TODO enable mouse? + disableControl(AnalogArea); + disableControl(LeftTrigger); + disableControl(RightTrigger); + disableControl(A); + disableControl(X); + disableControl(Y); + break; + case MDT_AsciiStick: + // TODO add CZ + disableControl(AnalogArea); + disableControl(LeftTrigger); + disableControl(RightTrigger); + break; + case MDT_PopnMusicController: + // TODO add C btn + disableControl(AnalogArea); + disableControl(LeftTrigger); + disableControl(RightTrigger); + break; + case MDT_RacingController: + disableControl(X); + disableControl(Y); + break; + default: + break; + } + } + else + { + // arcade game + // FIXME RT is used as mod key for coin, test, service (ABX) + // FIXME RT and LT are buttons 4 & 5 in arcade mode + // TODO insert card button for card games + if (NaomiGameInputs != nullptr) + { + bool fullAnalog = false; + bool rt = false; + bool lt = false; + for (const auto& axis : NaomiGameInputs->axes) + { + if (axis.name == nullptr) + break; + switch (axis.axis) + { + case 0: + case 1: + fullAnalog = true; + break; + case 4: + rt = true; + break; + case 5: + lt = true; + break; + } + } + if (!fullAnalog) + disableControl(AnalogArea); + u32 usedButtons = 0; + for (const auto& button : NaomiGameInputs->buttons) + { + if (button.name == nullptr) + break; + usedButtons |= button.source; + } + if (settings.platform.isAtomiswave()) + { + // button order: A B X Y RT + /* these ones are always needed for now + if ((usedButtons & AWAVE_BTN0_KEY) == 0) + disableControl(A); + if ((usedButtons & AWAVE_BTN1_KEY) == 0) + disableControl(B); + if ((usedButtons & AWAVE_BTN2_KEY) == 0) + disableControl(X); + if ((usedButtons & AWAVE_BTN4_KEY) == 0 && !rt) + disableControl(RightTrigger); + */ + if ((usedButtons & AWAVE_BTN3_KEY) == 0) + disableControl(Y); + if (!lt) + disableControl(LeftTrigger); + if ((usedButtons & AWAVE_UP_KEY) == 0) + disableControl(Up); + if ((usedButtons & AWAVE_DOWN_KEY) == 0) + disableControl(Down); + if ((usedButtons & AWAVE_LEFT_KEY) == 0) + disableControl(Left); + if ((usedButtons & AWAVE_RIGHT_KEY) == 0) + disableControl(Right); + } + else + { + /* these ones are always needed for now + if ((usedButtons & NAOMI_BTN0_KEY) == 0) + disableControl(A); + if ((usedButtons & NAOMI_BTN1_KEY) == 0) + disableControl(B); + if ((usedButtons & NAOMI_BTN2_KEY) == 0) + // C + disableControl(X); + if ((usedButtons & NAOMI_BTN4_KEY) == 0 && !rt) + // Y + disableControl(RightTrigger); + */ + if ((usedButtons & NAOMI_BTN3_KEY) == 0) + // X + disableControl(Y); + if ((usedButtons & NAOMI_BTN5_KEY) == 0 && !lt) + // Z + disableControl(LeftTrigger); + if ((usedButtons & NAOMI_UP_KEY) == 0) + disableControl(Up); + if ((usedButtons & NAOMI_DOWN_KEY) == 0) + disableControl(Down); + if ((usedButtons & NAOMI_LEFT_KEY) == 0) + disableControl(Left); + if ((usedButtons & NAOMI_RIGHT_KEY) == 0) + disableControl(Right); + } + } + else if (settings.input.lightgunGame) + { + disableControl(Y); + disableControl(AnalogArea); + disableControl(LeftTrigger); + disableControl(Up); + disableControl(Down); + disableControl(Left); + disableControl(Right); + } + else + { + // all analog games *should* have an input description + disableControl(AnalogArea); + } + } + bool enabledState[_Count]; + for (int i = 0; i < _Count; i++) + enabledState[i] = !Controls[i].disabled; + setEnabledControls(enabledState); +} + #ifndef __ANDROID__ void startEditing() { + enableAllControls(); show(); } @@ -497,7 +719,10 @@ void resetEditing() { resetLayout(); } +void setEnabledControls(bool enabled[_Count]) { +} + #endif } // namespace vgamepad -#endif // __ANDROID__ +#endif // __ANDROID__ || TARGET_IPHONE diff --git a/core/ui/gui_android.h b/core/ui/gui_android.h index a84bcd098..86b099aa7 100644 --- a/core/ui/gui_android.h +++ b/core/ui/gui_android.h @@ -18,6 +18,7 @@ */ #pragma once #include "types.h" +#include "gui_util.h" namespace vgamepad { @@ -60,9 +61,17 @@ enum Element Elem_FForward, }; +class ImguiVGamepadTexture : public ImguiTexture +{ +public: + ImTextureID getId() override; +}; + #if defined(__ANDROID__) || defined(TARGET_IPHONE) void setPosition(ControlId id, float x, float y, float w = 0.f, float h = 0.f); // Legacy android +void setEnabledControls(bool enabled[_Count]); // Legacy android +void enableAllControls(); void show(); void hide(); void draw(); @@ -71,6 +80,8 @@ void pauseEditing(); void stopEditing(bool canceled); void resetEditing(); void displayCommands(); +void loadImage(const std::string& path); +void startGame(); ControlId hitTest(float x, float y); u32 controlToDcKey(ControlId control); @@ -91,6 +102,8 @@ void startEditing() {} void pauseEditing() {} void displayCommands() {} void applyUiScale() {} +void loadImage(const std::string& path) {} +void startGame() {} #endif } // namespace vgamepad diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java index 9369b936f..4f84b6e2b 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java @@ -90,6 +90,9 @@ public void run() { } }); } + private void VJoyEnableControls(boolean[] state) { + mView.enableVjoy(state); + } // On-screen keyboard borrowed from SDL core android code class ShowTextInputTask implements Runnable { diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java index 0a928ac7c..b70cda441 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java @@ -186,4 +186,7 @@ public void readCustomVjoyValues() { public void setEditVjoyMode(boolean editVjoyMode) { vjoyDelegate.setEditVjoyMode(editVjoyMode); } + public void enableVjoy(boolean[] state) { + vjoyDelegate.enableVjoy(state); + } } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java index aaebc99dd..813ef7981 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java @@ -12,6 +12,8 @@ import com.flycast.emulator.periph.VJoy; import com.flycast.emulator.periph.VibratorThread; +import java.util.Arrays; + public class VirtualJoystickDelegate { private VibratorThread vibratorThread; @@ -28,6 +30,7 @@ public void run() { }; private float[][] vjoy_d_custom; + private boolean[] vjoy_enabled; private static final float[][] vjoy = VJoy.baseVJoy(); @@ -41,6 +44,8 @@ public VirtualJoystickDelegate(View view) { vibratorThread = VibratorThread.getInstance(); readCustomVjoyValues(); + vjoy_enabled = new boolean[VJoy.VJoyCount + 4]; // include diagonals + Arrays.fill(vjoy_enabled, true); scaleGestureDetector = new ScaleGestureDetector(context, new OscOnScaleGestureListener()); } @@ -215,62 +220,63 @@ public boolean onTouchEvent(MotionEvent event, int width, int height) continue; for (int j = 0; j < vjoy.length; j++) { - if (x > vjoy[j][0] && x <= (vjoy[j][0] + vjoy[j][2])) + if (!editVjoyMode && !vjoy_enabled[j]) + continue; + if (x > vjoy[j][0] && x <= (vjoy[j][0] + vjoy[j][2]) + && y > vjoy[j][1] && y <= (vjoy[j][1] + vjoy[j][3])) { - if (y > vjoy[j][1] && y <= (vjoy[j][1] + vjoy[j][3])) - { - if (vjoy[j][4] >= VJoy.BTN_RTRIG) { - // Not for analog - if (vjoy[j][5] == 0) - if (!editVjoyMode) { - vibratorThread.click(); - } - vjoy[j][5] = 2; - } + if (vjoy[j][4] >= VJoy.BTN_RTRIG) { + // Not for analog + if (vjoy[j][5] == 0) + if (!editVjoyMode) { + vibratorThread.click(); + } + vjoy[j][5] = 2; + } - if (vjoy[j][4] == VJoy.BTN_ANARING) { + if (vjoy[j][4] == VJoy.BTN_ANARING) { + if (editVjoyMode) { + selectedVjoyElement = VJoy.ELEM_ANALOG; + resetEditMode(); + } else { + vjoy[j + 1][0] = x - vjoy[j + 1][2] / 2; + vjoy[j + 1][1] = y - vjoy[j + 1][3] / 2; + + JNIdc.vjoy(j + 1, vjoy[j + 1][0], vjoy[j + 1][1], vjoy[j + 1][2], vjoy[j + 1][3]); + anal_id = event.getPointerId(i); + } + } else if (vjoy[j][4] != VJoy.BTN_ANAPOINT) { + if (vjoy[j][4] == VJoy.BTN_LTRIG) { if (editVjoyMode) { - selectedVjoyElement = VJoy.ELEM_ANALOG; + selectedVjoyElement = VJoy.ELEM_LTRIG; resetEditMode(); } else { - vjoy[j + 1][0] = x - vjoy[j + 1][2] / 2; - vjoy[j + 1][1] = y - vjoy[j + 1][3] / 2; - - JNIdc.vjoy(j + 1, vjoy[j + 1][0], vjoy[j + 1][1], vjoy[j + 1][2], vjoy[j + 1][3]); - anal_id = event.getPointerId(i); + left_trigger = 255; + lt_id = event.getPointerId(i); } - } else if (vjoy[j][4] != VJoy.BTN_ANAPOINT) { - if (vjoy[j][4] == VJoy.BTN_LTRIG) { - if (editVjoyMode) { - selectedVjoyElement = VJoy.ELEM_LTRIG; - resetEditMode(); - } else { - left_trigger = 255; - lt_id = event.getPointerId(i); - } - } else if (vjoy[j][4] == VJoy.BTN_RTRIG) { - if (editVjoyMode) { - selectedVjoyElement = VJoy.ELEM_RTRIG; - resetEditMode(); - } else { - right_trigger = 255; - rt_id = event.getPointerId(i); - } + } else if (vjoy[j][4] == VJoy.BTN_RTRIG) { + if (editVjoyMode) { + selectedVjoyElement = VJoy.ELEM_RTRIG; + resetEditMode(); } else { - if (editVjoyMode) { - selectedVjoyElement = getElementIdFromButtonId(j); - resetEditMode(); - } else if (vjoy[j][4] == VJoy.key_CONT_FFORWARD) - fastForward = true; - else - rv &= ~(int)vjoy[j][4]; + right_trigger = 255; + rt_id = event.getPointerId(i); } + } else { + if (editVjoyMode) { + selectedVjoyElement = getElementIdFromButtonId(j); + resetEditMode(); + } else if (vjoy[j][4] == VJoy.key_CONT_FFORWARD) + fastForward = true; + else + rv &= ~(int)vjoy[j][4]; } } } } - } else { + } else if (vjoy_enabled[11]) { + // Analog stick if (x < vjoy[11][0]) x = vjoy[11][0]; else if (x > (vjoy[11][0] + vjoy[11][2])) @@ -369,11 +375,17 @@ else if (event.getPointerId(event.getActionIndex())==rt_id) public void setEditVjoyMode(boolean editVjoyMode) { this.editVjoyMode = editVjoyMode; selectedVjoyElement = -1; - if (editVjoyMode) + if (editVjoyMode) { this.handler.removeCallbacks(hideVGamepadRunnable); + Arrays.fill(vjoy_enabled, true); + } resetEditMode(); } + public void enableVjoy(boolean[] state) { + vjoy_enabled = state; + } + private class OscOnScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index 3547f48b8..8b041cb0b 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -69,6 +69,7 @@ static jobject g_activity; static jmethodID VJoyStartEditingMID; static jmethodID VJoyStopEditingMID; static jmethodID VJoyResetEditingMID; +static jmethodID VJoyEnableControlsMID; static jmethodID showScreenKeyboardMid; static jmethodID onGameStateChangeMid; @@ -593,6 +594,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_BaseGLActivity_regis VJoyStartEditingMID = env->GetMethodID(actClass, "VJoyStartEditing", "()V"); VJoyStopEditingMID = env->GetMethodID(actClass, "VJoyStopEditing", "(Z)V"); VJoyResetEditingMID = env->GetMethodID(actClass, "VJoyResetEditing", "()V"); + VJoyEnableControlsMID = env->GetMethodID(actClass, "VJoyEnableControls", "([Z)V"); showScreenKeyboardMid = env->GetMethodID(actClass, "showScreenKeyboard", "(Z)V"); onGameStateChangeMid = env->GetMethodID(actClass, "onGameStateChange", "(Z)V"); } @@ -602,6 +604,7 @@ namespace vgamepad { void startEditing() { + enableAllControls(); jni::env()->CallVoidMethod(g_activity, VJoyStartEditingMID); } void pauseEditing() { @@ -614,6 +617,13 @@ void stopEditing(bool canceled) { jni::env()->CallVoidMethod(g_activity, VJoyStopEditingMID, canceled); } +void setEnabledControls(bool enabled[_Count]) +{ + jni::BooleanArray jb{_Count}; + jb.setData(enabled, 0, _Count); + jni::env()->CallVoidMethod(g_activity, VJoyEnableControlsMID, (jbooleanArray)jb); +} + } void enableNetworkBroadcast(bool enable) From a085fd2f6de16e265a51e1211ea83cf605f60ecc Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 28 Nov 2024 17:45:40 +0100 Subject: [PATCH 36/81] ui: never disable Up and Down buttons on iOS. rename source files --- CMakeLists.txt | 4 ++-- core/ui/gui.cpp | 2 +- core/ui/{gui_android.cpp => vgamepad.cpp} | 8 +++++++- core/ui/{gui_android.h => vgamepad.h} | 0 shell/android-studio/flycast/src/main/jni/src/Android.cpp | 2 +- .../apple/emulator-ios/emulator/EditPadViewController.mm | 2 +- shell/apple/emulator-ios/emulator/PadViewController.mm | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) rename core/ui/{gui_android.cpp => vgamepad.cpp} (99%) rename core/ui/{gui_android.h => vgamepad.h} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ff5a4493..24130ada9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1310,8 +1310,8 @@ if(NOT LIBRETRO) core/ui/gui.cpp core/ui/gui.h core/ui/gui_achievements.cpp - core/ui/gui_android.cpp - core/ui/gui_android.h + core/ui/vgamepad.cpp + core/ui/vgamepad.h core/ui/gui_chat.h core/ui/gui_cheats.cpp core/ui/gui_util.cpp diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 331b8baee..6d6ceb494 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -56,7 +56,7 @@ #include "sdl/sdl.h" #endif -#include "gui_android.h" +#include "vgamepad.h" #ifdef __ANDROID__ #if HOST_CPU == CPU_ARM64 && USE_VULKAN #include "rend/vulkan/adreno.h" diff --git a/core/ui/gui_android.cpp b/core/ui/vgamepad.cpp similarity index 99% rename from core/ui/gui_android.cpp rename to core/ui/vgamepad.cpp index 1705d8a59..b4d7e1d58 100644 --- a/core/ui/gui_android.cpp +++ b/core/ui/vgamepad.cpp @@ -18,7 +18,7 @@ */ #if defined(__ANDROID__) || defined(TARGET_IPHONE) -#include "gui_android.h" +#include "vgamepad.h" #include "gui.h" #include "stdclass.h" #include "imgui.h" @@ -516,6 +516,12 @@ void enableAllControls() static void disableControl(ControlId ctrlId) { +#ifdef TARGET_IPHONE + if (ctrlId == Up || ctrlId == Down) + // Needed to pause the emulator + return; +#endif + Controls[ctrlId].disabled = true; switch (ctrlId) { diff --git a/core/ui/gui_android.h b/core/ui/vgamepad.h similarity index 100% rename from core/ui/gui_android.h rename to core/ui/vgamepad.h diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index 8b041cb0b..3fa0fabc8 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -6,7 +6,7 @@ #include "audio/audiostream.h" #include "imgread/common.h" #include "ui/gui.h" -#include "ui/gui_android.h" +#include "ui/vgamepad.h" #include "rend/osd.h" #include "cfg/cfg.h" #include "log/LogManager.h" diff --git a/shell/apple/emulator-ios/emulator/EditPadViewController.mm b/shell/apple/emulator-ios/emulator/EditPadViewController.mm index 811e5a5f7..58c803a6b 100644 --- a/shell/apple/emulator-ios/emulator/EditPadViewController.mm +++ b/shell/apple/emulator-ios/emulator/EditPadViewController.mm @@ -19,7 +19,7 @@ #import "EditPadViewController.h" #include "types.h" #include "ui/gui.h" -#include "ui/gui_android.h" +#include "ui/vgamepad.h" #include "cfg/cfg.h" @interface EditPadViewController () { diff --git a/shell/apple/emulator-ios/emulator/PadViewController.mm b/shell/apple/emulator-ios/emulator/PadViewController.mm index 06035cff1..19b678ee5 100644 --- a/shell/apple/emulator-ios/emulator/PadViewController.mm +++ b/shell/apple/emulator-ios/emulator/PadViewController.mm @@ -23,7 +23,7 @@ #import "PadViewController.h" #include "ios_gamepad.h" #include "cfg/cfg.h" -#include "ui/gui_android.h" +#include "ui/vgamepad.h" @interface PadViewController () { UITouch *joyTouch; From c860807fefd958cc49edeff3e8e9270cda40abcb Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 30 Nov 2024 16:37:55 +0100 Subject: [PATCH 37/81] gdrom: reduce dma buffer size. reios: schedule dma reads by chunks Reduce gdrom dma buffer size to 16 sectors from 32. Limit serialization of dma and pio buffers to actual content. reios: schedule transfer of dma data in chunks. Fixes Soul Calibuf audio drop outs at boot with HLE BIOS (RA). Issue #1755 --- core/hw/gdrom/gdromv3.cpp | 224 ++++++++++++++++++++--------------- core/hw/gdrom/gdromv3.h | 89 ++++++++++++-- core/reios/gdrom_hle.cpp | 165 +++++++++++++++++++++----- core/reios/gdrom_hle.h | 63 +--------- core/reios/reios.cpp | 13 +- core/reios/reios.h | 5 +- core/serialize.cpp | 6 +- core/serialize.h | 3 +- tests/src/serialize_test.cpp | 5 +- 9 files changed, 352 insertions(+), 221 deletions(-) diff --git a/core/hw/gdrom/gdromv3.cpp b/core/hw/gdrom/gdromv3.cpp index e61096c9f..5d89595e2 100644 --- a/core/hw/gdrom/gdromv3.cpp +++ b/core/hw/gdrom/gdromv3.cpp @@ -25,9 +25,9 @@ int sns_key; static u32 set_mode_offset; static read_params_t read_params; static packet_cmd_t packet_cmd; -static read_buff_t read_buff; -static pio_buff_t pio_buff; -static ata_cmd_t ata_cmd; +static DmaBuffer dma_buff; +static PioBuffer pio_buff; +static u8 ata_command; cdda_t cdda; static gd_states gd_state; @@ -35,7 +35,7 @@ static DiscType gd_disk_type; /* GD rom reset -> GDS_WAITCMD - GDS_WAITCMD -> ATA/SPI command [Command code is on ata_cmd] + GDS_WAITCMD -> ATA/SPI command [Command code is on ata_command] SPI Command -> GDS_WAITPACKET -> GDS_SPI_* , depending on input GDS_SPI_READSECTOR -> Depending on features , it can do quite a few things @@ -98,21 +98,69 @@ static void gd_spi_pio_end(const u8* buffer, u32 len, gd_states next_state = gds static void gd_process_spi_cmd(); static void gd_process_ata_cmd(); -static void FillReadBuffer() +void DmaBuffer::fill(read_params_t& params) { - read_buff.cache_index=0; - u32 count = read_params.remaining_sectors; + if (!isEmpty()) + return; + index = 0; + verify(params.remaining_sectors > 0); + u32 count = std::min(params.remaining_sectors, NSECT); + size = count * params.sector_type; + + libGDR_ReadSector(cache, params.start_sector, count, params.sector_type); + params.start_sector += count; + params.remaining_sectors -= count; +} - if (count > 32) - count = 32; +const u8 *DmaBuffer::read(u32 len) +{ + verify(len <= size); + const u8 *p = &cache[index]; + index += len; + size -= len; + return p; +} - read_buff.cache_size=count*read_params.sector_type; +void DmaBuffer::serialize(Serializer& ser) const +{ + ser << size; + ser.serialize(&cache[index], size); +} + +void DmaBuffer::deserialize(Deserializer& deser) +{ + if (deser.version() < Deserializer::V54) + { + deser >> index; + deser >> size; + deser >> cache; + } + else + { + index = 0; + deser >> size; + deser.deserialize(&cache[0], size); + } +} - libGDR_ReadSector(read_buff.cache,read_params.start_sector,count,read_params.sector_type); - read_params.start_sector+=count; - read_params.remaining_sectors-=count; +void PioBuffer::serialize(Serializer& ser) const +{ + ser << next_state; + ser << index; + ser << size; + ser.serialize(&_data[0], size); } +void PioBuffer::deserialize(Deserializer& deser) +{ + deser >> next_state; + deser >> index; + deser >> size; + if (deser.version() < Deserializer::V54) + deser >> _data; + else + deser.deserialize(&_data[0], size); +} static void gd_set_state(gd_states state) { @@ -163,7 +211,7 @@ static void gd_set_state(gd_states state) case gds_pio_send_data: // When preparations are complete, the following steps are carried out at the device. //(1) Number of bytes to be read is set in "Byte Count" register. - ByteCount.full =(u16)(pio_buff.size<<1); + ByteCount.full = (u16)pio_buff.getSize(); //(2) IO bit is set and CoD bit is cleared. IntReason.IO=1; IntReason.CoD=0; @@ -189,22 +237,24 @@ static void gd_set_state(gd_states state) u32 sector_count = read_params.remaining_sectors; gd_states next_state=gds_pio_end; - if (sector_count > 27) - { - sector_count = 27; + const u32 maxSectors = (PioBuffer::Capacity - 1) / read_params.sector_type; + if (sector_count > maxSectors) { + sector_count = maxSectors; next_state = gds_readsector_pio; } - libGDR_ReadSector((u8*)&pio_buff.data[0],read_params.start_sector,sector_count, read_params.sector_type); - read_params.start_sector+=sector_count; - read_params.remaining_sectors-=sector_count; + u16 *buffer = pio_buff.fill(sector_count * read_params.sector_type); + libGDR_ReadSector((u8*)buffer, read_params.start_sector, sector_count, read_params.sector_type); + read_params.start_sector += sector_count; + read_params.remaining_sectors -= sector_count; - gd_spi_pio_end(0,sector_count*read_params.sector_type,next_state); + gd_spi_pio_end(nullptr, 0, next_state); } break; case gds_readsector_dma: - FillReadBuffer(); + dma_buff.clear(); + dma_buff.fill(read_params); break; case gds_pio_end: @@ -236,7 +286,7 @@ static void gd_set_state(gd_states state) break; case gds_process_set_mode: - memcpy((u8 *)&GD_HardwareInfo + set_mode_offset, pio_buff.data, pio_buff.size << 1); + memcpy((u8 *)&GD_HardwareInfo + set_mode_offset, pio_buff.data(), pio_buff.getSize()); //end pio transfer ;) gd_set_state(gds_pio_end); break; @@ -304,36 +354,32 @@ static void gd_disc_change() read_params = { 0 }; set_mode_offset = 0; packet_cmd = { 0 }; - memset(&read_buff, 0, sizeof(read_buff)); - pio_buff = { gds_waitcmd, 0 }; - ata_cmd = { 0 }; + dma_buff.clear(); + pio_buff.clear(); + ata_command = 0; cdda = { cdda_t::NoInfo, 0 }; } //This handles the work of setting up the pio regs/state :) static void gd_spi_pio_end(const u8* buffer, u32 len, gd_states next_state) { - verify(len<0xFFFF); - pio_buff.index=0; - pio_buff.size=len>>1; - pio_buff.next_state=next_state; - - if (buffer!=0) - memcpy(pio_buff.data,buffer,len); - - if (len==0) + if (buffer != nullptr) { + verify(len < 0xFFFF); // TODO shouldn't this be <= 0xFFFF ? + memcpy(pio_buff.fill(len), buffer, len); + } + pio_buff.next_state = next_state; + if (pio_buff.isEmpty()) gd_set_state(next_state); else gd_set_state(gds_pio_send_data); } static void gd_spi_pio_read_end(u32 len, gd_states next_state) { - verify(len<0xFFFF); - pio_buff.index=0; - pio_buff.size=len>>1; - pio_buff.next_state=next_state; + verify(len < 0xFFFF); // TODO see above + pio_buff.resetSize(len); + pio_buff.next_state = next_state; - if (len==0) + if (len == 0) gd_set_state(next_state); else gd_set_state(gds_pio_get_data); @@ -350,7 +396,7 @@ static void gd_process_ata_cmd() else GDStatus.CHECK=1; - switch(ata_cmd.command) + switch (ata_command) { case ATA_NOP: printf_ata("ATA_NOP"); @@ -447,7 +493,7 @@ static void gd_process_ata_cmd() break; default: - WARN_LOG(GDROM, "Unknown ATA command %x", ata_cmd.command); + WARN_LOG(GDROM, "Unknown ATA command %x", ata_command); Error.ABRT = 1; Error.Sense = 5; // illegal request GDStatus.BSY = 0; @@ -706,12 +752,12 @@ static void gd_process_spi_cmd() read_params.sector_type = sector_type;//yeah i know , not really many types supported... printf_spicmd("SPI_CD_READ - Sector=%d Size=%d/%d DMA=%d",read_params.start_sector,read_params.remaining_sectors,read_params.sector_type,Features.CDRead.DMA); - if (Features.CDRead.DMA == 1) - { + if (Features.CDRead.DMA == 1) { + pio_buff.clear(); gd_set_state(gds_readsector_dma); } - else - { + else { + dma_buff.clear(); gd_set_state(gds_readsector_pio); } } @@ -1008,16 +1054,15 @@ u32 ReadMem_gdrom(u32 Addr, u32 sz) //if (gd_state == gds_pio_send_data) //{ - if (pio_buff.index == pio_buff.size) + if (pio_buff.atEnd()) { INFO_LOG(GDROM, "GDROM: Illegal Read From DATA (underflow)"); } else { - u32 rv= pio_buff.data[pio_buff.index]; - pio_buff.index+=1; - ByteCount.full-=2; - if (pio_buff.index==pio_buff.size) + u32 rv = pio_buff.read(); + ByteCount.full -= sizeof(u16); + if (pio_buff.atEnd()) { verify(pio_buff.next_state != gds_pio_send_data); //end of pio transfer ! @@ -1085,11 +1130,10 @@ void WriteMem_gdrom(u32 Addr, u32 data, u32 sz) } else if (gd_state == gds_pio_get_data) { - pio_buff.data[pio_buff.index]=(u16)data; - pio_buff.index+=1; - if (pio_buff.size==pio_buff.index) + pio_buff.write((u16)data); + if (pio_buff.atEnd()) { - verify(pio_buff.next_state!=gds_pio_get_data); + verify(pio_buff.next_state != gds_pio_get_data); gd_set_state(pio_buff.next_state); } } @@ -1136,7 +1180,7 @@ void WriteMem_gdrom(u32 Addr, u32 data, u32 sz) { if (data != ATA_NOP && data != ATA_SOFT_RESET) verify(gd_state == gds_waitcmd); - ata_cmd.command = (u8)data; + ata_command = (u8)data; gd_set_state(gds_procata); } else @@ -1178,7 +1222,7 @@ static int GDRomschd(int tag, int cycles, int jitter, void *arg) SecNumber.Status = GD_STANDBY; GDStatus.DSC = 1; } - if(!(SB_GDST&1) || !(SB_GDEN &1) || (read_buff.cache_size==0 && read_params.remaining_sectors==0)) + if (!(SB_GDST & 1) || !(SB_GDEN & 1) || (dma_buff.isEmpty() && read_params.remaining_sectors == 0)) return 0; u32 src = SB_GDSTARD; @@ -1193,7 +1237,7 @@ static int GDRomschd(int tag, int cycles, int jitter, void *arg) //if we don't have any more sectors to read if (read_params.remaining_sectors == 0) //make sure we don't underrun the cache :) - len = std::min(len, read_buff.cache_size); + len = std::min(len, dma_buff.getSize()); len = std::min(len, (u32)10240); // do we need to do this for GDROM DMA? @@ -1211,25 +1255,15 @@ static int GDRomschd(int tag, int cycles, int jitter, void *arg) u32 len_backup = len; if(1 == SB_GDDIR) { - while(len) + while (len) { - u32 buff_size =read_buff.cache_size; - if (buff_size==0) - { - verify(read_params.remaining_sectors>0); - //buffer is empty , fill it :) - FillReadBuffer(); - continue; - } + dma_buff.fill(read_params); + // transfer up to len bytes + const u32 buff_size = std::min(dma_buff.getSize(), len); - //transfer up to len bytes - if (buff_size>len) - buff_size=len; - WriteMemBlock_nommu_ptr(src,(u32*)&read_buff.cache[read_buff.cache_index], buff_size); - read_buff.cache_index+=buff_size; - read_buff.cache_size-=buff_size; - src+=buff_size; - len-=buff_size; + WriteMemBlock_nommu_ptr(src, (const u32 *)dma_buff.read(buff_size), buff_size); + src += buff_size; + len -= buff_size; } } else @@ -1245,16 +1279,10 @@ static int GDRomschd(int tag, int cycles, int jitter, void *arg) SB_GDST = 0; asic_RaiseInterrupt(holly_GDROM_DMA); } - //Read ALL sectors - if (read_params.remaining_sectors==0) - { - //And all buffer :p - if (read_buff.cache_size==0) - { - //verify(!SB_GDST&1) -> dc can do multi read dma - gd_set_state(gds_procpacketdone); - } - } + // Read ALL sectors and all buffer + if (read_params.remaining_sectors == 0 && dma_buff.isEmpty()) + //verify(!SB_GDST&1) -> dc can do multi read dma + gd_set_state(gds_procpacketdone); return getGDROMTicks(); } @@ -1338,9 +1366,9 @@ void gdrom_reg_Reset(bool hard) set_mode_offset = 0; read_params = {}; packet_cmd = {}; - read_buff = {}; - pio_buff = {}; - ata_cmd = {}; + dma_buff.clear(); + pio_buff.clear(); + ata_command = 0; cdda = {}; gd_disk_type = NoDisk; @@ -1371,9 +1399,9 @@ void serialize(Serializer& ser) ser << packet_cmd; ser << set_mode_offset; ser << read_params; - ser << read_buff; - ser << pio_buff; - ser << ata_cmd; + dma_buff.serialize(ser); + pio_buff.serialize(ser); + ser << ata_command; ser << cdda; ser << gd_state; ser << gd_disk_type; @@ -1400,16 +1428,16 @@ void deserialize(Deserializer& deser) deser >> packet_cmd; deser >> set_mode_offset; deser >> read_params; - if (deser.version() >= Deserializer::V17) - deser >> read_buff; - else - { + if (deser.version() >= Deserializer::V17) { + dma_buff.deserialize(deser); + } + else { deser >> packet_cmd; - read_buff.cache_size = 0; + dma_buff.clear(); } - deser >> pio_buff; + pio_buff.deserialize(deser); deser.skip(Deserializer::V44); // set_mode_offset (repeat) - deser >> ata_cmd; + deser >> ata_command; deser >> cdda; deser >> gd_state; deser >> gd_disk_type; diff --git a/core/hw/gdrom/gdromv3.h b/core/hw/gdrom/gdromv3.h index e462a9df9..164c3c9e8 100644 --- a/core/hw/gdrom/gdromv3.h +++ b/core/hw/gdrom/gdromv3.h @@ -164,26 +164,89 @@ struct packet_cmd_t }; }; -//Buffer for sector reads [dma] -struct read_buff_t +class DmaBuffer { - u32 cache_index; - u32 cache_size; - u8 cache[2352 * 32]; +public: + DmaBuffer() { + clear(); + } + bool isEmpty() const { + return size == 0; + } + u32 getSize() const { + return size; + } + void clear() { + size = 0; + index = 0; + } + + // Fill the cache with up to NSECT sectors if empty, using the passed read parameters + void fill(read_params_t& params); + // Return a pointer to the cache and advance the index by len bytes. len *must* be <= getSize() + const u8 *read(u32 len); + void serialize(Serializer& ser) const; + void deserialize(Deserializer& deser); + +private: + static constexpr u32 NSECT = 16; + u32 index; + u32 size; + u8 cache[2352 * NSECT] {}; }; -//pio buffer -struct pio_buff_t +class PioBuffer { +public: + PioBuffer() { + clear(); + } + bool isEmpty() const { + return size == 0; + } + // Returns true if the buffer has reached its capacity and can't be read from or written to + bool atEnd() const { + return index == size; + } + // in bytes + u32 getSize() const { + return size * sizeof(u16); + } + void clear() + { + next_state = gds_waitcmd; + size = 0; + index = 0; + } + u16 read() { + return _data[index++]; + } + const u16 *data() const { + return _data; + } + void write(u16 v) { + _data[index++] = v; + } + // Returns a pointer to fill the buffer and sets its size. Index is reset. + u16 *fill(u32 size) { + resetSize(size); + return &_data[0]; + } + // Sets the buffer capacity and resets the index. + void resetSize(u32 size) { + this->index = 0; + this->size = size / sizeof(u16); + } + void serialize(Serializer& ser) const; + void deserialize(Deserializer& deser); + gd_states next_state; + static constexpr u32 Capacity = 64_KB; + +private: u32 index; u32 size; - u16 data[0x10000>>1]; //64 kb -}; - -struct ata_cmd_t -{ - u8 command; + u16 _data[Capacity / 2] {}; }; struct cdda_t diff --git a/core/reios/gdrom_hle.cpp b/core/reios/gdrom_hle.cpp index 1b16c3630..17e2f5fc5 100644 --- a/core/reios/gdrom_hle.cpp +++ b/core/reios/gdrom_hle.cpp @@ -22,8 +22,126 @@ #define SWAP32(a) ((((a) & 0xff) << 24) | (((a) & 0xff00) << 8) | (((a) >> 8) & 0xff00) | (((a) >> 24) & 0xff)) #define debugf(...) DEBUG_LOG(REIOS, __VA_ARGS__) +static void readSectors(u32 addr, u32 sector, u32 count, bool virtualAddr); -gdrom_hle_state_t gd_hle_state; +struct gdrom_hle_state_t +{ + gdrom_hle_state_t() : params{}, result{} {} + + u32 last_request_id = 0xFFFFFFFF; + u32 next_request_id = 2; + gd_return_value status = GDC_OK; + gd_command command = GDCC_NONE; + u32 params[4]; + u32 result[4]; + u32 cur_sector = 0; + u32 multi_read_sector = 0; + u32 multi_read_offset = 0; + u32 multi_read_count = 0; + u32 multi_read_total = 0; + u32 multi_callback = 0; + u32 multi_callback_arg = 0; + bool dma_trans_ended = false; + u64 xfer_end_time = 0; + + void Serialize(Serializer& ser) + { + ser << last_request_id; + ser << next_request_id; + ser << status; + ser << command; + ser << params; + ser << result; + ser << cur_sector; + ser << multi_read_sector; + ser << multi_read_offset; + ser << multi_read_count; + ser << multi_read_total; + ser << multi_callback; + ser << multi_callback_arg; + ser << dma_trans_ended; + ser << xfer_end_time; + + } + void Deserialize(Deserializer& deser) + { + deser >> last_request_id; + deser >> next_request_id; + deser >> status; + deser >> command; + deser >> params; + deser >> result; + deser >> cur_sector; + deser >> multi_read_sector; + deser >> multi_read_offset; + deser >> multi_read_count; + deser >> multi_read_total; + deser >> multi_callback; + deser >> multi_callback_arg; + deser >> dma_trans_ended; + deser >> xfer_end_time; + } +}; +static gdrom_hle_state_t gd_hle_state; + +static int schedId = -1; + +static int getGdromTicks() +{ + u32 len = gd_hle_state.multi_read_count * 2048; + if (len > 10240) + return 1000000; // Large transfers: GD-ROM transfer rate 1.8 MB/s + else + return len * 2; // Small transfers: Max G1 bus rate: 50 MHz x 16 bits +} + +static int schedCallback(int tag, int cycles, int jitter, void *arg) +{ + const u32 sect = std::min(gd_hle_state.multi_read_count, 5u); + readSectors(gd_hle_state.multi_read_offset, gd_hle_state.multi_read_sector, sect, false); + gd_hle_state.multi_read_count -= sect; + gd_hle_state.multi_read_sector += sect; + gd_hle_state.cur_sector = gd_hle_state.multi_read_sector; + gd_hle_state.multi_read_offset += sect * 2048; + + if (gd_hle_state.multi_read_count == 0) + { + gd_hle_state.result[3] = GDC_WAIT_INTERNAL; + SecNumber.Status = GD_STANDBY; + gd_hle_state.status = GDC_COMPLETE; + } + gd_hle_state.result[2] = (gd_hle_state.multi_read_total - gd_hle_state.multi_read_count) * 2048; + + return getGdromTicks(); +} + +void reios_init() { + if (schedId == -1) + schedId = sh4_sched_register(0, schedCallback); +} + +void reios_serialize(Serializer& ser) { + gd_hle_state.Serialize(ser); + sh4_sched_serialize(ser, schedId); +} + +void reios_deserialize(Deserializer& deser) +{ + gd_hle_state.Deserialize(deser); + if (deser.version() >= Deserializer::V54) + sh4_sched_deserialize(deser, schedId); +} + +void gdrom_hle_reset() { + gd_hle_state = {}; +} + +void reios_term() +{ + if (schedId != -1) + sh4_sched_unregister(schedId); + schedId = -1; +} static void GDROM_HLE_ReadSES() { @@ -64,19 +182,10 @@ static void GDROM_HLE_ReadTOC() WriteMem32(dest, toc[i]); } -template -static void read_sectors_to(u32 addr, u32 sector, u32 count) +static void readSectors(u32 addr, u32 sector, u32 count, bool virtualAddr) { gd_hle_state.cur_sector = sector + count - 1; - if (virtual_addr) - gd_hle_state.xfer_end_time = 0; - else if (count > 5 && !config::FastGDRomLoad) - // Large Transfers: GD-ROM rate (approx. 1.8 MB/s) - gd_hle_state.xfer_end_time = sh4_sched_now64() + (u64)count * 2048 * 1000000L / 10240; - else - // Small transfers: Max G1 bus rate: 50 MHz x 16 bits - gd_hle_state.xfer_end_time = sh4_sched_now64() + 5 * 2048 * 2; - if (!virtual_addr || !mmu_enabled()) + if (!virtualAddr || !mmu_enabled()) { u8 * pDst = GetMemPtr(addr, 0); @@ -94,7 +203,7 @@ static void read_sectors_to(u32 addr, u32 sector, u32 count) for (std::size_t i = 0; i < std::size(temp); i++) { - if (virtual_addr) + if (virtualAddr) WriteMem32(addr, temp[i]); else WriteMem32_nommu(addr, temp[i]); @@ -106,6 +215,12 @@ static void read_sectors_to(u32 addr, u32 sector, u32 count) } } +static void read_pio_sectors(u32 addr, u32 sector, u32 count) +{ + gd_hle_state.xfer_end_time = 0; + readSectors(addr, sector, count, true); +} + static void GDROM_HLE_ReadDMA() { u32 fad = gd_hle_state.params[0] & 0xffffff; @@ -115,7 +230,13 @@ static void GDROM_HLE_ReadDMA() debugf("GDROM: DMA READ Sector=%d, Num=%d, Buffer=%08x, zero=%x", fad, nsect, buffer, gd_hle_state.params[3]); - read_sectors_to(buffer, fad, nsect); + gd_hle_state.cur_sector = fad; + gd_hle_state.multi_read_sector = fad; + gd_hle_state.multi_read_offset = buffer; + gd_hle_state.multi_read_count = nsect; + gd_hle_state.multi_read_total = nsect; + sh4_sched_request(schedId, getGdromTicks()); + gd_hle_state.result[2] = 0; gd_hle_state.result[3] = 0; } @@ -129,7 +250,7 @@ static void GDROM_HLE_ReadPIO() debugf("GDROM: PIO READ Sector=%d, Num=%d, Buffer=%08x, SeekAhead=%x", fad, nsect, buffer, gd_hle_state.params[3]); - read_sectors_to(buffer, fad, nsect); + read_pio_sectors(buffer, fad, nsect); gd_hle_state.result[2] = nsect * 2048; gd_hle_state.result[3] = 0; } @@ -292,19 +413,9 @@ static void GD_HLE_Command(gd_command cc) case GDCC_DMAREAD: cdda.status = cdda_t::NoInfo; - if (gd_hle_state.xfer_end_time == 0) + if (gd_hle_state.multi_read_count == 0) GDROM_HLE_ReadDMA(); - if (gd_hle_state.xfer_end_time > 0) - { - if (gd_hle_state.xfer_end_time > sh4_sched_now64()) - return; - gd_hle_state.xfer_end_time = 0; - } - gd_hle_state.result[2] = gd_hle_state.params[1] * 2048; - gd_hle_state.result[3] = 0; - SecNumber.Status = GD_PAUSE; - break; - + return; case GDCC_PLAY2: { diff --git a/core/reios/gdrom_hle.h b/core/reios/gdrom_hle.h index c2e63d5a6..acc9e22ca 100644 --- a/core/reios/gdrom_hle.h +++ b/core/reios/gdrom_hle.h @@ -70,9 +70,8 @@ enum misc_command { MISC_SETVECTOR }; -void gdrom_hle_init(); -void gdrom_hle_term(); void gdrom_hle_op(); +void gdrom_hle_reset(); typedef enum : int32_t { GDC_ERR = -1, @@ -85,66 +84,6 @@ typedef enum : int32_t { GDC_RET2 } gd_return_value; -struct gdrom_hle_state_t -{ - gdrom_hle_state_t() : params{}, result{} {} - - u32 last_request_id = 0xFFFFFFFF; - u32 next_request_id = 2; - gd_return_value status = GDC_OK; - gd_command command = GDCC_NONE; - u32 params[4]; - u32 result[4]; - u32 cur_sector = 0; - u32 multi_read_sector = 0; - u32 multi_read_offset = 0; - u32 multi_read_count = 0; - u32 multi_read_total = 0; - u32 multi_callback = 0; - u32 multi_callback_arg = 0; - bool dma_trans_ended = false; - u64 xfer_end_time = 0; - - void Serialize(Serializer& ser) - { - ser << last_request_id; - ser << next_request_id; - ser << status; - ser << command; - ser << params; - ser << result; - ser << cur_sector; - ser << multi_read_sector; - ser << multi_read_offset; - ser << multi_read_count; - ser << multi_read_total; - ser << multi_callback; - ser << multi_callback_arg; - ser << dma_trans_ended; - ser << xfer_end_time; - - } - void Deserialize(Deserializer& deser) - { - deser >> last_request_id; - deser >> next_request_id; - deser >> status; - deser >> command; - deser >> params; - deser >> result; - deser >> cur_sector; - deser >> multi_read_sector; - deser >> multi_read_offset; - deser >> multi_read_count; - deser >> multi_read_total; - deser >> multi_callback; - deser >> multi_callback_arg; - deser >> dma_trans_ended; - deser >> xfer_end_time; - } -}; -extern gdrom_hle_state_t gd_hle_state; - // status for GDROM_GET_DRV_STAT enum gd_drv_stat { GD_STAT_BUSY, diff --git a/core/reios/reios.cpp b/core/reios/reios.cpp index 4f00676b4..49f501f4c 100644 --- a/core/reios/reios.cpp +++ b/core/reios/reios.cpp @@ -727,13 +727,7 @@ void DYNACALL reios_trap(Sh4Context *ctx, u32 op) ctx->pc = ctx->pr; } -bool reios_init() -{ - return true; -} - -void reios_set_flash(MemChip* flash) -{ +void reios_set_flash(MemChip* flash) { flashrom = flash; } @@ -763,8 +757,5 @@ void reios_reset(u8* rom) std::unique_ptr fontData = resource::load("fonts/biosfont.bin", size); memcpy(pFont, fontData.get(), size); - gd_hle_state = {}; -} - -void reios_term() { + gdrom_hle_reset(); } diff --git a/core/reios/reios.h b/core/reios/reios.h index 23ce4a201..c9bb54d18 100644 --- a/core/reios/reios.h +++ b/core/reios/reios.h @@ -3,10 +3,11 @@ #include "types.h" #include "hw/flashrom/flashrom.h" -bool reios_init(); +void reios_init(); void reios_set_flash(MemChip* flash); void reios_reset(u8* rom); - +void reios_serialize(Serializer& ser); +void reios_deserialize(Deserializer& deser); void reios_term(); struct Sh4Context; diff --git a/core/serialize.cpp b/core/serialize.cpp index 029d5cdf9..74912657c 100644 --- a/core/serialize.cpp +++ b/core/serialize.cpp @@ -10,7 +10,7 @@ #include "hw/pvr/pvr.h" #include "hw/sh4/sh4_sched.h" #include "hw/sh4/sh4_mmr.h" -#include "reios/gdrom_hle.h" +#include "reios/reios.h" #include "hw/naomi/naomi.h" #include "hw/naomi/naomi_cart.h" #include "hw/bba/bba.h" @@ -50,7 +50,7 @@ void dc_serialize(Serializer& ser) ser << config::Region.get(); naomi_cart_serialize(ser); - gd_hle_state.Serialize(ser); + reios_serialize(ser); achievements::serialize(ser); DEBUG_LOG(SAVESTATE, "Saved %d bytes", (u32)ser.size()); @@ -93,7 +93,7 @@ void dc_deserialize(Deserializer& deser) verify(config::Region >= 0 && config::Region <= 3); naomi_cart_deserialize(deser); - gd_hle_state.Deserialize(deser); + reios_deserialize(deser); achievements::deserialize(deser); sh4_sched_ffts(); diff --git a/core/serialize.h b/core/serialize.h index 434b7a9c1..7a6cb2895 100644 --- a/core/serialize.h +++ b/core/serialize.h @@ -64,7 +64,8 @@ class SerializeBase V51, V52, V53, - Current = V53, + V54, + Current = V54, Next = Current + 1, }; diff --git a/tests/src/serialize_test.cpp b/tests/src/serialize_test.cpp index b02151cdb..24f168fc2 100644 --- a/tests/src/serialize_test.cpp +++ b/tests/src/serialize_test.cpp @@ -32,8 +32,5 @@ TEST_F(SerializeTest, SizeTest) std::vector data(30000000); Serializer ser(data.data(), data.size()); dc_serialize(ser); - ASSERT_EQ(28191434u, ser.size()); + ASSERT_EQ(28050642u, ser.size()); } - - - From a40c51e682a2ef6d4bee6ddd423565300c015236 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 3 Dec 2024 18:33:58 +0100 Subject: [PATCH 38/81] gdrom: fix deserialization crash regression due to c860807fefd958cc49edeff3e8e9270cda40abcb Issue #1755 --- core/hw/gdrom/gdromv3.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/hw/gdrom/gdromv3.cpp b/core/hw/gdrom/gdromv3.cpp index 5d89595e2..d84eff64b 100644 --- a/core/hw/gdrom/gdromv3.cpp +++ b/core/hw/gdrom/gdromv3.cpp @@ -134,6 +134,7 @@ void DmaBuffer::deserialize(Deserializer& deser) deser >> index; deser >> size; deser >> cache; + deser.skip(2352 * 16); } else { From de1584d97c1847462c6c1911a93a76b4bec67a3b Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 3 Dec 2024 20:16:56 +0100 Subject: [PATCH 39/81] systemsp: input remapping. Input desc for mushiking, dinoking, lovebery Allow input remapping for SystemSP. Add input descriptors for barcode games: mushiking, dinoking and love&berry --- core/hw/naomi/naomi_roms.cpp | 100 ++++++++++++++++++++----------- core/hw/naomi/naomi_roms_input.h | 30 ++++++++++ core/hw/naomi/systemsp.cpp | 31 +++++++++- 3 files changed, 125 insertions(+), 36 deletions(-) diff --git a/core/hw/naomi/naomi_roms.cpp b/core/hw/naomi/naomi_roms.cpp index e1ce88de2..697d6d357 100644 --- a/core/hw/naomi/naomi_roms.cpp +++ b/core/hw/naomi/naomi_roms.cpp @@ -361,11 +361,11 @@ const Game Games[] = // Mushiking The King Of Beetle (2K3 2ND, World) { "mushike", - NULL, + nullptr, "Mushiking The King Of Beetle (2K3 2ND Ver. 1.003-, World)", 0x4000000, 0x3892fb3a, - NULL, + "naomi", M1, ROT0, { @@ -388,9 +388,9 @@ const Game Games[] = // note: this dump from "empty/dead" Management Chip with no game run count left //ROM_REGION( 0x80, "rf_tag", 0 ) //{ "mushi_type1.bin", 0, 0x80, CRC(8f36572b) SHA1(87e00e56d07a961e9180c7da02e35f7fd216dbae) ) - - { NULL, 0, 0 }, - } + }, + nullptr, + &mushik_inputs, }, // Quiz Ah Megamisama (JPN, USA, EXP, KOR, AUS) { @@ -649,7 +649,7 @@ const Game Games[] = "Mushiking The King Of Beetle (2K3 2ND Ver. 1.002-, World)", 0x04000000, 0x3892fb3a, - NULL, + "naomi", M1, ROT0, { @@ -672,9 +672,9 @@ const Game Games[] = // note: this dump from "empty/dead" Management Chip with no game run count left //ROM_REGION( 0x80, "rf_tag", 0 ) //{ "mushi_type1.bin", 0, 0x80, CRC(8f36572b) SHA1(87e00e56d07a961e9180c7da02e35f7fd216dbae) ) - - { NULL, 0, 0 }, - } + }, + nullptr, + &mushik_inputs, }, // Mushiking The King Of Beetle (2K3 2ND Ver. 1.000-, Korea) { @@ -683,7 +683,7 @@ const Game Games[] = "Mushiking The King Of Beetle (2K3 2ND Ver. 1.000-, Korea)", 0x04000000, 0x3892fb3a, - nullptr, + "naomi", M1, ROT0, { @@ -695,7 +695,9 @@ const Game Games[] = { "opr-24270.ic21s", 0x3000000, 0x0800000, 0x02a513ad, InterleavedWord }, { "opr-24271.ic22", 0x3000002, 0x0800000, 0x7e5c745c, InterleavedWord }, { "copy", 0x0400000, 0x0c00000, 0, Copy, 0x1000000 }, - } + }, + nullptr, + &mushik_inputs, }, // Mushiking The King Of Beetle (2K3 2ND Ver. 1.000-, China) { @@ -704,13 +706,15 @@ const Game Games[] = "Mushiking The King Of Beetle (2K3 2ND Ver. 1.000-, China)", 0x04000000, 0x5501, - nullptr, + "naomi", M4, ROT0, { { "ic8.bin", 0x00000000, 0x04000000, 0x5edc61fb }, { "317-0437-com.ic3", 0, 0x800, 0x3b6fcee8, Key }, - } + }, + nullptr, + &mushik_inputs, }, // Mushiking The King Of Beetles - Mushiking II / III / III+ (Ver. 1.000-) (Korea) // require rev.H Korea BIOS, not dumped @@ -720,14 +724,16 @@ const Game Games[] = "Mushiking The King Of Beetles - Mushiking II / III / III+ (Ver. 1.000-) (Korea)", 0x08000000, 0x5502, - nullptr, + "naomi", M4, ROT0, { { "fpr-24355.ic8", 0x00000000, 0x04000000, 0x8bd89229 }, { "fpr-24356.ic9", 0x04000000, 0x04000000, 0xed649c81 }, { "317-0437-com.ic3", 0, 0x800, 0x3b6fcee8, Key }, - } + }, + nullptr, + &mushik_inputs, }, // Naomi M2/M3 Roms // 18 Wheeler (deluxe) (Rev A) @@ -2531,11 +2537,11 @@ const Game Games[] = // Mushiking The King Of Beetles 2004 Second (Japan) { "mushi2k4", - NULL, + nullptr, "Mushiking The King Of Beetles 2004 Second (Japan)", 0x5800000, 0xffffffff, // not populated - NULL, + "naomi", M2, ROT0, { @@ -2545,17 +2551,18 @@ const Game Games[] = { "mpr-24244.ic3", 0x02800000, 0x01000000 }, { "mpr-24245.ic4", 0x03800000, 0x01000000 }, { "mpr-24246.ic5", 0x04800000, 0x01000000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &mushik_inputs, }, // Mushiking The King Of Beetles 2005 First (Japan) { "mushi2k5", - NULL, + nullptr, "Mushiking The King Of Beetles 2005 First (Japan)", 0x7800000, 0xffffffff, // not populated - NULL, + "naomi", M2, ROT0, { @@ -2567,8 +2574,9 @@ const Game Games[] = { "mpr-24280.ic5", 0x4800000, 0x1000000, 0x00000000 }, { "mpr-24281.ic6", 0x5800000, 0x1000000, 0x00000000 }, { "mpr-24282.ic7", 0x6800000, 0x1000000, 0x00000000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &mushik_inputs, }, // Mushiking The King Of Beetle (MUSHIUSA '04 1ST, Prototype) { @@ -2577,7 +2585,7 @@ const Game Games[] = "Mushiking The King Of Beetle (MUSHIUSA '04 1ST, Prototype)", 0x07800000, 0xffffffff, // not populated - NULL, + "naomi", M2, ROT0, { @@ -2588,8 +2596,9 @@ const Game Games[] = { "rom5.ic5s", 0x2800000, 0x800000, 0x7076a50e }, { "rom6.ic6s", 0x3000000, 0x800000, 0xd7143066 }, { "rom7.ic7s", 0x3800000, 0x800000, 0x98839bab }, - { NULL, 0, 0 }, - } + }, + nullptr, + &mushik_inputs, }, // Marvel vs. Capcom 2 New Age of Heroes (USA, Rev A) { @@ -4758,13 +4767,14 @@ const Game Games[] = { "fpr-24333.ic8", 0x0000000, 0x4000000 }, { "fpr-24334.ic9", 0x4000000, 0x4000000 }, { "317-0437-com.ic3", 0, 0x800, 0x0000000, Key }, - { NULL, 0, 0 }, - } + }, + nullptr, + &mushik_inputs, }, // Mushiking The King Of Beetles - Mushiking II / III / III+ (World, Ver. 2.001) { "mushik2e", - NULL, + nullptr, "Mushiking The King Of Beetles - Mushiking II / III / III+ (World, Ver. 2.001)", 0x8000000, 0x5582, @@ -4779,16 +4789,16 @@ const Game Games[] = //ROM_REGION( 0x800, "pic_readout", 0 ) //ROM_LOAD( "317-0437-com.ic3", 0, 0x800, CRC(3b6fcee8) SHA1(65fbdd3b8c61a4b5ccb6389b25483a7ecdc0794d) ) { "317-0437-com.ic3", 0, 0x800, 0x0000000, Key }, - - { NULL, 0, 0 }, - } + }, + nullptr, + &mushik_inputs, }, // Mushiking The King Of Beetles - Mushiking IV / V / VI (World) // change game version (4/5/6): in BACKUP DATA CLEAR menu hold P1 and P2 buttons 1 for 3 seconds, then change version number in appeared menu and select YES(CLEAR) // ~equivalent of Japanese 2K6 versions { "mushik4e", - NULL, + nullptr, "Mushiking The King Of Beetles - Mushiking IV / V / VI (World)", 0x8000000, 0x5502, @@ -4801,7 +4811,9 @@ const Game Games[] = //ROM_REGION( 0x800, "pic_readout", 0 ) { "317-0437-com.ic3", 0, 0x800, 0x3b6fcee8, Key }, - } + }, + nullptr, + &mushik_inputs, }, // Mushiking The King Of Beetles - Mushiking IV / V / VI (Taiwan) // change game version (4/5/6): in BACKUP DATA CLEAR menu hold P1 and P2 buttons 1 for 3 seconds, then change version number in appeared menu and select YES(CLEAR) @@ -4821,7 +4833,9 @@ const Game Games[] = //ROM_REGION( 0x800, "pic_readout", 0 ) { "317-0437-com.ic3", 0, 0x800, 0x3b6fcee8, Key }, - } + }, + nullptr, + &mushik_inputs, }, { "mushi2k61", @@ -4838,6 +4852,8 @@ const Game Games[] = { "317-0444-jpn.ic3", 0, 0x800, 0x6ded35a2, Key }, }, + nullptr, + &mushik_inputs, }, { "mushi2k62", @@ -4854,6 +4870,8 @@ const Game Games[] = { "317-0444-jpn.ic3", 0, 0x800, 0x6ded35a2, Key }, }, + nullptr, + &mushik_inputs, }, // Pokasuka Ghost *** BAD DUMP *** { @@ -8213,6 +8231,8 @@ const Game Games[] = { "ic69s", 0x07000000, 0x01000000, 0xc78e46c2 }, { "317-0408-com.ic15", 0, 0x800, 0xf77c49dc, Key }, }, + nullptr, + &dinok_inputs, }, { "galilfac", @@ -8282,6 +8302,8 @@ const Game Games[] = { "ic63", 0x04000000, 0x4000000, 0xd3870287 }, { "317-0446-com.ic15", 0, 0x800, 0x9e519dc6, Key }, }, + nullptr, + &lovebery_inputs, }, { "lovebero", @@ -8297,6 +8319,8 @@ const Game Games[] = { "ic63", 0x04000000, 0x4000000, 0xd3870287 }, { "317-0446-com.ic15", 0, 0x800, 0x9e519dc6, Key }, }, + nullptr, + &lovebery_inputs, }, { "magicpop", @@ -8440,6 +8464,7 @@ const Game Games[] = { "317-0408-com.ic15", 0, 0x800, 0xf77c49dc, Key }, }, "mda-c0021", + &dinok_inputs, }, { // Dinosaur King - D-Team VS. the Alpha Fortress (Export, Ver 2.500) @@ -8455,6 +8480,7 @@ const Game Games[] = { "317-0408-com.ic15", 0, 0x800, 0xf77c49dc, Key }, }, "mda-c0047", + &dinok_inputs, }, { // Konglongwang - D-Kids VS Alpha Yaosai (China, Ver 2.501) @@ -8471,6 +8497,7 @@ const Game Games[] = { "317-0408-com.ic15", 0, 0x800, 0xf77c49dc, Key }, }, "mda-c0081", + &dinok_inputs, }, { // Kodai Ouja Kyouryuu King - Mezame yo! Arata-naru Chikara!! (Japan, Ver 4.000) @@ -8487,6 +8514,7 @@ const Game Games[] = { "317-0408-com.ic15", 0, 0x800, 0xf77c49dc, Key }, }, "mda-c0061", + &dinok_inputs, }, { // Heat Up Hockey Image (Ver.1.003R) @@ -8532,6 +8560,7 @@ const Game Games[] = { "317-0446-com.ic15", 0, 0x800, 0x9e519dc6, Key }, }, "mda-c0042", + &lovebery_inputs, }, { // Love And Berry - 3rd-5th Collection (China, Ver 1.001) @@ -8548,6 +8577,7 @@ const Game Games[] = { "317-0446-com.ic15", 0, 0x800, 0x9e519dc6, Key }, }, "mda-c0071", + &lovebery_inputs, }, { "tetgiano", diff --git a/core/hw/naomi/naomi_roms_input.h b/core/hw/naomi/naomi_roms_input.h index 1c07768e2..2b21962fa 100644 --- a/core/hw/naomi/naomi_roms_input.h +++ b/core/hw/naomi/naomi_roms_input.h @@ -496,6 +496,15 @@ static InputDescriptors shootout_inputs = { static InputDescriptors vf4_inputs = INPUT_3_BUTTONS("PUNCH", "KICK", "GUARD"); +static InputDescriptors mushik_inputs = { + { + { NAOMI_BTN0_KEY, "HIT" }, + { NAOMI_BTN1_KEY, "PINCH" }, + { NAOMI_BTN2_KEY, "THROW" }, + NAO_BASE_BTN_DESC + }, +}; + // // AtomisWave games // @@ -763,3 +772,24 @@ static InputDescriptors shaktam_inputs = { { "", Full, 3 }, // unused but P2 starts at axis 4 }, }; + +// +// System SP games +// + +static InputDescriptors dinok_inputs = { + { + { DC_BTN_A, "ROCK" }, + { DC_BTN_B, "SCISSORS" }, + { DC_BTN_C, "PAPER" }, + NAO_BASE_BTN_DESC + }, +}; + +static InputDescriptors lovebery_inputs = { + { + { DC_BTN_A, "P1 BUTTON" }, + { DC_BTN_B, "P2 BUTTON", 0, DC_BTN_A }, + NAO_BASE_BTN_DESC + }, +}; diff --git a/core/hw/naomi/systemsp.cpp b/core/hw/naomi/systemsp.cpp index 02935dd10..7f1fb8e2b 100644 --- a/core/hw/naomi/systemsp.cpp +++ b/core/hw/naomi/systemsp.cpp @@ -988,8 +988,37 @@ class DefaultIOManager : public IOPortManager u64 startTime = 0; }; - void getInputState() { + void getInputState() + { ggpo::getInput(mapleInputState); + if (NaomiGameInputs != nullptr) + { + for (const ButtonDescriptor& bd : NaomiGameInputs->buttons) + { + if (bd.name == nullptr) + break; + if (bd.target != 0) + { + // remap P1 -> P1 and P2 -> P2 + if ((mapleInputState[0].kcode & bd.source) == 0) + mapleInputState[0].kcode &= ~bd.target; + if ((mapleInputState[1].kcode & bd.source) == 0) + mapleInputState[1].kcode &= ~bd.target; + } + else if (bd.p2_target != 0) + { + // remap P1 -> P2 + if ((mapleInputState[0].kcode & bd.source) == 0) + mapleInputState[1].kcode &= ~bd.p2_target; + } + else if (bd.p1_target != 0) + { + // remap P2 -> P1 + if ((mapleInputState[1].kcode & bd.source) == 0) + mapleInputState[0].kcode &= ~bd.p1_target; + } + } + } } MapleInputState mapleInputState[4]; From 78fa3ee09e05ba2c441936a3255c575c3e3516ab Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 3 Dec 2024 20:42:54 +0100 Subject: [PATCH 40/81] vulkan: delay updating imgui texture when driver has just been reset Fixes crash when selecting a custom gamepad layout .png (android) --- core/rend/vulkan/vulkan_driver.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/rend/vulkan/vulkan_driver.h b/core/rend/vulkan/vulkan_driver.h index ea224139e..e48359289 100644 --- a/core/rend/vulkan/vulkan_driver.h +++ b/core/rend/vulkan/vulkan_driver.h @@ -88,6 +88,9 @@ class VulkanDriver final : public ImGuiDriver ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) override { + if (justStarted) + // give it some more time + return {}; VkTexture vkTex(std::make_unique()); vkTex.texture->tex_type = TextureType::_8888; vkTex.texture->SetCommandBuffer(getCommandBuffer()); From 21f9e9fbc20014bf53a32f37d3b7c7c414ac753d Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 3 Dec 2024 21:04:51 +0100 Subject: [PATCH 41/81] android,ios: arcade vgamepad layout. android vgamepad rewrite Refactor common virtual gamepad code. Distinct layouts for console and arcade. Add arcade buttons 5 and 6, insert card button, service mode toggle. System SP support --- CMakeLists.txt | 7 +- core/input/gamepad.h | 1 + core/input/virtual_gamepad.h | 117 + core/ui/gui.cpp | 2 +- core/ui/vgamepad.cpp | 363 ++- core/ui/vgamepad.h | 16 +- resources/picture/buttons-arcade.png | Bin 0 -> 230003 bytes resources/picture/buttons-arcade.svg | 2279 +++++++++++++++++ .../com/flycast/emulator/BaseGLActivity.java | 3 +- .../java/com/flycast/emulator/Emulator.java | 38 +- .../flycast/emulator/NativeGLActivity.java | 31 +- .../emu/EditVirtualJoystickDelegate.java | 111 + .../java/com/flycast/emulator/emu/JNIdc.java | 7 - .../flycast/emulator/emu/NativeGLView.java | 40 +- .../emulator/emu/TouchEventHandler.java | 27 + .../com/flycast/emulator/emu/VGamepad.java | 35 + .../emulator/emu/VirtualJoystickDelegate.java | 544 ++-- .../emulator/periph/InputDeviceManager.java | 18 +- .../com/flycast/emulator/periph/VJoy.java | 219 -- .../flycast/src/main/jni/src/Android.cpp | 176 +- .../src/main/jni/src/android_gamepad.h | 103 +- .../src/main/jni/src/android_input.cpp | 225 ++ .../flycast/src/main/jni/src/jni_util.h | 2 +- .../emulator/PadViewController.mm | 40 +- .../apple/emulator-ios/emulator/ios_gamepad.h | 113 +- 25 files changed, 3399 insertions(+), 1118 deletions(-) create mode 100644 core/input/virtual_gamepad.h create mode 100644 resources/picture/buttons-arcade.png create mode 100644 resources/picture/buttons-arcade.svg create mode 100644 shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java create mode 100644 shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/TouchEventHandler.java create mode 100644 shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VGamepad.java delete mode 100644 shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/VJoy.java create mode 100644 shell/android-studio/flycast/src/main/jni/src/android_input.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 24130ada9..2c76bfb52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1032,7 +1032,8 @@ if(NOT LIBRETRO) if(ANDROID OR IOS) cmrc_add_resources(flycast-resources WHENCE resources - resources/picture/buttons.png) + resources/picture/buttons.png + resources/picture/buttons-arcade.png) endif() endif() @@ -1617,6 +1618,10 @@ if(NOT LIBRETRO) target_sources(${PROJECT_NAME} PRIVATE shell/android-studio/flycast/src/main/jni/src/Android.cpp shell/android-studio/flycast/src/main/jni/src/android_gamepad.h + shell/android-studio/flycast/src/main/jni/src/android_storage.h + shell/android-studio/flycast/src/main/jni/src/http_client.h + shell/android-studio/flycast/src/main/jni/src/jni_util.h + shell/android-studio/flycast/src/main/jni/src/android_input.cpp shell/android-studio/flycast/src/main/jni/src/android_keyboard.h) target_link_libraries(${PROJECT_NAME} PRIVATE android log) diff --git a/core/input/gamepad.h b/core/input/gamepad.h index 88bce6fe7..e4fdc049b 100644 --- a/core/input/gamepad.h +++ b/core/input/gamepad.h @@ -52,6 +52,7 @@ enum DreamcastKey EMU_BTN_SAVESTATE, EMU_BTN_BYPASS_KB, EMU_BTN_SCREENSHOT, + EMU_BTN_SRVMODE, // used internally by virtual gamepad // Real axes DC_AXIS_TRIGGERS = 0x1000000, diff --git a/core/input/virtual_gamepad.h b/core/input/virtual_gamepad.h new file mode 100644 index 000000000..69adc2828 --- /dev/null +++ b/core/input/virtual_gamepad.h @@ -0,0 +1,117 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#pragma once +#include "gamepad_device.h" +#include "ui/vgamepad.h" + +class VirtualGamepad : public GamepadDevice +{ +public: + VirtualGamepad(const char *api_name, int maple_port = 0) + : GamepadDevice(maple_port, api_name, false) + { + _name = "Virtual Gamepad"; + _unique_id = "virtual_gamepad_uid"; + input_mapper = std::make_shared(); + // hasAnalogStick = true; // TODO has an analog stick but input mapping isn't persisted + + leftTrigger = DC_AXIS_LT; + rightTrigger = DC_AXIS_RT; + } + + bool is_virtual_gamepad() override { + return true; + }; + + // normalized coordinates [-1, 1] + void joystickInput(float x, float y) + { + vgamepad::setAnalogStick(x, y); + int joyx = std::round(x * 32767.f); + int joyy = std::round(y * 32767.f); + if (joyx >= 0) + gamepad_axis_input(DC_AXIS_RIGHT, joyx); + else + gamepad_axis_input(DC_AXIS_LEFT, -joyx); + if (joyy >= 0) + gamepad_axis_input(DC_AXIS_DOWN, joyy); + else + gamepad_axis_input(DC_AXIS_UP, -joyy); + } + + void releaseAll() + { + for (int i = 0; i < 32; i++) + if (buttonState & (1 << i)) + gamepad_btn_input(1 << i, false); + buttonState = 0; + joystickInput(0, 0); + gamepad_axis_input(DC_AXIS_LT, 0); + gamepad_axis_input(DC_AXIS_RT, 0); + if (previousFastForward) + gamepad_btn_input(EMU_BTN_FFORWARD, false); + previousFastForward = false; + } + + virtual bool handleButtonInput(u32& state, u32 key, bool pressed) { + // can be overridden in derived classes to handle specific key combos + // (iOS up+down or left+right) + return false; + } + + void buttonInput(vgamepad::ControlId controlId, bool pressed) + { + u32 kcode = vgamepad::controlToDcKey(controlId); + if (kcode == 0) + return; + if (handleButtonInput(buttonState, kcode, pressed)) + return; + if (kcode == DC_AXIS_LT) { + gamepad_axis_input(DC_AXIS_LT, pressed ? 0x7fff : 0); + } + else if (kcode == DC_AXIS_RT) { + gamepad_axis_input(DC_AXIS_RT, pressed ? 0x7fff : 0); + } + else if (kcode == EMU_BTN_SRVMODE) { + if (pressed) + vgamepad::toggleServiceMode(); + } + else + { + if (pressed) + buttonState |= kcode; + else + buttonState &= ~kcode; + if ((kcode & (DC_DPAD_LEFT | DC_DPAD_RIGHT)) != 0 + && (kcode & (DC_DPAD_UP | DC_DPAD_DOWN)) != 0) + { + // diagonals + gamepad_btn_input(kcode & (DC_DPAD_LEFT | DC_DPAD_RIGHT), pressed); + gamepad_btn_input(kcode & (DC_DPAD_UP | DC_DPAD_DOWN), pressed); + } + else { + gamepad_btn_input(kcode, pressed); + } + } + } + +private: + u32 buttonState = 0; + bool previousFastForward = false; +}; diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 6d6ceb494..e826a6b77 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -1467,7 +1467,7 @@ static void gamepadSettingsPopup(const std::shared_ptr& gamepad) #if defined(__ANDROID__) || defined(TARGET_IPHONE) vgamepad::ImguiVGamepadTexture tex; - ImGui::Image(tex.getId(), ScaledVec2(300, 112.5f), ImVec2(0, 1), ImVec2(1, 0.25f)); + ImGui::Image(tex.getId(), ScaledVec2(300.f, 150.f), ImVec2(0, 1), ImVec2(1, 0)); #endif const char *gamepadPngTitle = "Select a PNG file"; if (ImGui::Button("Choose Image...", ScaledVec2(150, 30))) diff --git a/core/ui/vgamepad.cpp b/core/ui/vgamepad.cpp index b4d7e1d58..53867fdf5 100644 --- a/core/ui/vgamepad.cpp +++ b/core/ui/vgamepad.cpp @@ -31,14 +31,13 @@ #include "cfg/cfg.h" #include "input/gamepad.h" #include "hw/naomi/naomi_cart.h" +#include "hw/naomi/card_reader.h" #include "hw/maple/maple_devs.h" #include namespace vgamepad { -static void loadLayout(); - struct Control { Control() = default; @@ -53,9 +52,11 @@ struct Control }; static Control Controls[_Count]; static bool Visible = true; +static bool serviceMode; static float AlphaTrans = 1.f; static ImVec2 StickPos; // analog stick position [-1, 1] constexpr char const *BTN_PATH = "picture/buttons.png"; +constexpr char const *BTN_PATH_ARCADE = "picture/buttons-arcade.png"; constexpr char const *CFG_SECTION = "vgamepad"; void displayCommands() @@ -87,6 +88,14 @@ void displayCommands() ImGui::End(); } +static const char *getButtonsResPath() { + return settings.platform.isConsole() ? BTN_PATH : BTN_PATH_ARCADE; +} + +static const char *getButtonsCfgName() { + return settings.platform.isConsole() ? "image" : "image_arcade"; +} + static bool loadOSDButtons(const std::string& path) { if (path.empty()) @@ -102,7 +111,7 @@ static bool loadOSDButtons(const std::string& path) if (image_data == nullptr) return false; try { - imguiDriver->updateTexture(BTN_PATH, image_data, width, height, false); + imguiDriver->updateTexture(getButtonsResPath(), image_data, width, height, false); } catch (...) { // vulkan can throw during resizing } @@ -115,25 +124,28 @@ static ImTextureID loadOSDButtons() { ImTextureID id{}; // custom image - std::string path = cfgLoadStr(CFG_SECTION, "image", ""); + std::string path = cfgLoadStr(CFG_SECTION, getButtonsCfgName(), ""); if (loadOSDButtons(path)) return id; - // legacy buttons.png in data folder - if (loadOSDButtons(get_readonly_data_path("buttons.png"))) - return id; - // also try the home folder (android) - if (loadOSDButtons(get_readonly_config_path("buttons.png"))) - return id; + if (settings.platform.isConsole()) + { + // legacy buttons.png in data folder + if (loadOSDButtons(get_readonly_data_path("buttons.png"))) + return id; + // also try the home folder (android) + if (loadOSDButtons(get_readonly_config_path("buttons.png"))) + return id; + } // default in resource size_t size; - std::unique_ptr data = resource::load(BTN_PATH, size); + std::unique_ptr data = resource::load(getButtonsResPath(), size); stbi_set_flip_vertically_on_load(1); int width, height, n; u8 *image_data = stbi_load_from_memory(data.get(), (int)size, &width, &height, &n, STBI_rgb_alpha); if (image_data != nullptr) { try { - id = imguiDriver->updateTexture(BTN_PATH, image_data, width, height, false); + id = imguiDriver->updateTexture(getButtonsResPath(), image_data, width, height, false); } catch (...) { // vulkan can throw during resizing } @@ -144,42 +156,100 @@ static ImTextureID loadOSDButtons() ImTextureID ImguiVGamepadTexture::getId() { - ImTextureID id = imguiDriver->getTexture(BTN_PATH); + ImTextureID id = imguiDriver->getTexture(getButtonsResPath()); if (id == ImTextureID()) id = loadOSDButtons(); return id; } -constexpr float vjoy_sz[2][_Count] = { - // L U R D X Y B A St LT RT Ana Stck FF LU RU LD RD - { 64,64,64,64, 64,64,64,64, 64, 90,90, 128, 64, 64, 64,64,64,64 }, - { 64,64,64,64, 64,64,64,64, 64, 64,64, 128, 64, 64, 64,64,64,64 }, +constexpr float vjoy_tex[_Count][4] = { + // L + { 0, 0, 64, 64 }, + // U + { 64, 0, 64, 64 }, + // R + { 128, 0, 64, 64 }, + // D + { 192, 0, 64, 64 }, + // Y, btn3 + { 256, 0, 64, 64 }, + // X, btn2 + { 320, 0, 64, 64 }, + // B, btn1 + { 384, 0, 64, 64 }, + // A, btn0 + { 448, 0, 64, 64 }, + + // Start + { 0, 64, 64, 64 }, + // LT + { 64, 64, 90, 64 }, + // RT + { 154, 64, 90, 64 }, + // Analog + { 244, 64, 128, 128 }, + // Stick + { 372, 64, 64, 64 }, + // Fast forward + { 436, 64, 64, 64 }, + + // C, btn4 + { 0, 128, 64, 64 }, + // Z, btn5 + { 64, 128, 64, 64 }, + + // service mode + { 0, 192, 64, 64 }, + // insert card + { 64, 192, 64, 64 }, + + // Special controls + // service + { 128, 128, 64, 64 }, + // coin + { 384, 128, 64, 64 }, + // test + { 448, 128, 64, 64 }, }; +static ImVec2 coinUV0, coinUV1; +static ImVec2 serviceUV0, serviceUV1; +static ImVec2 testUV0, testUV1; + constexpr float OSD_TEX_W = 512.f; constexpr float OSD_TEX_H = 256.f; static void setUV() { - float u = 0; - float v = 0; int i = 0; for (auto& control : Controls) { - control.uv0.x = (u + 1) / OSD_TEX_W; - control.uv0.y = 1.f - (v + 1) / OSD_TEX_H; - control.uv1.x = (u + vjoy_sz[0][i] - 1) / OSD_TEX_W; - control.uv1.y = 1.f - (v + vjoy_sz[1][i] - 1) / OSD_TEX_H; - - u += vjoy_sz[0][i]; - if (u >= OSD_TEX_W) { - u -= OSD_TEX_W; - v += vjoy_sz[1][i]; - } + control.uv0.x = (vjoy_tex[i][0] + 1) / OSD_TEX_W; + control.uv0.y = 1.f - (vjoy_tex[i][1] + 1) / OSD_TEX_H; + control.uv1.x = (vjoy_tex[i][0] + vjoy_tex[i][2] - 1) / OSD_TEX_W; + control.uv1.y = 1.f - (vjoy_tex[i][1] + vjoy_tex[i][3] - 1) / OSD_TEX_H; i++; + if (i >= _VisibleCount) + break; } + serviceUV0.x = (vjoy_tex[i][0] + 1) / OSD_TEX_W; + serviceUV0.y = 1.f - (vjoy_tex[i][1] + 1) / OSD_TEX_H; + serviceUV1.x = (vjoy_tex[i][0] + vjoy_tex[i][2] - 1) / OSD_TEX_W; + serviceUV1.y = 1.f - (vjoy_tex[i][1] + vjoy_tex[i][3] - 1) / OSD_TEX_H; + i++; + coinUV0.x = (vjoy_tex[i][0] + 1) / OSD_TEX_W; + coinUV0.y = 1.f - (vjoy_tex[i][1] + 1) / OSD_TEX_H; + coinUV1.x = (vjoy_tex[i][0] + vjoy_tex[i][2] - 1) / OSD_TEX_W; + coinUV1.y = 1.f - (vjoy_tex[i][1] + vjoy_tex[i][3] - 1) / OSD_TEX_H; + i++; + testUV0.x = (vjoy_tex[i][0] + 1) / OSD_TEX_W; + testUV0.y = 1.f - (vjoy_tex[i][1] + 1) / OSD_TEX_H; + testUV1.x = (vjoy_tex[i][0] + vjoy_tex[i][2] - 1) / OSD_TEX_W; + testUV1.y = 1.f - (vjoy_tex[i][1] + vjoy_tex[i][3] - 1) / OSD_TEX_H; + i++; + } static OnLoad _(&setUV); @@ -191,18 +261,6 @@ void hide() { Visible = false; } -void setPosition(ControlId id, float x, float y, float w, float h) -{ - verify(id >= 0 && id < _VisibleCount); - auto& control = Controls[id]; - control.pos.x = x; - control.pos.y = y; - if (w != 0) - control.size.x = w; - if (h != 0) - control.size.y = h; -} - ControlId hitTest(float x, float y) { for (const auto& control : Controls) @@ -215,16 +273,17 @@ ControlId hitTest(float x, float y) u32 controlToDcKey(ControlId control) { + const bool arcade = settings.platform.isArcade(); switch (control) { case Left: return DC_DPAD_LEFT; case Up: return DC_DPAD_UP; case Right: return DC_DPAD_RIGHT; case Down: return DC_DPAD_DOWN; - case X: return DC_BTN_X; - case Y: return DC_BTN_Y; - case B: return DC_BTN_B; - case A: return DC_BTN_A; + case X: return serviceMode ? DC_DPAD2_DOWN : arcade ? DC_BTN_C : DC_BTN_X; + case Y: return arcade ? DC_BTN_X : DC_BTN_Y; + case B: return serviceMode ? DC_DPAD2_UP : DC_BTN_B; + case A: return serviceMode ? DC_BTN_D : DC_BTN_A; case Start: return DC_BTN_START; case LeftTrigger: return DC_AXIS_LT; case RightTrigger: return DC_AXIS_RT; @@ -233,6 +292,11 @@ u32 controlToDcKey(ControlId control) case RightUp: return DC_DPAD_RIGHT | DC_DPAD_UP; case LeftDown: return DC_DPAD_LEFT | DC_DPAD_DOWN; case RightDown: return DC_DPAD_RIGHT | DC_DPAD_DOWN; + // Arcade + case Btn4: return DC_BTN_Y; + case Btn5: return DC_BTN_Z; + case InsertCard: return DC_BTN_INSERT_CARD; + case ServiceMode: return EMU_BTN_SRVMODE; default: return 0; } } @@ -246,6 +310,19 @@ float getControlWidth(ControlId control) { return Controls[control].size.x; } +void toggleServiceMode() +{ + serviceMode = !serviceMode; + if (serviceMode) { + Controls[A].disabled = false; + Controls[B].disabled = false; + Controls[X].disabled = false; + } + else { + startGame(); + } +} + static void drawButtonDim(ImDrawList *drawList, const Control& control, int state) { if (control.disabled) @@ -255,15 +332,37 @@ static void drawButtonDim(ImDrawList *drawList, const Control& control, int stat ImVec2 pos = control.pos * scale_h; ImVec2 size = control.size * scale_h; pos.x += offs_x; - if (static_cast(&control - &Controls[0]) == AnalogStick) + ControlId controlId = static_cast(&control - &Controls[0]); + if (controlId == AnalogStick) pos += StickPos * size; float col = (0.5f - 0.25f * state / 255) * AlphaTrans; float alpha = (100.f - config::VirtualGamepadTransparency) / 100.f * AlphaTrans; ImVec4 color(col, col, col, alpha); + const ImVec2* uv0 = &control.uv0; + const ImVec2* uv1 = &control.uv1; + if (serviceMode) + switch (controlId) + { + case A: + uv0 = &coinUV0; + uv1 = &coinUV1; + break; + case B: + uv0 = &serviceUV0; + uv1 = &serviceUV1; + break; + case X: + uv0 = &testUV0; + uv1 = &testUV1; + break; + default: + break; + } + ImguiVGamepadTexture tex; - tex.draw(drawList, pos, size, control.uv0, control.uv1, color); + tex.draw(drawList, pos, size, *uv0, *uv1, color); } static void drawButton(ImDrawList *drawList, const Control& control, bool state) { @@ -272,7 +371,6 @@ static void drawButton(ImDrawList *drawList, const Control& control, bool state) void draw() { -#ifndef __ANDROID__ if (Controls[Left].pos.x == 0.f) { loadLayout(); @@ -280,7 +378,6 @@ void draw() // mark done Controls[Left].pos.x = 1e-12f; } -#endif ImDrawList *drawList = ImGui::GetBackgroundDrawList(); drawButton(drawList, Controls[Left], kcode[0] & DC_DPAD_LEFT); @@ -288,10 +385,10 @@ void draw() drawButton(drawList, Controls[Right], kcode[0] & DC_DPAD_RIGHT); drawButton(drawList, Controls[Down], kcode[0] & DC_DPAD_DOWN); - drawButton(drawList, Controls[X], kcode[0] & (settings.platform.isConsole() ? DC_BTN_X : DC_BTN_C)); + drawButton(drawList, Controls[X], kcode[0] & (serviceMode ? DC_DPAD2_DOWN : settings.platform.isConsole() ? DC_BTN_X : DC_BTN_C)); drawButton(drawList, Controls[Y], kcode[0] & (settings.platform.isConsole() ? DC_BTN_Y : DC_BTN_X)); - drawButton(drawList, Controls[B], kcode[0] & DC_BTN_B); - drawButton(drawList, Controls[A], kcode[0] & DC_BTN_A); + drawButton(drawList, Controls[B], kcode[0] & (serviceMode ? DC_DPAD2_UP : DC_BTN_B)); + drawButton(drawList, Controls[A], kcode[0] & (serviceMode ? DC_BTN_D : DC_BTN_A)); drawButton(drawList, Controls[Start], kcode[0] & DC_BTN_START); @@ -303,6 +400,12 @@ void draw() drawButton(drawList, Controls[AnalogStick], false); drawButton(drawList, Controls[FastForward], false); + + drawButton(drawList, Controls[Btn4], kcode[0] & DC_BTN_Y); + drawButton(drawList, Controls[Btn5], kcode[0] & DC_BTN_Z); + drawButton(drawList, Controls[ServiceMode], !serviceMode); + drawButton(drawList, Controls[InsertCard], kcode[0] & DC_BTN_INSERT_CARD); + AlphaTrans += ((float)Visible - AlphaTrans) / 2; } @@ -349,6 +452,7 @@ struct LayoutElement void reset() { + applyUiScale(); scale = 1.f; const float dcw = 480.f * (float)settings.display.width / settings.display.height; const float uiscale = getUIScale(); @@ -374,6 +478,11 @@ static LayoutElement Layout[] { { "RT", -32.f,-240.f, 90.f, 64.f }, { "analog", 40.f,-320.f, 128.f, 128.f }, { "fforward", -24.f, 24.f, 64.f, 64.f }, + + { "btn4", -24.f,-216.f, 64.f, 64.f }, + { "btn5", -152.f,-216.f, 64.f, 64.f }, + { "service", -24.f, 96.f, 64.f, 64.f }, + { "inscard", 40.f,-250.f, 64.f, 64.f }, }; static void applyLayout() @@ -440,6 +549,26 @@ static void applyLayout() scale = Layout[Elem_FForward].scale * uiscale; Controls[FastForward].pos = { Layout[Elem_FForward].x * dcw - dx, Layout[Elem_FForward].y * 480.f }; Controls[FastForward].size = { Layout[Elem_FForward].dw * scale, Layout[Elem_FForward].dh * scale }; + + // ARCADE + // Button 4 + scale = Layout[Elem_Btn4].scale * uiscale; + Controls[Btn4].pos = { Layout[Elem_Btn4].x * dcw - dx, Layout[Elem_Btn4].y * 480.f }; + Controls[Btn4].size = { Layout[Elem_Btn4].dw * scale, Layout[Elem_Btn4].dh * scale }; + // Button 5 + scale = Layout[Elem_Btn5].scale * uiscale; + Controls[Btn5].pos = { Layout[Elem_Btn5].x * dcw - dx, Layout[Elem_Btn5].y * 480.f }; + Controls[Btn5].size = { Layout[Elem_Btn5].dw * scale, Layout[Elem_Btn5].dh * scale }; + + // Service Mode + scale = Layout[Elem_ServiceMode].scale * uiscale; + Controls[ServiceMode].pos = { Layout[Elem_ServiceMode].x * dcw - dx, Layout[Elem_ServiceMode].y * 480.f }; + Controls[ServiceMode].size = { Layout[Elem_ServiceMode].dw * scale, Layout[Elem_ServiceMode].dh * scale }; + + // Insert Card + scale = Layout[Elem_InsertCard].scale * uiscale; + Controls[InsertCard].pos = { Layout[Elem_InsertCard].x * dcw - dx, Layout[Elem_InsertCard].y * 480.f }; + Controls[InsertCard].size = { Layout[Elem_InsertCard].dw * scale, Layout[Elem_InsertCard].dh * scale }; } void applyUiScale() { @@ -447,7 +576,7 @@ void applyUiScale() { element.applyUiScale(); } -static void loadLayout() +void loadLayout() { for (auto& element : Layout) { element.reset(); @@ -456,7 +585,7 @@ static void loadLayout() applyLayout(); } -static void saveLayout() +void saveLayout() { cfgSetAutoSave(false); for (auto& element : Layout) @@ -500,11 +629,11 @@ void scaleElement(Element element, float factor) void loadImage(const std::string& path) { if (path.empty()) { - cfgSaveStr(CFG_SECTION, "image", ""); + cfgSaveStr(CFG_SECTION, getButtonsCfgName(), ""); loadOSDButtons(); } else if (loadOSDButtons(path)) { - cfgSaveStr(CFG_SECTION, "image", path); + cfgSaveStr(CFG_SECTION, getButtonsCfgName(), path); } } @@ -554,8 +683,13 @@ static void disableControl(ControlId ctrlId) void startGame() { enableAllControls(); + serviceMode = false; if (settings.platform.isConsole()) { + disableControl(Btn4); + disableControl(Btn5); + disableControl(ServiceMode); + disableControl(InsertCard); switch (config::MapleMainDevices[0]) { case MDT_LightGun: @@ -590,9 +724,17 @@ void startGame() else { // arcade game - // FIXME RT is used as mod key for coin, test, service (ABX) - // FIXME RT and LT are buttons 4 & 5 in arcade mode - // TODO insert card button for card games + if (!card_reader::readerAvailable()) + disableControl(InsertCard); + if (settings.platform.isAtomiswave()) { + disableControl(Btn5); + } + else if (settings.platform.isSystemSP()) + { + disableControl(Y); + disableControl(Btn4); + disableControl(Btn5); + } if (NaomiGameInputs != nullptr) { bool fullAnalog = false; @@ -618,6 +760,14 @@ void startGame() } if (!fullAnalog) disableControl(AnalogArea); + if (!lt) + disableControl(LeftTrigger); + else + disableControl(Btn5); + if (!rt) + disableControl(RightTrigger); + else + disableControl(Btn4); u32 usedButtons = 0; for (const auto& button : NaomiGameInputs->buttons) { @@ -627,21 +777,17 @@ void startGame() } if (settings.platform.isAtomiswave()) { - // button order: A B X Y RT - /* these ones are always needed for now + // button order: A B X Y B4 if ((usedButtons & AWAVE_BTN0_KEY) == 0) disableControl(A); if ((usedButtons & AWAVE_BTN1_KEY) == 0) disableControl(B); if ((usedButtons & AWAVE_BTN2_KEY) == 0) disableControl(X); - if ((usedButtons & AWAVE_BTN4_KEY) == 0 && !rt) - disableControl(RightTrigger); - */ if ((usedButtons & AWAVE_BTN3_KEY) == 0) disableControl(Y); - if (!lt) - disableControl(LeftTrigger); + if ((usedButtons & AWAVE_BTN4_KEY) == 0) + disableControl(Btn4); if ((usedButtons & AWAVE_UP_KEY) == 0) disableControl(Up); if ((usedButtons & AWAVE_DOWN_KEY) == 0) @@ -650,10 +796,30 @@ void startGame() disableControl(Left); if ((usedButtons & AWAVE_RIGHT_KEY) == 0) disableControl(Right); + if ((usedButtons & AWAVE_START_KEY) == 0) + disableControl(Start); + } + else if (settings.platform.isSystemSP()) + { + if ((usedButtons & DC_BTN_A) == 0) + disableControl(A); + if ((usedButtons & DC_BTN_B) == 0) + disableControl(B); + if ((usedButtons & DC_BTN_C) == 0) + disableControl(X); + if ((usedButtons & DC_DPAD_UP) == 0) + disableControl(Up); + if ((usedButtons & DC_DPAD_DOWN) == 0) + disableControl(Down); + if ((usedButtons & DC_DPAD_LEFT) == 0) + disableControl(Left); + if ((usedButtons & DC_DPAD_RIGHT) == 0) + disableControl(Right); + if ((usedButtons & DC_BTN_START) == 0) + disableControl(Start); } else { - /* these ones are always needed for now if ((usedButtons & NAOMI_BTN0_KEY) == 0) disableControl(A); if ((usedButtons & NAOMI_BTN1_KEY) == 0) @@ -661,16 +827,15 @@ void startGame() if ((usedButtons & NAOMI_BTN2_KEY) == 0) // C disableControl(X); - if ((usedButtons & NAOMI_BTN4_KEY) == 0 && !rt) - // Y - disableControl(RightTrigger); - */ if ((usedButtons & NAOMI_BTN3_KEY) == 0) // X disableControl(Y); - if ((usedButtons & NAOMI_BTN5_KEY) == 0 && !lt) + if ((usedButtons & NAOMI_BTN4_KEY) == 0) + // Y + disableControl(Btn4); + if ((usedButtons & NAOMI_BTN5_KEY) == 0) // Z - disableControl(LeftTrigger); + disableControl(Btn5); if ((usedButtons & NAOMI_UP_KEY) == 0) disableControl(Up); if ((usedButtons & NAOMI_DOWN_KEY) == 0) @@ -679,28 +844,41 @@ void startGame() disableControl(Left); if ((usedButtons & NAOMI_RIGHT_KEY) == 0) disableControl(Right); + if ((usedButtons & NAOMI_START_KEY) == 0) + disableControl(Start); } } - else if (settings.input.lightgunGame) - { - disableControl(Y); - disableControl(AnalogArea); - disableControl(LeftTrigger); - disableControl(Up); - disableControl(Down); - disableControl(Left); - disableControl(Right); - } else { - // all analog games *should* have an input description - disableControl(AnalogArea); + if (settings.input.lightgunGame) + { + // TODO enable mouse? + disableControl(A); + disableControl(X); + disableControl(Y); + disableControl(Btn4); + disableControl(Btn5); + disableControl(AnalogArea); + disableControl(LeftTrigger); + disableControl(RightTrigger); + disableControl(Up); + disableControl(Down); + disableControl(Left); + disableControl(Right); + } + else + { + // all analog games *should* have an input description + disableControl(AnalogArea); + disableControl(LeftTrigger); + disableControl(RightTrigger); + } } } - bool enabledState[_Count]; - for (int i = 0; i < _Count; i++) - enabledState[i] = !Controls[i].disabled; - setEnabledControls(enabledState); +} + +void resetEditing() { + resetLayout(); } #ifndef __ANDROID__ @@ -721,13 +899,6 @@ void stopEditing(bool canceled) saveLayout(); } -void resetEditing() { - resetLayout(); -} - -void setEnabledControls(bool enabled[_Count]) { -} - #endif } // namespace vgamepad diff --git a/core/ui/vgamepad.h b/core/ui/vgamepad.h index 86b099aa7..d97fb21f0 100644 --- a/core/ui/vgamepad.h +++ b/core/ui/vgamepad.h @@ -40,13 +40,18 @@ enum ControlId AnalogStick, FastForward, + Btn4, + Btn5, + ServiceMode, + InsertCard, + LeftUp, RightUp, LeftDown, RightDown, _Count, - _VisibleCount = FastForward + 1, + _VisibleCount = LeftUp, }; enum Element @@ -59,6 +64,10 @@ enum Element Elem_RT, Elem_Analog, Elem_FForward, + Elem_Btn4, + Elem_Btn5, + Elem_ServiceMode, + Elem_InsertCard, }; class ImguiVGamepadTexture : public ImguiTexture @@ -69,8 +78,6 @@ class ImguiVGamepadTexture : public ImguiTexture #if defined(__ANDROID__) || defined(TARGET_IPHONE) -void setPosition(ControlId id, float x, float y, float w = 0.f, float h = 0.f); // Legacy android -void setEnabledControls(bool enabled[_Count]); // Legacy android void enableAllControls(); void show(); void hide(); @@ -87,11 +94,14 @@ ControlId hitTest(float x, float y); u32 controlToDcKey(ControlId control); void setAnalogStick(float x, float y); float getControlWidth(ControlId); +void toggleServiceMode(); void applyUiScale(); Element layoutHitTest(float x, float y); void translateElement(Element element, float dx, float dy); void scaleElement(Element element, float factor); +void loadLayout(); +void saveLayout(); #else diff --git a/resources/picture/buttons-arcade.png b/resources/picture/buttons-arcade.png new file mode 100644 index 0000000000000000000000000000000000000000..980212cffc6f4ca38dbdf17d83d3ede431f19c3f GIT binary patch literal 230003 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�CV1w1!Z@rD*1+VLs6u@DNOr&Za}>!7=z9lH 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

ig zm%^K!M;W+jfylCUVwg?E!$t0ZNXo-TZVd~w@@()B&4@zlpq}7iG=(c?(X^sh{UtDv zL4ujMgq+qZ!Ri`YTc>ZdyS>4HnFK%J0+^GXu#Nwo4cxHMmFuuIVyRzcx<>tEvcEO( zD%?}47%d{>17%>=9-gUnD&ocr?uT>o6V)1v@LI%_gs}|JQ(oO%d%NVh2#1&+PsMF> z{I{oK3b|p>$oFf5AzDkZTf?96ZDyefB8$O5 zI*~ZBq^`hN{c4LLLbmH{#uMoseGi$)wcqvQa6}3{34v|t2`#OvPNwNJW`t4tMciyX=0sk}2F^g|j05}t? zDd`qR-2yrrfZu@_d~|btrxeYq+2zg`KM@|T&=cfJ4y@ww&S!^7rJgd zrK+(EQecH%b1m5lD<7cA#RFCe5!eZD0kc^lyv_)2V6J&#`ctV2R%rtlzYw_@gsy3J$ATB2=`CeFo52%QGdq~6ZAcJn7JynA36dV5{H9S&<(Zq4 zGXjP!8>NA<1WVZJOnO+rg$whC@k`(aZ~VyEkpDs=0o!GvbtwgYjWM2R?Qi&6$1D>z z(=^Pozx|gsBKB)32SFkI(2W=s9Ir4pIkaLJ|M>}ofzgSw_Oy!WSrEo(Gg1YI@N675 z*lZ$jIh5)N&MF%F#Wz^U*o&njG9S?ZZ&+nLa5UUAL5>BMIcuJQ_F${r(M61&v{9ga zQGaxbG!2B^TWp(o$xb#-aG}DnUj`G;!`q96vnx~x2pcds&5l;b9umMkHjhB`Lo^4^ zl&BgJ&!maoyE8M;KFQS;d(zjadhMcnYEC|)F=nUQVTQ2hQli+`*pyxUi`XM+0>Z#6 zcs@{raB4W94=dv^QkJbtPWLF<<4er$^%4#l&B^XFJAJZq(D?g9FY;GNpHO(^R@U=Y zwQUVT(rLzslH9c+zYb_fuzYt0#nUu6^>S?L0$c0nC&_Prn&DV`k8daiYa%c-HD%>; zF#G{dMR&q5RTVz(9iK7|{=S~-Uee7rI_04#2K`{066TE!;83=lK7kPuW2lxl_ z(4#QE)8^UB*E7owXhl0%`Yxw2Q@+a*kLmgUVs3xnS-T}Rf>i09aC%Lup#vHU`+$#L zW&J=nPY}g|jYA*XPIiu+xFNO;T?QJdx?%$O!4AYdfTpT23>}WPNL&bQ_f!P3F@Jj|x;6_2O=euv}_)3x2{Q zoAnMUn$9DyGnpc!TYqNE!6A{Nee@Q#FYwqqiU1B4iS>*Isxh(K^1$(O~sfrH{Ig zw{@197nJvj8djY{RiLb7CA-CMAk&)dJ&-e;gftGs0d{-Z{aQjvYdvyt?%0@Wy!&wTj>c8Gt99yR)jnh2iUAouSFw|tJdkA{*T31Q7J(Z%oIqURT{EKL@ zz;lT*&+fAgUS)?+?Ea@OvaDwZR0gXR(B%iwF`AI(g^q@~cCOf^oJz;?uqAeo zN3f9>A2xhN4U?+&D*$kokq{ZeLGBuysy8ufw0GZX(BXT@@s7`B$0O(}8k@~5FW-o> zIS{5SZ)4*&bwUOaAyH19RK86_CeAdAUa#|vL`zF;ciEc%X#rYHjvhTQqbdv^nsF*Z zMotmxvvb2H&YHJ(B{C5u$a|378=JBQP4Q6*3>j8DH*LP#>52 zI^+IC{q$fY#E6nC^r$7qtSoUV`7vtsmXDi~%I-=1F6h=JQ1zN(qjEeC8&OkP7Zi#x zfdb`2vUkC+bM1`EC`myt!A@l6z5Gkd_Xs@OKns|V*i?wN=ro*>+| zT1*TEg|uI>C&icbQZnZu7VL(s%?4f}`dRx;3y0#wR{wcZ{cl>Aa&5k@vbtDE#n317 z#tS17;`C66_8gUWqt3gmxH1ltbYv-_DOuZR=ZY_6%5iMnIh+lQV>E9E!GlC%Smy4c z!Ft3~ksU2pc+D1r)q^Z7x*Bn6`{!uMTJik#h}L7(L?R9c2B8&8(VN^Q-O|{rutOuy zvIUp(UwX=kf>y5+TiHd5F?`%Yv&=LyAd0)CAz{N)!9)z*)ODT%VdFacVq00!Ixjaj zS|sEoLL?uh8tCXSokH{@PCQz#!?SyLeux$AEzj-dVE6?ONhKs#>CMi3k73I{BMf=M zt*Hya_XE{_^>j}z1gk*>If8K0Vt-mRJNhrar=IUy*i35d4$zN1kn*6^bHE_($BJu& z+GG{NcwXVaYVYaUVM2F@Awh`lStv9-$m33@ZCuRPK@F`ye~ zmT;d8?2f%AgxGi=8^c|VFd?{8lMC|lmj2PCts1nljc@zPefGxQ@}7Cp>$RDkq{EvK zlymz-0;CjEleB0u;1+5W6)}}@CvST$clv=)Nje3;Q|y#@J#RR6pvLyfkZM7#x!1-8 zTIB3^2oDxtjl4Gv&&k>L5{=0jrvgoPkdrmOoU$VO?rKX(J|vG#$y$gx%bxx zkz-m#acb}6-k@|f?3AnP!L;;rpmbmwdEJs_mSAX@+DCkPR6BPhWE*S4_6sz9vEBJ{ z+Yq1;IEZm#LCAuJvOH*O=7>l8eYE$K|KKOReNBrh-rhQ0|Ld0@oH@L2n<`+ zq=wnDqI=zXDuS_NV6!HEo-NP8jAK=TTr8>BL3%{acEfn??seh-{SCx2_QRxyvr!3f zK|Bh30r-HC!iY6WLo*76qL>=`LJ?9iY2dd>LCex_f8Ex`CwP`oo{OXlSjToiukrU! zUnPDV%C2gjHr!8+?(S>|W-@oSQeTB{AOzWTE4wM{r7o@U2{9;%+@*!sybt#?kJd~? zoV(LHBnL<(ElmjDy82K5is~{b3)zY1?T={K2~zA%ukgy+$5n@ zdhn<24iK!WKe!p`@HYvCB3^BElkY6GBA_r17HivgR?pk|kAGRJHd#p&YlbO|Mn`)< zdk^3w^JW+n=hVN()CWhYe!re~D!rn$t^$K0JMy0qEPKy}r$lbgo@46BFhd>uhKA9? zqnx4v(y}jtC2tTvAR^3&ZjKj1_l5~cCuqFXK^^>x%xMHQ4MSC?LSoayN26d^=U(~& zrdt(MxqB`FZ7r72?AVBGit5#nV>BtAC5F^ww6UQ?$NBX(kREMUaP zF-(+U)~oXWBY+SD;1xZ8UL#!P1(2JBIBo515qch3SLmKk(z5K9z}PrlxM11-uE9;A z_~g60-AP8-V;{8{E9x&l<0=AabPLv(dFV6JlkVKPfq$P{GP929@|!n@ck9JW)VtN!Q_k*gt zSjkY!Lrn=s`(5R82jPgJR?(V;MD?bRA8T~dY4AZII;dS_mFZy@qlSRqOsBuFrsiYB z?*lva2~yjT`M2jh@%n0XXRb3)vCa*`y3ZO_H?^{|G#3_#y)BGx;7Q=1^Mu3E3==`# z6DG%~8-(`ym9#G={jmRcW)%B&`vIB61EAwvVSG2v<4TOJ=oyP1q90wG!K691qLf#A z1!b(>(99f+Y1l8DwMsM9B4QZ;%wp!qd!J3!e^UCFC8Q|yt;Ov3>Hhfd!^w+6h4$IV z{k7~hTU(3QH?TdSn|sWwNhcz}arw;$MzX}BJ~!jB={G^9hgTc0V;PO(6*v?wauqt_ zuTYFDD%S+V)wgqW!u=`}AKA1=$t-9J1sq1z{ZxrMxh}+e)tg9R^pS(>I|eVfv(>-c zAfp`4eU#r@g=*j+@2$XIFALZ34(y0H>a}6BSchdY({A@sy(g!S-C`>x+_g=8Gn#bw ztrT4k`7J!Xusi@)2QWYmAOXeFn`G@?e$KEvzthLW{X~4AQ9Izh2%7C)PvRCT#P%m4 zqZ*Piz{_nx6O|S%SKh}xQYShIcSfx<^jByhDtqwLzNmG09=NjThgE_Xo(yJY=Is#R z<@p?C4^I?}mQ8)~u-AC>Heo0?ee+JSgdWfLslDJ4uEMmWy-!7D)d?PBe1mGFB%vJ= z(OW2@c=q7_uz-PIF!*2hN$NQ{{$(4|q1Cq$=@=$_jr3?*Yqd;E5I<}xb5X-S};H{r(K%y4576{G@c!Sj>nklN-L9Q+c@ zxh6^Hq>as{9Esi1QWU{Y$nq{oO-?=woJh`o`TGVK%bq@b_!FYgY?|Bx5Atcl?2g!* zH`m>79BkJv+|Ui#3Ma6bcyM3~H?If(;KXeD0)aV+82S!DyYZ8zDi}Vid`|SH2q2`- zv_vTfxk=&;Udbk(ubGTw#d7q}2nt=?<^i`dz%Tr6a6D3+O1h2Ismn9{%x5@s+8dYk z%b5itV71}_DYqV^zMU0fIOpi@nx-%D);eVvgi}@Z;xfhb*h6B(^uita0g=6~%-W?E zcQH)#W=&HQ3#b(u0^m*Co$XEUW~rRJ?zPG^jGpyP`nzM%4xMuCQcBCCm^dgictfLE zGWzH<4>TTNGbQAOtwYKl;DXvUBwHXPY}*{(TNgWz5(!yeGY_0w5nZ`+l$(bjwkw-a>jpF!8_Bs4d}%0R z?LFb^`&3LGz}(d7eJ@=reeu#c8JgnQbLUNbBpU?IQfPt`NL$_Ic#TAnQFwu~K=VGkDp|C27l zQmWckMvE4&7N!>8td5i zOf4Out!Po*>lQc>m3p@1+Qb2N%~P}K7X4lR$hIR?!F88Hq-$kmRc^A<^@uuAwIqWv zbha-HPbH_e=(^>e>Xl7)7rrq;tr1cCq=p`Gh)Q_6l)xwMq=?SAay10}!|7NSl*gi( zA+;T;C(2d{=n;W-2;F@m+#)o}N+#9lD0q{zDydeN9aTB%xJP}&-nh<4lUMNs`hou0 z3*0wVUf|j~VjSu+zf7rTw2BxjWYwWG@fb-5{&!cT>5;a5IVatU?(n$*8a|DUL8cBB z@{gvcr%w?=THK#F*RIv@8~KpctJ;>L7w1g(1Edw#3tJhOJZn6G@rn>f<@g`)U%qML zyf%LvtLk$RmQA~p4nqyiKn|CpC>VC!4&)p*6}Z+T+aXgKmNMfQ)x>SQb|i% z+C@=mN-8ZW4QYv{iUta)BuZOb8l;elBni=wq>YxAqTThqU+?q#{LbzCbI#kFT-WRM zd_Erc`KV`-8jSuaQ!ZYN`d9xV0AmHR>u-?K_6&Nn<4&Q~F&%|-_+v5SU7B#=CZCN2 zHIk`Zh)|gVOq%J~D|{R6<%BS(0stdgRPiAPxYdBZ#x8;@X8r zzMc=gOYLLxPassq`@_t5?D~4fhf}5&7Cfej2iVfsGvD|kIkg(2A{wCIqO);DKKeu- ziTxE*@$zYwXq@cgV7)rdwyG_hArAT^zAVemtR#Mr7(^1?0ITi49b3Ll$8`Ot4s9YuRehJgP|<&+nTgm4va?haFuI-Lf!hTlSsYo4_=8sG{&(u9$=qKu>VzOg}7>t29+KMEW2dkzZk0ev7 zre8bmQ6pvUZ(7k*&uF6Ha;wc5tBjVnJ4Y9Q&mb9!QVAr7WC9(N2Q0ciiL^JH9A_sN z);fsh~5Cd~<(3lF!Q4w}6#1qSi zEDU9*s-Bfjn%BP9jH$!*t!`Is*sx&=C-n$o8xAGB{{35mM(YYCp1RJ2x=NVm*cpQ} z9jMa!{Q^MsLo6(&IE)Ax6%{ZT4FFHQsQIkk&~=<9DeAJUT3|iyG7@Tbq4Ex|9V^e#bwS?e>{KZoR37_i#Z9$GTIF@Ic-#VT{E~W~f z6RTjf2$``LjcFgDE${@@2?xBu?){B)CFH@ZNDmz0ib8_Je}{-NPwUw)KD-kZ^ZoA3^=(C;dutjD zipjc!#U&wunCxmwL=Y~{Gq`WG7L8A{2E&|!Y_oZtHasaAlY;9rm#ePPqTw%7EM^*H z&hD0q&5e5kEWWQD(k!7Bf`U1pRx4aAhB^4!Z6ZB2bw6UJK(()wR8&;76C%ls7LU9C zrBiHH(21;oPWueaSb{L3!tj<;*qqG?!nP!KI}vaDGUU_M`~(Uamz11*1mj}17<6T* zF?H{oUhtn)4I|addCCguFGwuQJ_AQH5d}S^|c@nGB0&U#Bt?zQG{$g zU%v)`Hh47t_m)9zszkX9qBToZz;h2vCAW;uEg=G`{QChZEp|;)b0!^&*D)DXnT%_I z7EvQ~yr8s{?1}8GFuEC$m}^(L+lL@=KnOqZ-&hI)fK6SQlVDBwz~?xveL=^K%0H8a_7TF_X{p>4R0sj z%)D&3Idtv}sLPYwFcV2o9Q7C_fvR5;`lH&~)N%6p&cxzMLV{YGZS~w}fcG{%4!{)& zc=1Yz8MKrC1-PEp6|^DDw&22jLHZe;QES5gg(xpml=H%eV4HK&00&54`J#RBAwEM4 zlKb=C4Y7?C1fB%*>tV-efW)tHzV9SH)6K9-a&oRkv@!g^9Ec`iO@ zB{D@A0z}udXoT#J9+hwF={b&I(MaXKO#}~<8AP0G=mFt~Ig`&3!)VK)IJO`h)2!VO z^1*)(%Sq4+0HzSIhJmgb%X^M$YX;g&jx1CSloSRg%AJYQCi$P--;Nv!q0_~zfB`zR>C{VI%~@WtaO_=wWwj9v`Lb=hA} zrybL1EhL>SWJNfD*IDH>DaE^o4=AVU%JEAUB#}RY&{o8WjKZJJ>~C*x3F|^ShO`Fp zB}GRfL*(<1 zx@iYgT|2QZpLzYa5!$Tv5`_2SeJ@}nxocRr`ofrmJo~G8V<=(cjz00Lb0Ojv*$1=w zzJ2>PanW2n$wx=rM=M9y=g+g^Efp?XHZ)O~flAPph=CsxGwXNpKEehl>*M$K_D4v6 zFydxCCKi)YSX88JVQ(My)kDFU5~v?9{PYh#;^_rY#; ze(+V4rjmf&85zDiFmNt#((KV~pBIZcdV-elM+VbT9CN4AN09r%rNw^ej2X4bgIj=& zpqo1hUs}Vs%P}YBaNQ-C>tEr4zmH=&f?_}45EJ!@d)K_8vU1GJmoF(|2xCxfVZ^|8 zi?UDsrA+*$DFpl)d*QClH;si0qub*RjL+2J`NGVLZy8AL7sEhw!4v~16xjg*Q)BrV z>wVPhi^<9td5H7?)hQ&RAjLJd;yMnd&daio-nSgI@?;rE;GsCfR2OfegYfF~R!ErY zet)ZMDN;UPr{Kp^uSQ$`edWj2>m*@=zF~ow=D^JjGB97I0noEOS!vCCl@+oq4!nRoA9;C_!;v*z(cazD^R}(jv1jms3%69fFv_2c;a5$Bl1dIb z?ZdKn-=n&^3`%;h)2D+kk1WMv{07?-+6lBrV^_FX{%wQ`jeJBWk{c!n#RnD>Z?UZyn{ai~J1E2-G6BpK zADCVTc-|IX9P|R%78e%oyPO?*@y;P(3raT9g0$^3b2~db^#$09Fb)1m>GhnZW#`l> z$E22u)Td*37JttM=sp2W{~be=PQ2a;;N`{wnBB<_N=lNTYB~v+ikH&m+u>gMkGR7@ zLsD&xL&JSp>&WgI)oGNlDL@o(+nK~Q_7j0~=-IM+5p*u&8P9H`!$z@Jv4hH}t8c|8 zvJxEEG>1;+vR9Gow`KX@D($YbILK`@ux<1&`5Y$^kI{Y*A4e4=_^u~+L+j;arND4$ z0Hf}Cu0kocNgb=8lgI#%s?TulHZWFS`ay1zdEZPgG$TzLo6S+Ik=$a1e@SjR{`Q#% z1qFOZ6jPnMjM|w$`rD|DP5By*7vmYNIsU*;Bf_ z*k*5j?o)p|0H}E7g;!g()f&?JJt%F#;0R1Y0u1UD=iH;=nO)U2q*&eA&PgXzAg zG==1VzY#Yicu&80C>#ToUIh1No>}1+T%rwlT$;`(9A88v<=4$wCZlH1_E{&~4@}8} zE)i&oC9LtljshS}(iA<{lzFsci__ueWLj?=VMoD~zP zuS}n2D(x!9Uq%@ZpQ=QQGH|F>Oh}UGKw31|a5<;NRn7?gHuqr7Z2r>ODGvkv+L(5h z!||eaNV#DaPY`b2^!?taGRL88vlLascFNy!$%|0B#I2hs?JR?j`<;5mbUjEL^2U0- z2QqXGy&^;f@e2#Fm*2T_%>eHboHv2*bdae(^=Kos^sM-lJ-HeWZ?*oLshvgWnBFwX z2SynMiuEm*j&0!3RngjFx^=~lxiAyY4Q95S#<=No8XdP)#_vdxIfxWmh@x$eD0b#v z+QH=6Ku{pdn8?RC1TzF6PLNhsR`&3JMTIS&OsdZ)x^eHuLY-)iiP+7mv4y(h8|%$B zN}QR`Kqvt{aS&pt#BL9fm8T9 zYtz%&`4cC>BRrT9I7LFC1tjZTWi3&(qjI<{tOu4R74i!+A$AA1lqWjd<9{XFhc0QT zuP+X3lv|e^?0R6eH*)B{1Y_*V3Mm|y{uuH`UP61itSMAqCSia!QAsrCp;_tzR@xxg zSlE!~V@lxJ+dIC`^`)3SP%b#vnB!3U&d%_OM(Yme)z?puU=iQ)^E(K`z^J^jc>JC_ z#-Rmz;55=%sSkG0*&!f6>7~y-6+RY@m~1{UQIjguHFHn%(}Q9#NTvY=-AgrF&bSiz z!^aUb>+v&rI2yA~Itv*k{K#l+ZLOPTYm>W^$k;s{MA z29B(~i{GhGK_KPr6&$1^I;QV|J_Wq611yv%l32Kk27n9>9iPkYJK`LT$u-MXC5GFX z9lk)7eN*^uo){Cb3_S2AT)C)b`qvCHL?gm6*7wQl=&}x7&?)ly?VG5>D=K6WSJe)E zBjrz3#~%4e%*f-D$7=(Qjn)&5mN@o!2~bnjHc}9pn8EjYcA)+uuVeYw^Mo_;08r=8 z1mR8c3J-bwPW{)ahF|qB%W_ZHzzW$w^sr5}HwoGpPJ|JynW4_`7;(>5?`7TXNJs`y zogxL4tc9{K2(E}F)Mwl_A-yz--XA;)VN8LF8)QESczg>b{ z-yBH7>;Ap^Vxp+=JBYBm0N|iq47aJEw?IClmg=F4VY@8P8NQ>0I>8}5zuB!@7Xt7o z$5xA>^CJM$C8#ejg~O}F)&~82s4wQ$$X`OWf*_BHMQO1cWoMbsM%UFjB%!Z?=vAg?@E_Ktm&ZLKvB9wQf5b z(r5Y!JB&SAJv}{9B&Z1K$(!uYV$gxXh-+R3DS;6}&*4-kL3CqAE6SR4ii%U!YfSg3l_(JkHhwIhoZtX1Z-a&60Jq+4f><;CY>@0M8aZM+Id@LUliSR=P1_sCQ zIOGDe&=yI&_~F!kuo&pPhuNh&@sO}Wu^z!u#Zz5Dyh02>J|b#>qi{m*2Fn=KF^ABz zhfcyR@PYI{6mfs+d}BW$X_@2lnmk7H{UM^~*__JDR+WpFSA>a6GyzA1b!_f9O zj5moZ5!F{Blm!2=)zE8IIK?JN#C};Z&-#&YJ>w#MjdX3($jZ`a6kWJkwf~stl@iB? zQ@v#;8nEF><(uugrJt*>E%svqG#cU|DsbZ%y>qJASJ&Ul{N*fedttqVF9)`l%k#vZ zW4_L~yzP24n6+WZ2r_=mEy3t!d03bOOE>Pz9o$Shy+3~Z*o}u#@>Ot9S>6Cicvnmc z>e!DVChb0k^u}QyfHII~!sfVwmN;J)P3Ebq*EAMGIf=Y4rS|0VmkxZ8;#u0*Aff=v zAh_#E96c+9oW>89+F#eMqOfzV#!@0kZL2Y2K@i?o@1bx68VTC^Z%9cFc^ka3^k4^K zJSo^b{d5q8y94Y^H$w|8a< zJB2DYz@>`z7yfp(s;|!MKacenxw9xIMOnzM}`yRKT6x3C@BHaeDb*NaWL+-WN)6L2MF(d@n(fDIP|jb&U@yGOaD zSk@B|gok5WM<&X9COPAjpszbx+^GmmxO4LSBYeG*xJ*$m0VESWa!o&dOR#oqAy@1r zLe~HFc##_@UYQKgJupt70f-N!JO9AO^dN>uCrMxaYOq37r3;~nK83N6r#8P}SM3+) ziRD3M34>f%Qvb9)y%1`3tXB81906uCP7gJVU9hpvif}@$Y{3CnRI<#2$l3P?; z*#wbRN#$uqvO7Fn1jd+0|_%}(zOrvOuVGiv=a+)0Ysp|fBgw? zESPpEzMbp30JN+Oa?N(b37M5?WBQL9eZjDdaxuO9>6?VYa+mfzLvo{&Y#NPBxi%B| zo3P1-dK>X1YdG>tpi}n^bRLDfcH?#36HqB@mt76*9??kl z^!UifynAeWJ=pP^(}x#p)#aKt)ZSfPUB=PcS%%OxfiwXkPtnCikhaDhMADMI*Q0JJ zU_7HEf*~_6kK=bl{_YJx(JT#RpjJ0Slchea#Jk=z5W-$ieW$B zC7ledtAlAmr`!_pz+HB=A%TCPu>Z*1IviDFE?INTXz}?d1@8D>d*M0zb=?I#A6qM} zKOlSF5sx#{d{R(xvux@Dc}AP7oGzui;~g@FabPHd#C%S{Y>o1-9um$*Xj#zrlSG-k z%P8qBo&%V6XzD8|T_d^utG^WM6yXwg;)@35j0dl1c6OFhYJAS{C3zrt7u6kjyVhwm z-4T|Ay2_B?r#2Sta)!jGXzgupJgbh4ksYVN0(zK~;rJw~~A&L=lJfp=n!YfAiD6F3(J@};icKzM$knw8j7AVD?r_us$1XW;D& zF!LkA->@Auh)DRGN82|UF;AXMI=KNG?MIn+g(Bie26C;IyNBOY0ZR(QMXL?vIwxJz!uuRo=umEAB91S|C6e!=~6xRovL*BocHe!pYb2(tBKj2d=Y^g`ulJvtv;t zi!=Y9T4|R`Pe;e&LGW0?F^$;}JDY8dyKH4`y-F+1m6!IQe6#HXc#@#+M-83IQ=6&US9Q4G^Iu`ID;75G(b1{WCVhP1`4&etj5^o&K{m(xA&X(Lx z5R{ds(z=R4?`{WSse3Kd`+~lcrCM%Z%vZu8%E^{hQ$<1H*L@)8h33%tfHlc;1EDmn!7m zR}xZ0760T#a>9?rLZ6|4ZN8sty9Mr{shj*bCA$> z!^jBe;0~fW*_bm)niJ5wg$c1+s^B9+m{Tl#i?x4X%WEQ|M6&9$E6eH1Z&&3{f4NtS zLm=+MsU0WFzdo>$YR$@K`=SbI;8}1_J*Yeg7UqcOjn!FzXrmIl`co3kW`2HcVrty) z8L6&+T8t<5Hu6x>4E>X^Oul{F8X;4Zy!#hOyZ((+Iw&SvWJmm4Nv&4*dnfLEf@M4Z zf_i@uq>#ryW@vbgO-!g@d(j6;_2lv6j>GJ)pW(9;gplG3>=m|!=Dt-=o)~4xlG16) zX5Q^C4M5m#>Pv@FgpVDNN>0TgN*jH`gGhm{>mTLVc~)KRnvt5?E}Ym5PySH%lQXHj z^|u-w8L94s;w@Q)pnn>l`#=1PDZ|y$hsn@FQM!G3qb@B+m6JK${^0z=qFVFPLCGJt zbRD%f3B(y$C<0Y`bIIT+$=vlqc5SxgpK`>X0+U7;5FDa#l0)ufmdlAkL`=VpFmf&j z2M13RGlQY6A9;&r(6Cw$Q$V1P&z>@@z+p)KS>>x|m@-{k;$H1`BPqcirAsL9S&SVD z%Q9iW@h)Zl+lps~dxD=vYliJeNocnmA$(|&XFIHbePKxOoLz(e%6H#yo|x=U-Q9W_ z0N{-&X)|849MkXBb~EfIS~^Jux#aeO!v*F%3)7H7FQIu6wX?6x-* z;}TEF!-8z`6}luO&WV|y|FQ#X8*ztXzC}ksJt2l5Kl)FB2>zc&ic_tJ!s)`&Qfa6W zAjCkk;*~F2aiOWe?hPP66Jt4pd$`V@K@ z7n7s{+;juCNM9sjpC}-hIzUF!4G{DY*b-kXKkgOcom4cTtGpo?0*uN?f1D(V|4OtgI{(@icT8&`aEivT9Mf z>hJR5EJW#tQ=5^p8@xa3=F!rw2$bqEBlLSaJ3|K|0{`M%Gh%8Sp(5fM1|1@8+Ble^ zmZ2@&04Q}D!ms;C))3}E=J=l2Mh;zPM@gPpD{~8dtBamq)Sm*uU-nBU=}#a%F|V+Y z5kNW)`zU;DD*V?)Bx;PyE#Ahu5Ch2hmW)5-$F^a^x(qZ!D8qD5Y>qrE5`lk!%ak9v znfP~a7dH>i$(Qa&J<2)oeE#69ZzBHwWVt2Ej~L;?K|0-OG>tbp?lEeg-`~ov&StPZ z-cn6PWhD*M{xSH_olyFY7&}-DO3ezj5jf1Esk)zZf=^LKrp$=TfqQNYk$(ZI8?cZI zss_wRh`*2Et3C0nyx^L2+YjcbSBn4|*7@sqnF};nT=PEn)J7N8Wl(ZJMPJ(KE=|Gi zy~?RJv9vzF8DeSDOm)YF@6>n4^7lxK2Doyo#TiEukaJ{hrKYF%U5hz)nWw+()7?WW z2xoSJm zSgzT1CK9>0MmdnEaXn_Xa#AG3_=@z>$3u9U8V_Pv9EpQmMR3fR{5E`l`{?+1TPqaD z_gfYN&Jp6fi60FKN%>-LPty{OwpJg&0D~0Evsg90r)gX5t+gzNo%9CWAj-(w`OiLd zR8$hZoggTGnHAobViazJND?J=j7~Z1dN0u ztr^C5Mbpp2%eu$LUU$jE{f7gA8~CR*3arsDAG*7ymix$RY>*2M9CWLlDti8W#S4PQ zWew6I*Y3APGdk|X6;I*_Y-J0p|?h=3XP7MNMT4rHXB6e5Jw8 zFo4YK#nrji2IvsT(}N>nG#!4EPja@s@+lb^ z(q3Q&w@>kI4!pNuANo~3bYNw>JW2SFxU0n?o>7O z&&(#(56|8u>`ll}@`@9vpOr_z=Z0LfrKmA7UIm1j_`TeT2L}@g94q=qZ^k)y7v1d1h_5$g->!2 z14ToI2SWDg=l1rqv~lNIPm(N;6n4;|Vg>VzJMdW76kTDCmuMbz1NQ5J1i4a1+TMnb zH>t}iBzjbolmb3E$Q0n$!N;y70LjpY-@VTlsBa8UsSdHkA3ZL}K=T94=pZ&j3JLMYumoJVBt;)zXh zka6WnA~%i!#ZvO0x_5v~xu7C&L81khmBM#u2my`#u6I zU0q$P@uPcv;hJU}zlo?HTdA&(=Hf@ac-u@sH3y<91@|{IElUXKDsRj1uytv1fBr)5YxCl zL|qViq9>3UBZsMrSR~!r>ia^Ec-Pq2_yqj$?{@b}ef*H1Xz)X#i18Y*JcOesLH5SO5{7cSrMltt z4-Hkb%$iJHU(pTt41XyPFAtAMqkK#%onzQU{ySGx!+i$#hI~J58d|o4#74dO-QvjvBz&9Ui1%hljx z)47Zr33&~6%Uqbf>AdHD8KScWDZ&{$Gi{_qv8zr9>zUb#ado_UA<^__l+5;PB9^d$ zPzQ*LZ}1B{bUSdx>L9cb2-~CXYq*epfiF2oc#uBXi6^BXv)Q7o+ zxwVUVpU31TYlMknyp;!jh<1dN=g&V;RaZ|3^`zX3e1NSDGXAC;N7|XL$I`c&;7;~K zcfw{JQ;bTlDErCz^9t>KeW9pvxPV-#@7nJ@*qQvP#aUP^0$ecd z_+ef63>A-?mzRd8?A=q5BXZQ1v<`DnbiB;X%?0zUThQrF`LHGz0cM>*jK3hMdbgZ= z`pSI9NFYAQ!=R4<9gU%sE0w;}n^0!;qi z&wIf!Tt#ry0!2Hpo7lg2{`0Ef(bd>jVU$3_1^h-wVJz_$xa|A%+J8MBfmjbIM~|WT zv+K>GFGPlZ9P%Y$v^gHth<9f%AO`reEiF_jrDzc1WmHu`(7oH$cqa8#j${5O!qUj* zVcPlT$ZRcJ@gzAsXV+ha`W#)$5Y~g=1%EXI7J`wZHcR^_Fvp{c;nsWV5ru!+-ag|k zVKR`;S?@bZG?I;+NGx@*$_dsk^aYbk5YDW#TNzbwW`)QEM2Yz+C<2w9;g6@%F*U#u z-uu)BRCn{h-=M(T&eR|QdGU}N6`PAsg9}xL=&J2b>j<-WlDN>1Z;XTF-6MhNf0sgv zP`73T8Df<7Mh_fZl{kqOqK4|cfnHTArx|sB{mRFgE~u-+z(y$5^?O13xwm`rpD&og zKdOQ~Aep-H^%dv#6CO_UC*?pIe-}>_*}LEIlbwc~=QNC($0>~_xKj~S-PhgMX9UE&Oyc<3ZV=lhiGpe$ z=X95gIKMEY;|sX`sQbYLO2^5YqfZ!R4-GtVbkC!YTE{(02_suAFeUYN4R56yNpz1YgO=OKbd-ONmr(x{h>Vy`d^(GTBKcve`6>BqA*yTtn=||!3e(RF+2i{8i{QGUSucczg zPhYFrQy)@P7t_is_=A-#%*iNDLJ5AVP*+RDrtZK5O~LM$>|x?@t@ zsRN+wg=g=x-Dsd-h?4RLQB28}&BFU+j0U?Mh}f;h5PVHH=P9EDYQAq--To^jA#ng& zTwUPhVK}l7I4PLx7KDE&HXrn245xR$G*9edA@+fNH;%|DDn3$3Wo3%W7GFix-{8VZ z`Qyi2{easUv3bXjSly42LqbU$l;W4#$3jZ0sulzZmVtEE<(P2STqTU?V`F3Ulp>*V zKV`CB^~2bkqwB3T6c9j|sM61U24jRG!fzk)Mxgo;QLv??8!{rFMyN9vsh$ZR6 z#sx5OKa?JKFsR17tL>=BC&zh{EX(l^e?rUgvn?%y7A>Fpxg2Cop<%3lC1pd$vMnXD z&BgVyS)|$C!Gt!U@@j6i!cPS+nv2HL56-$y_Y&TGp193|_=3%GBuzqK`gFZ)(M&3N zHnNo5^Jhz`(d^bY1iJ;Xn9~3C=e_3aSA+>+0-e3Ba+*bK9W6@(HV$^0i=gKX{1fm7 z2q8Y0MA%yb+D(T8;|qLk#)al)W?X+WIqMUDR0d|f6`*k4KDhVZP5kBOV$wNhA}s3< zDDo|SlWB^k_Ya~TI0c$)p}Fb9DfPTS=HPz971kBV>OqgGK;;1}vaWB;uMcQT7XY9z zrf_nT#`^3E3iCdBR?Xqm+|Knk+c3S&Np=q)ANUB=z=5yU*K1ZC7CGyMOzRyO;}|su zP@DF-)aBHvhj^TS!mW+JdaT67oy0go!niJ9zI+C77@TL1Q4CUPSL)yHUpto;M(5q& zp+HYr1Qg`u1L0uYiz4gzyfI?RbZP?GuAr~%QI94^x!BdzCLtRK9km8QP?sK(@Qw1% z4qVn)&2j;j4L9<@|%| zSA*7@Gn}{8r>Pb;hF#=~nnD#gCBpJQyY?N_dGDh$xg5(9jcDyZZEXn?;2A9AS)!A^ z5d}=wJOCplW{_7$CNFAg<^tC?D+IcIqN)e!4381Au=Eia-z#nZU6+G-HXDEYd=K!$ zKj{50f>NoAjj70ClTNOuzRq7BZ;&jt<&@PIB)H-CAj~7lh~&!t?D`9ltzUUTpPpxziyb(juawWUtTa^FHxNAO@sLPkCIUq!j{V(dG{D zP|1gF%?Ih(Pz)WrMl~xtW3o|mn}B38ttM3_lr^HsEhv38y|6H43&3p0fmx0Cb-}nM zNHTZ!8gdASL{Q$cloV~G7V^F(u;2U!HLZV1=vH+;_07;25kmU)IqC3Jig14P{oTD9 z9jl0toxB5m8|&#goM6P_E$TWNrEefF%v6?_^%d24Cd+Jwe8T;Z*V&F}vB z(J0wu04lnNY7uldPgyA-fof<6gZ6QId*d%%T}Uwl=qHB>?d^dey z*3wPxK4#|3ubrLT7%CCkBZt2R&TB-DT^s3?|B0|FEIZeg1a9mlM0~uQhfYi1nOjnQ zH7El>ARtNq+)osThFy-qIhx%Z8|1Y;;^m-sNEz@B@-^jP7<9{^>Z8)DQz|6gX zX7YgX*-{mH<623pjYraV4EDtSrsOoMnWu!dXhS>8!>#=Ng!g?fh0(&53MBzB z4jjQL_7TQ^Tc#+gY&)+1=wB@OckbNzL=p4XW4{|as8eYFes7HS)K;v8g4dgdkH7m_ z(dePDKfm1+WxP*J#2$M&IAl^r;4ochYbk{d2V#$yz3px|Ugi1XJOF%FWCD~}>;_|J z*#3A4Wacz_-tnin4K7{1oQB?HVA@^-D4#qYnC=qQrS+_LY8eyZS|6%U<8#Esqu%Lj zQY}ROdp(i-pt$%f`sqL8#=K!x_?m(f0BZ{hdbW+@>hH~hf^moyMxNZvAS{Yj%gn%g27PR59u`QQdGAoS4uRCmL zh`yGC6q(}mIpyA9%M*a*4XA~rzu!BF8AKVU!ZWZZiNi`)I`(6wgZjr2l@(PVPR$i{ zj_YJ?UX+Upt`W()(5>!WzNknwirHEP%WQ4cix+9^CAzOu(NVnt-VNk)NKLIDoQdq< zyCn23&XQzVC`BysKJX6xY!?^F%eHB7RyE~FR_WmPMrXP0cF{_9I@g(ry7wcAgcy_w z+1lh|GpeQVL!gV^9yE^}>m?zIeUY2YAzVYiaZeYz;2&1?jX*%P}tAOvX8?>7za0qK&4O)VXik11c7LRYEmm3=oh19;j2$n&&FVm$IP zKFJy0ZgF12+Y|@0RYlL_3G?U+Z0Yf|Whj7*J_Z`v83xP@qx_R-LB>spQ`DKFvYpse%>U7OEJK36B0|P5hh-$RP7sxiWMtLV#=g5B^ed3-fod!7j`%BIL%i$OmnXs` zKm8afdqrVq%Aq?#ICP8e)751P2Crrz6mg<)r>Ol#bJ>Cb*S@~GFmfp`4^JDi7z@pv z9dgXY(d`Eg>SZWu;G1F^ht3nW+N74DvDb!nZ-z2-8f9}iHOcuIp+WCGt6IPuf(j;O zip$BONerE<(<_80bH?XH8Uee*Ca1|ZgN=1G5H*D41Xym9TJ0SkC$4acDJM3+jf^zM zVkF<~VX@5!)+OBdLV6rPjoYGtaM9K;E-GPkgTK&=71I=KL>~_)+~-KjOJ}k7qY7xd zw{HhmSOmp5t|QE$hxyXc$7dsuK%^>&B%)6xJ12)47~0>V#@IS$2gUFW+ZKeQv;)Qm zUPjc>$iPtA0?ZH!amVtTV59dWxBMk}XFHZ3)c8>;j70O%$w{^J){zmI^!LOcx)QNJ zVYpfu(^)Wp@uilJ=z-TSG`GW`m8hOk$`#jyJeAL%J`uz}PEn(ps2H`3 zk93f^0(Fir5GS`1i(sCl6ZhTv=Fzf*7oGyw4ODkwGxyh5HE>IvwZK zF_SEBHN*Nr!E!303*u1Y-7Kq7GK!fv!sbI7$QV$CQDIZ$7>0cv}Q{RiW=uDLz6Ise>7 zN}Y@8Y7o^te(2?hmkw1oAAM1f`I@4w48+&}B&uxdz<-1S+GB5Z;M3({pb0{NCnqtP zd~YMJ*=nrqMkL@cQwaglW`$1J-VsmsJLv61X4F|i$OAHDm9&p>FaYsy+N34JN7Fh7 zzViUGK8{;jy0WJ*C=(lqWlmCj+saofbex~^60Z|U(F=jI<7vUBC zV((8%H!U=;Q}g}tRuC$u^-bdKe`p`P-?D^VXUVv3PNiKux$HqI!(z+ zSLE?#xVgJO!w1BE^#B;xsT^+DhL;eR62_2eFXmKf#_3?y7^V_lg7J7QwA|7UvIitY z0)S4ail4EcLc1L2pK3 zkb26UJ7QPg+muu*aR|Tydg4 zbtYFL5Au@NV6j{cMHEzLZ!arM0=Jz_C3tWE4W57gaxR2|jMZs1o_a0%ebKn{Z*iLW zRdUTG9bzYi``k?3Iu{Vhthvd4l1g)EeHYITE=B<@_CNTWywJ}rnwm#_MuM;pHU>Ac z{iR4=21rd7*B7wek*-jEd+A_eV0sJMXK}ZbUy<2Kui6<&Uw%zknfq0HqRqkQ?qOXN zp4P-=i)BMU|^cOepy-B3K1@*yAZuOYb-Szuj6c{ME*BJSxf`5&OeNa zlI51~0pWE(X07S`={2KHh*~%gij^?>eaQ1xy0<>%U$=6+#6qPC5YNI3yXkxFV>bR! zUaH}e;*4wBRN7?#cg#lU1?&&1Zk?7$(r3j7IpJXS1>>+HC4^`fYP=|B8z>M5(*2Zs z5$}K8HRWG#?S-B)vs}+0APGw|50@J={Lvlp)L@RB{w%xWkpExs@j>v-CRJLJTY3nc@zB6;{Y;UMg(on5p=a`+iuOkhdZAhbb+rPnPwn>bgo)?Ael9=E zZaWe0WovEij;P91;DH+;lcKBYDRHqnAL8dXZwB3oTzDC@VQ!V*nkm|_r{^cF(fAvW z-q@=Mf5R?(8=D`Zq`N5R4JMrIbH2W9hfF&7y4nNSWyK>r-rM1v=GJJsj$nK4a_bC_D9mKc{P= z^WXgZkHGZ)Pzc2pd3kw{V@x!JsUGYacm?}e4VWMCpRbhF>IlJWQqI0AA~9ZTSb58w zw|0P)_{{BKE|RzlrccAapv&e}BwF6-B}7^j(>D1)Xi9_w*65=+lR#I=>oRy}o4q(N zU2Q*EM_qqAmRCAdgNy0!`Sa&Ffbj`5W!XsWfOZknyJwY39aLETz5=3j_gvmk3JuvF z5)AG4U33f*g|se3CR4?=8C&U6XWph2k|ln?+ygrMr~z42j{~8KxcvV&r)h(9LFGI) z@2TU|z~w0NKB!3^nuxvK6dm}9X~v4%e3SX_tU{c(Xt;^lylGQ69u(6a8ur(W5WgbB z3iEFZmeL68k|{0cinNWs3wNLdfT->Wa=#N!+}TbXg%}0TN01mhg9ojHEdTWbStgo= z=G%cP>BQCvhYM3OlG@=$+(e@ZA*&q}u260$!e48v==ojgt4AnmDn&5{1>fNbF;Pba0uM2(QdLorW zP@qZDS6~Dxn_@5yWZ*Pxu0SooS)+@&ScQzm%KZi9@@1gmi9?5CoSb1s;~GyS#^2p| zbv2u{%lJRLSN;a>oP=!R(AYe;Xkk747NBbINZb?#8glg?o5t!48)Cg*GwHpz9bC$G zR3j(Yh3zBuir-*7Zk(q4cMToIeVv`O=s=9p_Q4~xZ=cmSa8Hb>AO10l+rG~)x1y;n zIA9z4UD$&1{tAcex{@L?9>E&^(~=akoA4!kOS?{on5@PL>!BxL+_MzF$W8#vFw%d% zkARAn!24TkF78w9Lc1t&kJx&^RP4MO@fLS0B`b3`z;q9(B>hMzvTz~PKK!Nu8UHf) zUN1c4dyP$RQ6-V=G~f0&pxuwKgr;)=KB~~0`nd{#)?q@`6-m?rYzYo#7Uu0^VqE*a zZ??Fqqr|F-C*lFN@)ujbfPvSAn`g0P0BAg*flnPN>37%bhTwM6*kZI~{HR`7fL-ota^T%Iqh68xMwhEhi=`G}&bA}IBi~mLB`kQ4+FQF^` zG>g;^d9*Y8#BKHBx6Yr*Z@qFWOkSL4$LZnO#gCXBw;+r58A3?WNId?r&T=*G`t>Ae zV_VUsa)EF#ss3dj`{_z`zWBPg-v8EJRa1$bTTR(jA1>D|iqeK7AlWxT_~+FF+B;&% zS6K+a9DDcfFeGDCChbvEvpv(%`H=K_*oV7yPWF<}5B#t%)v6v?^VB~QmuM6TXVxst z&li3Bw{x$YWNm2&=!9U+NR4+pdYq|1>7^{ouYiumCW{CEliVaGxq|AsFy4f+r;*8z!4lo#5?|3d;N>O#(#K{U zRB~yO{?2FRH7(&>Z{3bc`f3`26u(ebgdeLdaXfmGaIUba!sUgzyA<_MpQ zU{hCm<#P|sXIb!V32bN>nR7-dL8~mw*)2(9VR`{Zlk*vLnKm{y9#1ij?uPCU-3~?Y z{KQ}T>{@eDj{l(L?VZFtl`E*vDWopPO9I-i#HfN5`pQG`Y#s|FCJqg4M~EHHxp$B# z{KK*qdL!mrT|wPn+4AK7N7I!+Q@OV7cOz7aOi3llbWkLX=0Z`D%w$MrqB4)$&_qcQ z6&a$8Nk}T0D&a`T6q1l6WS-gYf9>6s?Tr5PGsEGeiPc4y$VG=(yp3XYrNF8e~4L(^r$T)7`K->&e2? z>35coFF5Hw+TuB>P(A_aOAng4dZ4)=JR4A(AWVcB8*~Ej^|?X03Mhbm%#la)b3Z!F zg09EuFTamAptEA|?BXn!l=)=ZzG&ndYfAZmhCcDB8M1V&_G;#U)ms3fJi92&~x>CDcl~3xn zJRT>T)2t_og@4Ye_XawlWj&7e7^4eVDzLW|KbqEb0=C|LbzS7W^PVQ5EyVnn-~w^(OghD-4;!wy zlL%6Lil!~`q!>7hc&?re$AC?Hd9^LjYveE*>1hvd?_^cm;+iWF>PMO9GWSZ3ismGK z%Ovg-FNohWJ__=%CwyrJ%MrxCGRD;E8*Hi{xHPva5vU*)YcU;G)r>aFY3CAwqG>Yz zYVc#4v??B9)W)SMV=aZJSz5X+my8HLqQE*|eC@UO{L#YUrc26@m}))Yh^l_V1GMjC&6F^{vTcSBX1&{{y0(j?e*Fe=yn39Y4O- zvAAnY@|Jr2hW{j;03K@*BFfXj_T+TdrvevOjL+xn3Kv8*qbWo-^jzMEX5DCjDWUq} zROVAg;mhrn=B0i`LzuTcy-G?EPZ)>Z%LV>^V=%bNY#vg;F?E?uBz+5iBP{$ z6%FUDQ;RY1CU6yvp-&<_1^~!|?ShS`@UD%>VHyba&mu4=w-sa{@-En3WItjQ_}EV}AdS zz6a;qh8@4}Zu>lUPgaZGzZlzbsn`$-?^gY|_>lO}!*x-j2I9Zbus@{GD7_z*-i6KR z!;|&~tfvA%F5}T|POTSyT28KbZX`XptX%fAm9)Lx)5*Z{`?DDh%vXHNjRSP1*`ZTOjbPy4ON z1Y>Vq0aND3AWPtlM0Kk#gv%PJ72!g?5$LMvLs~Kl8xP)bF3>cJ0kPQvNLb4=6PVWF z@)=mXF_2ZD!s2LIs#k)_y|6n{e^EpQHt6?=3C{0d;0&&Om=c-d+U_BTM9P__}F)nBhT}m#r)To5$a#~}9+bliSIsyP{quc1gEg0mH3jI8>brhY zG$3|uMgK!p&|j~F-V|ij5a%sxB+x>9;h{tGSA3!nb7(sXo;5m71BqB0Zgs_*lyQZ4 zLVbJnXU1b#(C%9P!J>Ts6{3urfI@gf3UM0iQGBC64m{)c7mvu6)o|vgOoLPR9%gia zl}xc4JC1e=I!!>g{jj9XftuMIU<{nAJ8!VPSL^W|Of&;qx(Se_+XodabK9RD&kT2NChMy9@jK_tuHIAaFgbcpHD_g!r`X3(n(qc)knh~NZ%oPg3 z$OWinWLf_KQ^j`}$bXMJnK1-{IO=uHz@(EsB%1FX@9i+i1Zhb1h85VznR*BT_mq zg17@4izjsV9{MbGJ>m;_Aj!S@&fvn58hKgppWRSa-4;Ci0NuIHk=i_7N{34vEPCV` zrcOX$Da)p}ljgjc;9Q{tb{mV<1)J^+_BfPFRE#S$z?w4c-F%igW$}&sH&bRe0hM^+ z+6q)Ud-!d<1flk=02}j8u>HXX!I4C(-O3|Oq&V`U!;{lKl53b&R+a?kk2laYfT9af z0yGybVNQJ+zB7n~-3r;MJra~S+3MvEZsOqY7!1*nq=RVzGW~Xt zifysd5Ys*TJwlIk%Q^6xDT^{*w2u;Tn37;xXr~P$Hb1&C?U4zeGXQ7D^uBX%VL(j; zO8yl@_r2xI1JXtZx7-TH$altu&yFEh(dW*F+Z>-iD_?LAGT-lbUWM1ubdA2$$_e5r z@vy_is}+`kUW+Rv!n0YIc_)*a;h9Z+jVPL<9Ria{gc z^Q}ix3+$U1Rnf$gl4{Js#>>YM947pB9tI7qOGRz#1QSozU13Jjo^Y7-Nm}wzE|r_= zCXaq=GhRo;`D=iQw*XU@`vIrSg+;V&aTlP9@Q)g4~lWh2&u&UZKsXf(Mr_?{GDN^H~4Iax>C$e!}HMWhG+k*aHRh<}zZC5c>lB1J?~c1mFk z$I1|#M{KF!VFLogq2vZ^@-Km|b^g-ooxSZJkELv{`M2PvU+Jy?F<>F1@ln|2tB_QM zJiw*AOvYNb)TZG)`xVdTRl(95PW*9r`Y#8;4aWH&X0QE_L-y%s*SY&Pv2;U4h%qBk_pVhckIOtA2RBN)#TuMC`*{$qTQh@9@a< zbAf8?LuY6F5lJSD!FF+p0mq(5E|GC&y9R@E^b+J)DZA?XO=5YP}4A-Z07k@9TDe z_Xa%FqXs8+uPXi4*>8@PweqqHtKbv$+G+uR7sZ&`hkk>J-bv8J&EEMU1V3JQfum$5 z4Z^+&1Y%Ub`ye4bL>zF?WUW{f_{~R=WR?N8s^hw^`c=ViHP|w<-@VUR%`ngQuHkIP z+n!WT?v4CUXj>H2Q@$?e&!0VieqFTi z6FU1^Ly+SYs}+Bw(vH)UR3irV)n8Q7_*K@mNfFAys<8^OGbx!}MjSUD5^o&iz3!ZT zC%>KGxR7GE>V$*-9}kL5pYyZlO`~NT#7&3m@6I!ah1?T%G-}9lqW(*$@Dn>Cgb)WU zStpypQ&S@rt2Lx!xxafhALKhT38Li#M~?U!EL1ZhTKqq=zdnALcu-!n&T0S)f;|wp z3jjA~Z}|LK>3KmxAAsw6Pd~fD*ROY~Y))fM*XLfP(6IknFm|KIq7gV1&yb}de*Sw; z=cDjWs&|*0MO=a{4bKmKC{m-aFo#to^mcXBS$pbidWTEddMkm*-hqF9576EWF;r}2 z7cTf^6-x-i@N=Z3XXP(F?k^c~T&37!!eCl(APeB2Fq*M_SAs#wh4VG$6xhKD1i}>N zsEB5Er_0Mxyj-#1nkFbTxaC|saP4Y@(J4|%b)}~G;J4hz$wr#WwbSwxA2@SaEI=x=baYybY8o*{(t0JCFr5#0i*G%)!vBPX6j{NMJ(lSarRklqHDPHj2nn^M^f z%o{ZSn*P}S02vyes*78o zXOiN*v9;p5)|X38CmMTfyZ;)q8uUNetO5~Z5l2dPVAQ6ox^mt-ltg~%{Ym^#0Jq`j z4-u&~PK6|s@Zgm1%Fu9nm(ojEntd{MRwqp29{82IqU+D?AWIZ|Om1}NNF;q{dVWAb zK|xVLVfhzs!U=F3Szum5N75eU=ALhV94N&6!f?isWjjN~_qCZ`Jp=wOdL1Hs4w}?l z!z2GwR>Ik7cJ}4F0+)S!Y@g|Aolsu=YWzK6S$*=OrSx38_C})Yoz-cU+f^qWn|^^8 zV#p3(5<0tS5R$@b#0d2?HDN8(%qC=GDWS{87(&1?1ur+45}vRY_nLDa&QsO5!? zhP*9B>Uf|F0pFG|5#>DL5B)_{7=n+;wj)UJc5~#_Ne5 zr55%M@pg{!c1)^PW-J7Q3c^UXmASqdJ*UUL3YaB=QjD?`-pgQ}CVH#iIL>wxPt;$r zuH&rN8I3_wZ$I*=(TrFGoFZUsVzLP`dIyBW+-x3l5&ifgmg2vfA=$IK*pNj$;>*)5 z9O@T04#7ATYi?XS#3_uFb%<;ku|)_LyVa^lS@QC~svdoL3A>1IS5!_P-63DD-t6(S z_u953d2pS+(iK&%*!LhCVly0Gue7|#~xUnG86sQ!j)BI>YWv$ zwn3kT`aD&<03lzg;-;+Ot-<-`j3zZqy0Yqb170|*{h2u=teJYujap=?j&~grYS^(0 z-9hR=RasftX8J){f(n;n{O&U|qwG(eZ(?JiY`ASTw>R86;UbmfYl_iGU&wQ07)Z+$z9TXee{a&Vi=!5VLGzpeh?yLXh6x!!liNJ&8 zjXqxphpT5Dbh#g#*=7H2t<=Uft5zye(MsW*Y4pg#aiX5Sd2Je*PJnw%eC~6<-0rZ7)@k)^&GQ%O z;#zuucGjebYk~6U@#M5B7FAjLoPgebK@!$dQ&Ynuceja^f8ca(pvIDBYb>Fk)0+Jd z9%%J;s#lF1lha(^1h%56B;2ExS{&V%0JA1gn`as>dR@OQqMu^`_zD{cyPhYh;w7RP z__-O`K7RMuB5(gSSmE1Z%n^?e3b=azNua9cT0$>N-^ZM2)GiKlmT-0HJ9#NdGIkBS zXP`KLq4N57BI<7TMO8J5^#sV>(UO5y!T-~q6Po!Do4aU?8 z18P7tQXz{OHOjMO^O)=gqGfdp^2Ba@Ji|ZKEXOo@&DJE_Fz2Fwcr)$?>TlE?;;Cuw zzH^sncW82=vr2nr-u8^5D4t&RlBwFc-F|ZupYVCLbB!-+hdkn80ir%-&pgw>SKqMf zto`-<5Bd#Gmj|gInH-K+cB|?-+lumFpBCY`d;Icox6Li)o{g~w%R_NA{+Zm@d)5q#nYRx`M7fSerlC< z_}ujLCk2G8?{|#<{^76Z<%4^VwdRwpM+?#~)?>xP*k~pZdo#X+72l4x#R=WJID|C? zF11!NC$a`}=Ko<(S*Lws!A`wFPaWrdfX>m_gw(0heCq!A_?PLH-pe9QSFHvvTwY>OXrcaHNbxl04@`1vi zth5vyuyvJuZlQnfT-)k$*Ed^FQBn+81%JMH%K2xf+`kbn#6yJECSk-^4)#*zSiW=Y zvDlqsOzy8Adb_(f>*lrB!x;G3HqoIWv0Mhz3mP@$Uhxj47DjL;fb0XT@Sh%sVz3)* zLSf^%)xXa`meXjp@;~y~KYqk+X>A=WnJlI*6_e7z>xuPpEyjs^hrIS{pKj3Sk5p9} zc3^e(pE(uNs}@e*!bZ|B#dRt~q(RaMg+>3()(h)Y0`1+2eZ;4%xsQVz2yv4*M@*Hv z+(U+wd20#$RAp|aW~LL35)6>9=d8XLc9N!ioTIUGW0uElw<-T_kLFPU4q}#*v8GW) zR;(geqvQ6f;K=?1!xa_5?p9V6t1GfNjbVM}1rfNtQnEMhrQ@&9yhH)e%Qfs5k4QC+ zi$$6oQG*AA?$7hu`#Vz$0h&a35vd2nb?wv~4}DY#1Xd{Nca#Xahc z;?>sEJ5syM_odDoJ=ZhPs~VLg{x}8PZsOCjF+wA_L4U~WIU%#@eymYX)3-qm!hxu- z-@~oml=~A>y8ns^qjjj@w@X2-)CLIn`e|Bkes7|GL#u)twu=9e5Ersm;q{;6n;=d= zDTu(4bO_K@&Izs_+TAB-X{1XP3Dhtc`713HsLoSAHugkm$Ki*Q{2}UMC*F)^%(>?k zly|LotIq^}R>`w&I!}G@xGc>xfY#moLXZ`poqn$2xfdToZeQ~&eWq-Jb0)|T95?SA zlqW*6*s`X$z8Rv|a&@-Gm#YbU2*;(Hd?SAkb`B4>?iI`}sy_CyRNh$ac)K0WwLnJUU`q zx5siuSNpG}rO)Ju;-tGnEv1`bUbBc;I;dGOT2tn;?XBS4VK<)A>b&WVY{aC1ig~%< z-}B@ckDdoP{8IX6s(7I16&&}GJ#Xi#)SDMO$_j;}%GL?Y3wf7+&%QwIC6^e;{JGnO zbN|k}JKQB=3aGiPji%Nj=j^2mGigb89oM`n`e|voyFhtGC%{D|tuL*Wl=Av_M5&%+ z@Y3>ho}Z53=$^et&K)8@4NjN)BuwQkSr(VrZK6Pb|CrP_oa4nq0n$?^WJ8TfUWS zcFimi3uq9q%5V<1C>-1t`6yUaPLyiaN)_E^!Nn>*B5+B}af|liilhTuRb5(>(V36m zOODnaf5Z}&*G_hJu#47LGgLfX>`C_(^I5maWu`oH^}Inyu3pOja{*e*MOw+9X^HD} zD{~74JxFTcMy1uim`6UXJRj+$-uRUq`PFfiwcN&#FP!7G9c_!{nG^mdQ$5$;ke*8s z+RT`Z@Y>sqcA8H5CrbTBhe7G~e`uhti#>ZL8JXH6lB~ICypCBW%UfZg_0r~Lw?->s z^wX8L4ynECXkv3z&6O58zLcQNx+=wa8_5S|Bi@~k6$MY+w-O4nr<4n zBmxde8ycU(GoH+mGvp!aB!b5GGrIdc!yI0d zIN%fcfM|8OP2+4$MGWu$@xOES{-L^p59Ae~=(*3HJwwR)mqq1VUzW*No?O@eQoH|( zFR%y}O6AC~?QjfN-p=l~3jOp}w*CX_bA^&p#nBq2i)a7C@cX40v*6>X+w%UBG#0uI zQJI=hFD&W)!rm6{eQi!fP7D>|5$`ZVgr^>3OwnH!YnI#jMNV5rCDOJdqT96r^i_IB zK-Ga;b(!A)Bgl$^r)teF@XwS!GP}t^3K7a`SgOA1cJ2B2nfI0cT3YiOhMBOR{oFb4-7q_Q^4^9UA5 z6{d<$rivftRtM+CH212$#1#j*2-93q+*Q54W!P?VLAo$iX@&f0(csIA&RQa@!h9_= zD&tR9dbAe9Nya}JfsT%jGMwSlK#$PB7u`s8kV~i+q``Phn=F(<3>+N2pIKnFV5}MK zqszS$lCqWZqM|FYu{_sP#q*`icJs0MW;8$G+qyLow?z)ompxW&%Je8ZXu~(%rs}5? z)XfLU0x6bvz2jii55r>UyJ@anjO_!+yFt}~tssV4=CDV;G_#dF@ihm@`a+%MZIK!q z)?eGM8IAto%HYDouF>z1$CnIwm84~p#Vy`~k=;3KJjr1%u@jV(ngXdkY&)Dak4y=* zOA;Q<=gDn0k;d&342^O->k6JW%H{TK@{b9M(OUdwx{NF}dDvZDMHhP%H({&O-W!!x zDXW?BQJ|jEUOp-l>~$kTs_wRMZAuk^CJ&P|NYwL4_~D69civJRi%tna63)IhHo_;G zJefy(X*~0(nd?60^ZU%W7vL+b$ez~Q-N-Hzo433{{$Zhz`Ac6HTyA{n z*Gv5V59{Fu_(V5haTTKVqz`mg=4WhcM!I7V7iqjt!xYhSE=1bN6v=yYN zZf5(f^~Cz0Sgp9jkA9Hj@7dXJq#EN6tyK}^*o3F{(lj@R5F)IdWNKdArYLd*(86@o zwk2GhUPX8q=Cme(?7X$->(}48txmhrnre_jfRu5$6*n2aiUOCIajFFw#C2d_|r;U$?;^EX|#91=EhQRI1 zm#&!8>HewU>9G$=-+Dv_IzL1i99d)}LWwu;d&fV#d-u*b-*}oLb8yT01BoXA3qlKF z9!iyDcBrD7Dm#o(Ln->pI#9)&1A@(aL%m^ndASi!Mo%Mc_wa;I+qJ7#?HEHULjB0R zvtrvHi-nbKp%rG$H)Jd-!JUm6l%XJq#=Y8{_Lj6dJ)uWr!wz^}M{@Q>UwPL8t$EzC z$aFv=c8!q~?y`DZ4jWY6QWaP}2ty@b~o8=}HE2&pjZjoOD|N3kT<_NaX z<@N`ra|=y21iwE$4{U*=J{W(7Yr5FZr~Ii?PT+RT>o|HKiO|&z;3?m_hjdcz>Wg&0 ziShh%)w|rc6th_w@G!oV%T5tsRZ1qqe>bmADTakAKJ*GG+MA#1OCW4d9l-?Vw7~<< zHcc0)(oQZWi@yghTV>sZ`h6Jqh1{M!Q<+uPnsYG3Re)y1Ha`7ctJU*o5fov*-)iH9 zD{gJqmAVN($hj);lbcWzY7%1&eLw1@M<>*ExClDA<-DV2++;m$N*YP@zCVKIr?p4Y<%Fwt5*jwEi=+xJ|=vi5M2jv`k2}l;T7Q_cd?0W zl?4Cc>z7`Cf+;2}_a@#h{Vh2vtSJHXJ`WU?$#bwn$g>U>=WnHKTDPfXf*hSlR;{A0 zHy9scXpCtXq$8zKZdmvpi?(qu>DEVj3>P_}KzjBoBXY+W+Pi}_GSIqN`w^8pim zOJifBa4?TtbY+4BO>*?;a`oRGgU(yWjM!Hyz&M+L0*eKnu{}c`vhWDMa3~Q_R{fC% z`U~mEm?IvcUA=+fD8sIcQoFx!ssy!4TA!CE(tEgy+?ddNt!ZxkuJMa+@hP ztZFV}WIE9Ene1#BUDhcptL|@#pm?A`0T{c7zFh+M*czs2MmXD|6sgR6mFXYJLmR%y zkC(I@FK4g#fu}wMp81hf@i$mPwKg|1qL}5hb_%bURvz^?p~jPDVlra`)KS}a1yToY zg*7?2ELw7_1Cr>YV)w|x%gf6E97VZ*#t$&emW2<4Q`%XysBapq=tR5j1X<1fJX`Bj z)Mi?%One>nS_vOQ%O-Ql&`cc}6=(G9bv3_cF}&)zuB^H2N#O&?*AH5ZpD=O)Hb-id zfocWzSIPC{xN+y4gGJt?#T-?V=3GVOwlYGcXC;GNov$u0MYlg~b;61pNb4_siM_K+ zJ!DXemCR#M`|Lot_CpNbDjxD^lQ>uVmEh8eWS&S)QZP8kB<>n`yncSTfX~F)!om0n zaP@fHA+)aEa>KuKj`KQoE3=a;7V{0mGy7sxuN)4+m`#5YKcMxbvpjR5;2hae+@+W{ zsT@kq8VFr?3gqw}`r#aIsT|yk|FD_<4AR9TBso}N@mAql{GM)mwUB;p_TSIZM>Li2 zw$AvKF4*TD4vw3Skt%@hv|*}VKqzT2VNKf0dzasOJeCDq12;Y-C!Te8=L!|1J1vMs zZBwQR1Se$6ayN^I&h6QNej%I2&cH{=c)&a4D4xoC(1^rn#T_oi*Og3VHT=B6Yu`o2 z=m4RNIBdE-cs}_B8wbhN!-e0)5$k`hG@oHY1R`|(#O=iU_vdX~Tt*m)Ud78POWmaK zcu7;{EZ=Qg-+Al;?BO@`5xrf<*c>EzBb+P88rzxuFO@0Rk-3LC)OG8v@)SvpwERtO zU;^zNv>c`iF@rmiP9p0M&HHJX;FcpnW^Q-zK?By#V2ceYUG_Ipg}Hc7_4oNaIel?qQb1xle5$2^75UC1}`~IYS@I&^4C~NqmzSE3$UZqLCQ0f^%d6na`}9< zlTH)LfOGj$fC1rNA09f2Kt#`AP9du7`3ct!L~SI;hxb=PGLPd-B6v zktqm=0TQ(4K5FimrsqLl=@(^U_NLtG(X1N#5=7=L(TRk!zzseO;bq|wq|tNv4IW17 zDJ3Gr3xVbJL|B28&Y7}+g$ZPYtxg)u-MV$_xL+x9-qA&AKZ!bEo>KjwR8 zeSjp$x&ni+%2QxBK_U=eXe=cXk&2yqghInwm2(3SuFB%iVEeJV9X**K>o0T?=s0*a zqi>4FgoF5w`+#2K3U+Yv)vQ&mQ32Qty#<6GA3ws${F0-C!|%n3{_37pGB-2~Z?jUo zSJYqKh{!wQKF&_p9eu;g1wd;r#)sNjCw9eyzx@}0&EwsCbLdsRi|m8ReF;0=@{P6V zaYJf=VW+A2#&Y;b2noo?MhEi|?EhUL5SOZxb=#MaKtJ5*D;yXR&Rx%$|39WqWaK*5 z(A(R#Ze96_46uy`LYbvM#*9d=-aM`fD5;b#VZ&36bI5|=$Vc!_(OeGylE?E+?uie) z6-$<-HKRYsoY0=3)>`NPY}GN*<%SPf%fVYQE!}nc`Nl8s!#rMOy0?AUv(S%ro-S4w zkAPS>WoEhfGyis)SoQ_*WM^lAKkid$_V@!YhkJ`lOUkFORHz%UO8o91x?LG>)v$3Z z^YiTHiWb&hZdP7tWRd%;$iMnU((c5Yy5;m3so2;@yT zuvR5z8b5wuPIwp1`Cud<&aJ*0_G0Upk90W5LCWO(*{KNAVeUT>LMBpi{QS8FmjFwF z&Heh!u7u&4H%~0`60%#fb4_zi@o)IS$e1o4t;u{jbNd3%uJA7LGs)th*)0=8q=ebv>#J8-V32qRHsa1-qeh*3|-sJuHalGdobMofi zQ6o~XgYTL+0Tw02eBBA^p5+ezE; zF4*uUC)1+`tcDZG?=F6ug;~DRZ@SL9idyIJ;wtxZf8(Eu(!}kj$>xglfy8>)&Lz=f zw;dDe?bb7g)qlF$HgS16N2D0{2)1lkS-@A;smQb$s;KWB9h+8PIlNoWQ10&Y!uN&Q zK}>S9O8>@>g;OfB9^VakN{qmU0^s@uclW|Q;AdeedM(}Y`>5A@5CmL{d!iGCRRKBg zWek+!Jpu+9WJl)m7yCYJc1lS;nkJ_}P0kE4wc1R`uv*dGE_FVC`t%TMuK+|P@BE-6 zxtO`qUa|}UOO;cRC>H5&OG@N9S($1EO!OJ0Oj?bWxXBpBe4F~mJi$jenCyi)hrWUS zi*5>ns7vl!>~Z$dqiEa4&nGi6Vx@n$0;d#CCRqGUqCfc0)N3ozoGtV=D=2x+&+NM4 zeBqjoe8U%zdy;XdAeM3M@Bx%AwVffeDQ^ zl*7#=aT?O>Kq_qh71uQ$KkT;l%jhi8k-_kDw@DoP%9JuW#SWfmPe;e<3pzry>dXc|L6(`sRYeJT^RRHgI59#`hKV5sOx- z%mZd!cBHhGcz-`v}CC(FcZ^B3MkIW8D=E*IOsDyey+D}vS^Vu%uQUM zs&`9%p5%|Cx0y*+J(eQWUwv)dWfd7A6ng>VL@8_>xok z@M7&FF&f079iTt9LL#>bTUtMt*~#*@x#V9|=92c$IPuzKOkMC(yBOisC>8%B(LhRxXvQWwrxCZ2Tm_V(ryM~l4N-LtaQ&fb1TECa1k3lwk20J0p_ zVuL^xxwGLo{SC<(;!g4Dj)&Pe=k60K*8EwEn{lyM)0wmOsG*iHi`KRaz=$h)$Mt;P z@r>rL-@j`>0tw5SuMjNPgG#k(#`lh=i^&Kp&Gd`Tb5HsV4s@C`^fFsCPfHnc5e~xf z5R1H4I@p4R7Bc(7x<;>J>@~<&$!54wB?S5g$_?lnIrK3B)~83@2toJImlwuFLs@Bs zIrDBD6}@miRZvn?3*W?p+){6l+*r_tiv^rGCt8?)GNW8bK3nUi3WO zkQ^w&$DWx-Js%V6k>kuYbtkTaFLFdd;3z)O!;^zwV83r^XJWV{^m8m%zXkwC%HR>7+mFVBF`GXwe=!%`Yv@ zJ;fahrIVTOj}R86-3j$|{3KUX#?uWeYTpi>7`;IUCib)j7d%p>ySC>;q{jA_Qg)d?y%WFQ7zJbu{9**RyiFnMLf_f~&;e$1v5FRent{I)s7 zC|_RQS`H!OexRX@7J2vBuN=lD3+bSC@0o-H+xNtxk*&8YJb&xv%@gp~_-tiA`Zvyx ze;|C_AL+ozDK?_ZUb%;cXAFh!$4fP%!G{&qdyG(h-UW zhY&*zOb9c8UVTC0bsp4E*?qR5CCJO5f|yiL1RI+yXU zrL0d;8Aqq=#e$*~MwK!2_aC6lf|$N}XvS~Tr0nfpk;|-cS|aenMJdAfWO!@K!O8DH zSE-DzA~Y6^{s^pQ_&}a_c;UYVW*ULNAQg`C^zh(2_5<}x>mDhh*zLB{G%3;JUOAJ(9;5@N|CnXDY z4PbF_B7HTZowmsy+mm>5Ej%>`9$gc7W0P;pn_p-l{b-`xu(!%+gv!8N(_&g4-}IX< zuB(MF^%1nCNB#I}8;;H@$ISUVJFi9il-WK!MBh6XsR)j*4<6>{NGWFeR-WE{Q*}`o zSk|HGcWb2;jPQJ6xmBYQgMo6xMF~G~2>1nyzWZrF#T+}~oiJu^e{E2u! zgPEY4+4IS)WNs>H6haogdkVyCUo0Cl;jwYem0qCEihNPkOSM}hWvg$pQaU3lFkIn5 zY9e*;V@ILAG%>F>L-MW<+BmL^yMBo4 z2>{<|*QUsi>aBCT!BVWsVu+f#iJ$*~&+m1W8uX-$lmM-{;#lpNcyP=ZilsN|im0Eze}ai+ z(jWzjTDkCx3If$_ZE?`4rqb2*u-Ml=YkwimD{pc17-_WNewKZAMS6DVcDdQMs52N; z(VXu)wivHzSrW$zpE(y-S8GSd)o`Mm$jV4sT&LZ9uTgxhQjMyL*<|-E4e}2oEE?Lp zp@g=wwA{wS%4Aj85g%g7aoIeKE2j1_3>xF$oz&}%xB~51KDSKO^6^cn4eg`UYknJ; zDAT-au1wIS{rS)taj`PpR}pL+aTW>z5&nX-Iib&qJ&AKjM5hfmX@S#yp7Z@0In{7i~f%UQZui zsvtXOVjoLna#bMXQ(+phh{=#3SKhj{K7v)%^idAJCUk}1#d3xp10uOC8OSO_uw*v> z+rTJW29G6chT!9hM|NOteG50S21uqtkkoVg--B?A>mg}#^y>Ra`Czw4SH>&|njEmd zZGfvof=DGO9$Tub%I8YQ%x?=1L0{u8QLFg7@a(bF4`Cm9W6t5V)3Ge|o5{y#Vf<%7 z)F3(c$rs3o&YJA=#N8H!+OjLhyN}HN z4B-y$R+6=s)`mWp+<-6LtfRa#J%;#2y3hMo&1eR+ z-1T0Tvi&#okD}X;@pX(<5tJcoc0|3cFOcd;QOhnZ(o~+MK)Qqa>}$|gYYM)d2`_l_CIU`T@3N{9NGjG$X_QBCPuGjdYiR(2Y&&3$FpD4oS`Q zPNZISZ8D>kx#Wcq1^Nh#80JiaO#Pnq2V`B#rCOkh>yP6{+}X1jhVFx zRXGvA@xfO-BT{W5E4Dk)T)Y3Wr#)|uabmLKVX9nJk#I8O2xh#*kt3p08t~Ei?$aOn z%TEa0nDf*QE**uC%F0SRdE@^`msjfsI{%D~CEJ_qd-l5j1!E4o>4?}nAEM%{Nn8VM zB8s|P3j^kGq>aDK$Y22%7cLe=yp1@2q;*h`#Rm0@Q!q=V!lUxTTI^Z0k8;u3+I3V{P8MLpRwP;G%i@m$VB=QY!n~t_Na5Pyq$+*VD-TR2d>Y~dLf+?yKPkt z?7v|NmH+D1bvUj6i#^Cu9fgSxZa~v5bh%RXzCO62YN}YIxZ=H*T+P3HPjtD#wK#A6 zx^+8Y-myG6Ss>4Q!vuWP7sDT6aC3zO8109uj9bm`54XDJSH&0Ok{kAyt)T?pR^G@j z(vtz-;<7oI3UFU+Cf zJkaM>C}9Q@PuiPvF`?S3Ev}RjS1AvDac9vkAspR>N>xTkC`(@Z+O#HC0vEe>uN%$; zoHeHCz;+%Mx;ccB+Bp@W+N<1SoI|7n?Vf?Mf&O?L({sms<5W12)L{%)mY45JIw7h} zc!d)R4KQx0uq%X;=uqC7*RoCDzn^@7$kY;+jxE9)julG-3#(LPCU~!&eMl%=Ib2y@ zQ1C^ai6{tUBCwDY`HvSC=n#?m1FDT(;PEC*Mb?}eh8>*)Zu7^EetMcm*NYYXBgBd-bg{q`;FSmxz# zy>NC!m?pvBS8Dl}n#-qBW3^`!Hyg7N)J2Avly>4zfvupywITQi6@lhgd-9=S1xH5W zYj?M~?pWaIRhKx_2z17+a}W0S1Wbv+xjF+oNqi{_=-gqNFCGyJ;TN!%r%#_Ezesb^ zN1>2Th|UAY)tP^}IDJFlAMcQ9GHX8S2MGb04qJ0<@6 zu3;FWh@g}nLw1_1m(EA-xdlpN1f^5udDX>lzYtCO!A5p9e2G}WCO?wHs$#(uR#^dW z*IC4mP+=ryGG#qFSEq8Era?S>4b$@|P!|}%gv62EkFf*te8`~x3xTbM^Adarf5EZf zzgpsU;FiA;PtXfV+4Z5ECwDRAtFw0QLY9)wkK?uEg@rhNv`eUtE}oEkb{+}I`Uko$Qpqp<=iZ12<`=BoWfr{W;c@U z5Q|V!HlAX@Ff=>BL|EA?KG_r;lPbQSn1eIxso{^CZd8Sm%ME19|GMF&rl=q${9k^h z-yb?9Ydw*Hf85u$wwGGE`dj2n>kYO%{J@T-PHZ_lmZ5H?=vb7Ixf>S}5@Hsu43|hW zR>l?0<4?zg=Jw36>U}8%3Jym$aL%j)0+{7QCO=~}$ zb=MXFZQ2}6@vJJjnpV2p+Hm(v0$&CQZy6!|2kH!%#sFGtStRXq65{|%R2HM}4k z`vOi+-l-b^z=h~0->Olso&YS;$h@vmTAJVZdnXR5A69WHMQbRl3|PNj|pZuBJ z+@001M2EaAo{30hM$Lc?2;|Z`pCfpAWOP&{mOVCbS;4fn=K+avgr>PKVe8h-o70e% z^uGuwklaC$M-bHV@>_bg&nPqzWV>r|d5A{V#v$si?jtlE#$zbZ>QDjqVRrtDo`G4# zF^rffueC|Vjd2JXbLd?*kTEK+woyX%&2QsXpZIoMx^&uzTirAA=1oCtRGo@KQ^moa z#ekW*xP}g6(?ed%^{dEijrr}i7U=d&x2L2?yz9b(jNYDv!|?;%oBIn32L27VlO8-6 zVzioDS~1;lY$N6GHi}9~+`njLB2wEOIepw2i55m1;U4Nfc$=i#OPqoV_$xocHz z!F)pbLV|giYprP)-fV5aXP&AYh-?<#`*wpbKb8)kW{qJH7gOj~el1vT6N~3lPSWeK(|OE%2X&p}3bppizzi z7Dwpd&Utzk1&w^oFQ(4~&S_2a>-lrbm`H=alx3r;lS1Mh9UZUR;LL3SAyUt`Z!f+B zRNhhz)97`;>eH(G7ygY_@zx?5qsiT<1`{7QDp0-E)pud<9@ohL`Q3D(O<8U9OTAyLtVNR*_ULC6GJNVG_P54Y8NktlV4&?gC zOUWj3SKsnnyr7k&6h(TkVgQ!Aw7A%Yas?NwR-i8?oP_DJ4w#M1(2&VvKAd{xS=Adf zS-v`QyZBKg(#F8bXgnxtvas~RqRg&U2VIQ zNB|-sdwhPL1CX*}TagjE3wRs4#o%){*ot^v$4f@v9 zv>W9`hZfy`K!}!SozX(3Y#?>G7+$!iMnhhrgqX&ya7GGOdzYTe^y|R0aMBq@C2$Ja zG&(etHK|}(d2;he5GUouPtxbnDW65S`cQ|UmPagk$@sRcMPT~q$-C(J-&BmgG~x#* z{~m#t`GpI87VoGAk*s&O@bgFTU~7uv$)YkQ!mx>r8I9l@_Tc)lxtK?D-g2uw#F&E3 zYDhQgQdcrGBu8YafIbMIj`5#2GHs{Ty9G@t%FTKM!*$JA-Kg3yl2 zUPCL)y|BDNepKLr|2Hyqamtng{)v47Hf-{WirtB|HlIv;oO3Af_A|p? zO@NQDCQCK=y2?^5IZE8(UUS85neGr8-Z1FCo{cR9oE7jz3#7o=I2RbW6*Esn-N2iL zT(`jCdx)RbmecRKzIf*Es@^d#m6T@`%BJ$3@}aq>rDlOzgix%!R;cP@E@WES64-Q|(l|Z3mqj^$S8$*k0EjZI*jdO8O>U z>u`Z-;j&nAfGj5)Gn~=0QX`HmtrCm*sUsiUpQZn&1C|lXd5{f^j*VSwA70fk#i5>W zGK1qcpdxr-WpmH`M`?uJbV<-|SZDP0TU1f^&Ya1h*nm0RhomGWyvXqVcwHJDtk9s> zd*Kz*pI+m_sib!P`@2Mf;23C#)aW47^4?DVYnd9>f>C zuan7g+T}MiT92Ly6zEg@OFg~ri*TqygFV=CocYF2ck)O29k`Wds!kx86(nhkGiOE+ zbn|a!L-s57V}jesAmUofC(_c(!{aL`1W+SZOew&|9N9{ExA7sX=$_*AK-fXb+bE3k z>5B@F{lAjAttzVsbV||io9@70$|lOW`dZuE2M}`Zn3|iP?}08Jc;Mr`L`sm@E=u=o zuCBu;LU8aCLxrmDT`@$l!)6XwHAV-y{S;g_bPv--K^1^HI+d2X7ii1AKqXQw^2Ry| z7$3%Jo;1R@q$|x&GAv2cz z>|J&wS~)pIcT!v8%-G(^_6Q1h?BMN}!DrCweskF;32!5F(9|(laHy*5zJmEu6v(I% zwKfuG=Tt^YBFRq57I2w+dx@H)&`Ja#K6G7B4{{^oq(L( zp6ksVZ!=#r_iC(2=oCfeqieAkDmVQkx%>NfX#_kwzZ#o`7Wm3OKPCPOKoYOKq)fS- zC9QygxRz*O8C5pZW~ThiFlPl#cm1clxMes2)#CmeqX$&COU##te+<6)uj1e3wvCU9 zq=cJspPrX}a37>v?`Ape&5<9?Lb38G+_h_$7aU#E^@~Paqg8U}_pJGmkEQiMhC%}i zY>JyeuF|KmF2Ej~gU<4LqcOAYTt>Uun}+4QT;>|wgRZMXt;!H`dcFTuI_rn206ivL zikEWp@`^E>-VvyrG#3t0Tb^Lx+lY<{`Dj6XGk%F^e(HbdXweLfj1Y_I6s0)(^$PTu z_lSVYAD>>E4!kfiwu&LXG(=8&5FetrdFTcG<+hjMbKMZ)lXO&=a{@ATSaNE0*B)0X z4%|s_YzStt;$*6|H zo;Gt3rXHIdv(TZv;8cxfl^|+foq2}A$NS=?O9kcdmrZP+XruU;5DupaCy_-El@B0+B;Bgya2e(d_N37jcS?dy^F zo2I{io!YHg;-i^)e-&2mgE^tqbfPL_=CJ(@;$&6=NIxFPa)ulZ*o;ZZ6Y)}CQ zVgTE4T;m<+wvSjaoaTHtMc+t21Pa~Q)ph(>%FC@khUJ84Y>YnBtwxqBS*haPou8Cz zmobq*YFXq3SH`tKJwSAOZ&|LhnoXBcbKilnxurc)b^HjJYN{`A(PI*#?dOlgI%p1= zh=&=byu+ECs*-n~AE>UVAQS|2VwM5a!}1>t!@~Jbdm5Ji9h@p%^Fx+D$9!_`pAZAk z_E~NZkBppvX4Dzb(ZwxCiy}~m&z(K{h%p$f23!4!=a;sHWGv|tDSNFG9-QTqC*YBB zi#|-2beB2vq`dE~K*Wyov1kjvRBWniX;^FUL#Tv@0m1!2;)1oc(8z?3p?E~6%8u<` z9Q%ky2YumHl!tUYun^Bx0d3$rlmO9%_7{N?*u^Z;hYFSobvfo0kqcB2>g+p0c*E@h zUnSru_mUqz+yW3q^wJ>w6m+@iQ*8+5T?lm4BU#5i%fw0~sOq{3bM$6eKVfbVLlFS=KP?ab7qatzG+hZel>PdCXRKKw z*^(lugt8=rvP7kz}j;M`ou zTP?B7PK^o_KL8pRBthhC&0;*180TXJ>|oPo?v{1USQa?QEl*NFu>sTUv^SpK-h3k= zmE(U(n>kL1Gf{Y_!gO_FYy9SPAD<&x=@!uL`9KrM>oZuu@P0hF^EpOjh|s8P?%#Hh zMILv2r+H^ukNDQqh;cAu-DP16^wyrL0#zNzJ@o1EZ}zvMD*Z|W_rW-;W$4l6o9p;r zhySA;TAul?t1CUp`%@UVhS8gT_ID&b z$!fV~-)Gm;-_LZuzvCXcvzyF{`7R`1Ck2f_AZ{9@C$^aBR{Cgxnxu>Q)+F%X_~QwD zVb78>Z_KDiQ;a47&asR?_lmiUrEI;8 zQKFkc#SYsmeYj1ZF=_@)NH*#gGyx|u`yFsBOmd()ROO$46)|U`PRs+Z?dNZViV9%l zh3D8MKMm~i0owJXq@@2W`wWv<^AILXemd3q?aA9eTZb!GO-Wt9V6OB)+z`=+&!xT+ zN9L{Ede!+(Vs7K#lA82^J10U74E{Vg@apg5&iPtB*DqZD{a}UBynoE^lb1SwK0c^k zen&Y$Y1{6X;qIpKyj%VpTwNbdYh=|=tzT|pr&P^1=R?IjK??`TWj{Kns)o9P-maIa zebzi`&F@QO*Oz96xwyFeLrULUZkUpxq{Z)*F_CZfpMlr>m%kupAS0zSr(0?#atWBz%YzVBASkVU0^hEYD*!L);BA*G~nz??p8Cnx%w1V z?K4RWEj({PEhqt0G&CU5_eN|Ut=>l`3S_%1Mbc%$YR~Z@{@C)w77_YYWlP_}HaWDH zZ~b+J)BBeQQZaiwcu);Kf!Q0@ul=Bs=v;Pqk-7a@m6&g#Zl8GZUFOThMiqrFv!-cv zyvpH}@%U`Ly}be{jHjOD=I*x{%dlF0;d|MKj~_$Ik%LTb?D`#Uckg&t?;h_8xaE1_ zXJf1;%s!2*M0g%6knOk{SCzYk-Zb?qTR;FsdBMFa{~+(4a}YcTZ*ZjHyqevTnUli{ zqB?9DPxm8?iu{A58*{!JS26|%?-ioTnBy^q%gOk?kB;}tu>w0Lb9!W-vcw#cn+cSh z`@5JJR|*)vcGfkoshEu975V(ocYbl}?-EQiDxg<}X%ftAVUw+@stN^I)Ogt9HjIu2euvwVwFr9Nw)v)Ku!qpM7vdmm^3dcRcF>(PxNV|Lt|k^@ zG=7@vN7wrCV|{$eRk|?>G+BB1rhf?izLJ=j*yA@w#%@u(E^JFZ4@~K7o8RrBujQ=> zXYz^UogX9~ll2S1X+^L^&H&3#rQdiqHDeWpW<=<6J<__tKWKaZ-VEylf&-Pq#hA3i zyy!9m%s68R8uQ}yW^XvAkpWFKA(OA)k0|@3>TU=b_K0;W&g)Y%HMjS|s@PEqz!087 zFZe8y?y%j$pVyyDfqWv(Ir-7nLNujGL`1Ykz+@YL#Vi0&FyEcjdiRMOze|W zQ1E-G%kJ5BeF@n-71cIm9w5KTZJ%nAHWtBJIZ=!x7a{t>-O*7umZg9E$J!~!mm>`M z>SVAI1L9x;4?)GP`j2|eV=<#Jhlq*;>m$P@dHpIqa!p`(wl3LiMT_$lUu7%g>j?wG$Y9s`!YpH}tdMf8Qn*|2vqxbK(52mg?2xuLyyNM0TM`vXa zczn_mcGDTsI^@wu)8wm-CMpDFyKY)+`KhVHi1)rF?42HXYjbx+aPiiNIc9GEl({LcF9;Xm1L7Q#r`3~n6^ROhLgShh{*IB z*u_P_hln&yW_Ks0kvX3;BYKvZ^u0)2ouZaxwR{^Y?PrksUmsiEjj3d7Eb@C%M<=qb zjO~2p;bbem)I^S+?Cr9MxTq4d%&0j9`B07tJLz8YO)M?vM2ud^$hWl-T^plZMY4yG zve|r{MC4iiojY=qC!ev2HHMqYYLXkO~rOqEWg9Rfxp(2V2rT09M3&z>VO+7BkYak4i# z)!Sz*`bal)p17i@qP?SI@(uVSFI>1Vs9A3Ya||Rj0Q@_&bLY+-d=uSSuT-UZ2=Uf9 zRpJXm&I{dthE~C;-(D*m>}+hoSMb!x!RILjOlfQp(Z`5qI`!0=&@X-E-H%O(fFPf_ z^XrewJ0~LytOX3sbqD?YmZ!`}sQ(z&GwYf-iD{@iDmianrnie@LiohglrU^bjqOo! zE=YgNDttp;?9bTp0y(E@kY3%u(912m*1OYhM^N($@QDowZRCoBJcr5Gx5^c)`F1Ukdfu+jFDF+S=P+ zXo~zjW)L38X)v(Zx%bgYf*55#hz}=xQXiwf>;eBY_09e@JEsr-nmBpGn6~))5$Eue ze)$iY_G-!wbIBHwicSB$|BtXJU6kFT|N7`x-cfD0tlp+uPecgcUcH1Tlw(*yixHvm zrSt1oD;#JmbDQz5eqkVEleh3I0%~Z!uirg5xl-yn`gY+?6q@wSzmTs=dW3F`XIVlb@z4-HH?9X#|X!xn(a5+s1EgjE(^D zfM)-WRoF!SFriD*t?+(5y}WXkKh`4L{+|mVEArWT`Fx?+?+c=y;~LqD{}E97X}{O- z5&MOYep!UEokte&cko+W6TJ>bF2!+h$wYsOz#IKj_P&qw|kElE^@yDG2XF{F6SppLJLR%{w42p>R6^qG2hznYHxDFZIDp zcyMqTlS?U1!8~Xg?EHcBVaf%9#{=g}fTWtf!1gdW11!B&dV=4buF*YQD-eHmm(7Q*qkup_Xr!jTjozr$C1+UFTPN>BzNlvFCck_vuGfUOZ;D z>WSk{Qjnr{ZZ`~)U;$=;>|)v2j+U(18?d_6oo++mIE z@O%wzXYZI{Cfr-5=oy z)>-z!^)wD^3BrC{fAwUfroNVkC*=HIZ}LpzY1U1n(D(2v2?(n`tC$z(jitH@7^MkS3RDpLT|EiUJr})L8(&6C%Lr9|x z@t|BiXZU+>8)M22H^2u&Vf}^*OEqQ{T=%{)wqNH%_<-}KiYw#<*uR@3Ei^3bSNHaJ zTbJ+t_mQg%>}Kp2_lTJ9V{qgfeU{5;NC!UwNsYA&kVE7NgZ@A?LmNSjlS@de9#?xy zw0KtMYFyIF|`1*~UKzY3?WO8Km{C-^KWX-wHz3Kwq z>-^1)5&4vS>cuP!RXLy3cq#QK=mL0tCHT9qK_8JvBo(4qPNOq!^Rw8wQwovIJKK;n zg+@g`ily>Pm}2rrxQcKjL~q&U2YGsW9@@2QA;^29BeU!X5aG8$T|E8XLvuGV>~noX zgzlAwu>0Y&lg3KlUJeSHiXS04;ft$j2^atBweX4@k#~BJhaAov{W*n(`pwa=H!_SVM(_=g3j{)2vXJ?g3~L?iZ~Zt zy|naG7-W^<)(9tE48qP)c>&}E6t>0SS$?eu4uCx5i>Gc{olrX2*q3n$gEjoa43(JM z7}cZuetoYS2Q-jWuek~vSUVQ5mJ~)L7c%oG&Z~Dn8MZN6Op0Nw;0-h%xkl=ZRpM%@ zJO^D(#Rrf&oWW(_(6Du%wtxD>TnN6dWHD{6c!ROaNMQHiL~@Z!Y3_c|0in@zXrgc3 zx|N&6Ha;UI%`C~swilQ1Us(rh9QoH~G z@mY^Ei&EUtD4At^mJLhNIMgu>PX)TQm1sDtGw-*;3k`0)0Yax${i!nu^$7AqMRo!O z9l+|-I;ZDG^WaO5qu-e1>I$ZW?Myh!#EAKX27LnJ{?EJi?6FP=&o~!NJAQm(4;(P0 ztHaCpu9~dL;PYT&9vhM3{i*w~pP%ZdvH%4HmP2}4&Iw&wMk){XHAr-|F&(7 zlOPTWINjULY^cC})-BCSFE%7Cdw(7ahEmCRmf1{m)7r4X%}-MnUFuc)coW4;a-gud zs|#hdV*1m6cMOA_HpiQ-7Er9ww2JI!!Ugmw?ov2PUOxAVK#umk%ar_Eue58!pc!aT zOxkK%*tWd$a4@}}2}s|Z5^r&|gtQ^vz>wSsQi8isHrep#BPI|$xAn(K;$b|32hJXvvXaCvBsK`(`9$|FHey@%&tZ!;toTFw%buO8 z2u%-`DWRcugmMfPjs@^OY4+|7Sl1unYnfP7c>miH>u<{RQ#bRX2#FW(B9`(jeKPzrN36|N>T6CX8LS+rgEi8aL2jr?@id|4zIwm>#erRtI zwKX6$7tyo>;1qq1S77U?_v~@qLfw#GRAe-GM4un5J`_$%VzPb+^o@i(S##j6Jny?m z)~l$L0`ho#ETb8;EBxI{X~r|_U}Jx!K?g{)b-l5%!_T_GdYWXY_un^Q>4&c~Fi^!~ zhfEexP`DIczut<5^d3I(LQ0WodSy0aWkdJ)2+kmGkLtK{C*OYrQt;Qrh0Xzi=dyq< zsyBAM{8~=_G&p8mIKE7Ad*)suSoiSP5CG*42;boiS2d>~nsaxwQBXk01)gdG7Hyt` z+;5IwS33a@R8&XOsMFu$a%}i+9Qji#JnVb-N_c@U!pLAdN>6vZtxp#c8ZPQy(6NSo zTbp>|T~Fh!7O&APL%(?s^fQ%v!(M9U-rmc&-NWHtJ+rINyg1gxoy3OTlZ@_Ht&finr9DQRRX0kD z1Ko9lPG_1H@WH9t30a0`9z4)O5~g5eWoGRkbgdCil3b`YqLT;x2|1rh0~(9&7)jJ4FhO|P$|PTi z5{-K{N=Xpb-18Z&h)Whn%3|IFXAu;q;7!+HRM7t*ESupjBZn_{tIh9s?FSx&Hh{pt z6Q)F?r~y`N07oht{MC=AS*n#kpV;=t(@{-q=mQe@UxxhFC8xOs_yzMjSWab2ZK>DX zvp8$0`Kas{;!eH|qmKqJ_~yk7)igC(oxo^(vsMagXByezM6Op|wd$Gk_U+mLQKOvI z`Ryg8q>lLdt_()2$wQ>}2235KWEP*&lAI*J*F+m)stUx)thj@xzLWu~8P4;Q&}$s9 zGs;!y+o>9=enViW`0m~HuzZ0p(0>8!mJ;J9*Kggn4aD~)$k(|M$ zI|*w1qIIk}vxW#Hujt(1;NoI=;+b@5PhlTU*iJmPfk8wh4kAVQ=*6M$07TmSYT*$x z*gb4vGCCwMoQa`6ii%RyVsKbt&i&|th(`X#lE)|=52x5yTioTl9?VQ3e4e22TwM0$ zP5ghW2j3R5VK=CjrI?5K8u>WBHVFEx0P%o4j(XC0a>Dk&I~~O6sVv?QHho2J+)(E2 zna*xpj}NveCo1z17WSyB0Ghzr1#HLb*7ePY?x@n+1gI*T=!B_0k;e?49p1k`x2tEQ z$(LY97$Ys?G4M=b$^O@RU%1VF9Y*jSnCtzneH)7l8s6Z?PMjD(Pu9K1(ecDT5Cor< z9q#-rTwNkEr5eZbpAxS-T9%rYRt)eQBW`+ssQ2Q^tgOhV$YEODyR&+mi?Gc{njZZX zH-zidGq=x&%ha+NK-aO45ye_{tM#`abK#k%|7MSOu`PS0+NYpafRsI-j-eTrotB|t z#d7(%0VZ8fzC4s9=#_*wk%~*CtZi^Y^%d+e+*utyE11jpU9nf3k`P0|q$egSdUx#PGwflZSp^Y(NCyJBhk8N^ z$4W|TCXcJzGR{*OXhicP7rUuv+k=|Gv5rzJ850R+Xl56{##U^yAO}UqLMiB#r|^iB z`WeSAF!d<;(8nHvdHUrhwv-zOc3!XsyX>xLJT|zQV`JG+B)7Tx=?rbaz47EAVLN)l7Qfq`#^1o*g#6=X(4J+uAxbF>rT3T zYVJO_;@~KPKnyp90HYw=|B2m3(Y0MxkO5(ur80_S)Qdn8kcIUA{d;;?z{}n-qci0T ziI`i=6}%>$=p3hD()Ku>Q2awfiMy(tBELF|KOWVM6#N{XJ=j|lWjyff7jk{yByucX z3U4Xy$gZrEN7~@T=f9Kg@;G?|-QC>{yNu>sjq(@jY-u^Fozi%2|Ni}w67+ZzjDFOS z@>~6W3m&->SOAxObX?(LZ#vKia|L8G4I3#ybf|yMh`EsdbmY5r8*Qn4T_@DBRXg*k z%ZT{8o?F})x`igsSfa*doKs!fo+~xWJFW*%V_2ivZZ-)wL@o;xE_NUGmv)+`v(;ka z+!K|1RB3Z0B{6{O!i$dshzkzWRTo8=WPu_GD)e7-H-4`@FDZ7v*mgmHwGYECWyQVX zEEQz8>L^?r5;^Tr$g_gEMUw{#qj$?f3R<)0j}YCI)UXV_swFm8*XQ!VwCt zu}eL<1sxC=yB2&u0?QR+JR6|Ye6?&hp-k%bej?KWgyiPrcs*})=cm2Gk*t7_o~fRh znUe$7ay1C|)0e+&{4#Bq@Rrqtw`3-26L@=5wi86C)E1nVN(oym5dI!5V{*F9Su4eO z0p;7z+$E;1$F64OgF-oI2R*v>^&4yW?cCfhfBx(`@w?#GtA!$v_u=L}&_rUW?RW5C z-O894CFA$kuQGmmULh9}Av+2_JVnJa17qO^#89Q1MmUAG=+IHJ#e!D*>+ipR_uzw9 z+@yIqO9dke+)CMbd3np^=ej2i=KS=bt{}z_eR=%%>-6*!a|Z&(UPy*TIq?*j8F%x~ zt<;}m_k>x%0o>mSg4*)zlCx|D1%=IEIqWRw?2^ijQ_JD6f8Fg$0x+~aQ2Hsqdk7Zz z={y9J%}Isa|5AV~PadO>?+DfB36@+kMpWMYE3UKAElLFc`FNC{S)Spk6_L3zM!P7m zV_zhdpBNA5d^+K;DE@7Zu$9Pj93dl^m5A1t(l2_SUOSrM8#3B!nO4^m8pQ_YQjR$J z8=Kr_+>3rGoWf00W0N$to%a?^ifBsb77hwc_ccfPV58<+_&u_|(5rXyy7VNEPT0Ze*IYB)$K|JOGI6IhXa9Q41{F}ssd%wC8~ulp9YOgld1wp#$|anM&T9oePNlP9Se4ex zQ#~Cis-{NL)W90og+wge((=L!&Xs3Fa0+DTUGJI4|||ME%0hs$YDW`84j>{`V1mXYE;w z)g%&cb@cSQlQl^1g={#eNa2iNoV=-KEE9v_VTcju0!wtY6OSxKbN8-Yr>rGwk0^?V z@blsREdeNr#uUH&$cGWjk`$IxtKzgeO(x)7C7+6zuy z&(W*N$ulm=vF~&iC8Uu|etNC=OZKy_{;{@sOg-yX@TJjwp(bf~lctwp;l^I5n#iCv zpwF>A)cLf{yz;f16YBNzS<`x>3o7M89&2z@K)b3qOSV#pvz@{}1>rZ4LL z!{;c-appHK3Hjs`FDXS*JnGN#b^vA|YCn`o{m&b6Lz zQUvCxsjJ(r9OKE8e))7y8bM#DQH=>mSu${{KgOSCVGOd08(BZu9H$h-W-a@8$|^^( zlt}mI9vw|hmt^NgOG(jdJx!6O@~@l0u-tT7I*8CG9K=LKih?UTg=TF(C;-AQ!!26e zTSJqsZQvL}Zdmp8{$8Fk$x=o!%yzL5lRfWE-O)fqQyu0^DX0&VToixs*}1xI-NT5u zd5!YfO_TJREI#4snf2h|LrKh1bgb376u=xS$1OKTL=AtK9*Qj`xwC@C+t+3*Kb(=V z0B3e9kQ8#Cy25~U6-|N8!CmqNCuDPG^@)m`TpOyn#7^hb1vzrHpIBG42v1(wB;}Rw zKYupZLeR~mP%duTyg3>P7IrN&rATKq5jVs2*)!13_$WD8#Sj9w>YV>oN0W~ z=z=5XS4;L>Sh?ViUP5tjcc_S_YxSGyV(Wyr!J{kmUW{Jm;h5E&}tth$1PG zSyjbJRTOM_u^dyLEPO*0t&g%)+P@bFjW5htIyx}qn$hgE!Eq1gw%r0bKLdvBF{FcX#_el0uCBa4B|6t=SRbv{aFK7lMSu*zhz zOu9cHQvC_e%W&=M>dQ*WMCFyE3VE&TfO~F~)R-;CLHUu<{q%U@w!Bjl#b^FJsrs7e zXU~~@g{j@)2kO}6XM}{U9Ej&0_k}`vuNCn=*XwnUtDMQRNK`KDJnS?IN)C!S%&wV& z%yP!hN|rSoR;C)akjpH$Pp+BYKOUfMg+yLdgC`-w+MmK)2141)&SR|OJbtu}V99MWMoBO?4_=ivkR|MV~K=*4#56ksWzgR1v>~$vAFqH;@x^PIVmSe& zA*$(IXi9mQT6CFmLY+_z=&pKJj`aj4^m*zEv$hx~XT#jZrQ&MK zT+76G=nM7rRrXYctz&|Q%ZGS38Pq4P`@_O+NFEgCd@@&}K+(D)q_mIbo22CqxNee0=nZ|N=A(I=t2Cx&-F43rVEE-a1<9K^47>UIl{lcbfjWpQv_Bpvw=gIT2XTo++!T6Z z$KEr68K~&FY=!dObJ1EQY*FrLZ$BYwyP46gGOJVU=-T{@JfkhTpZ zq?~s4ez*UYaCAN6FTouuEW`;2)9E)bR#Bmr_bpD&IPm7njGIQpvua{SE9a^&w z+i!o*`3+Ng>|6=h=i?b&iplzjHr4iUcNM!4sMiY0*^8NT&QoN4{rsMWVn}`ybc+Lz zT+wEtgZX~UKucf$$%)RV$E5-}83#UY|HX0rJ2k6@LQQHy>^gf*+M5k`9cP`ZSSc6T z9adIWtadmHr(FGlK@JdyMcruRXgi#oWK^dYAsqIQ0T98VgF311U$ExhAm@7Z3!~;9 z4tsWG=tO}tZ}*0k#3smK?u8++a1i(D#!Zhg&w=RoU)QrQBT&xY+~iiW;GF8GT@6d5 zS!>kv=8ZsydE3{3ng?8k| zk0czJFoms?w2*pD?g-hMDS2_0F^@ilt7aEc<^oeBCBS;vqZi-Ja!}`u$JRNfM^SY< z0PlMaCt@$jVH>YZoXLnM&H!9c78leh?>vlhAAhc1z4{F_L-Z<6HAio3y!RzTV7CvU zA5{JoquuS$BnV_>E*l7luF{D*OEi!yIUb6w?@L$*zF+qQ0%KEw}ON;vv;%mgsrw$tdh4b=FHr~ng*}nKtL*ThKLZ0ZTRD%j zn|X+U|Z$ofbQw!V6_m)y zSvHzQo{fi;&%o0?`dBmEw+9<4sd_cKf8Ya?jY)s+@+;DBkaJ=; z&8KJwvujrUppfp`k${mr-?w4>d7itLYJ zREpw8V07O-4U8P63eNex*0Uq6(WXb>K3phtP97D^5<1M|@v1r$8FObAP_Mat<5LMA z{$^TO>$w+ZST|(A*xZU*vLhjbeOdC;cRm7{H-9|eA)^vSg5`WE=a;rmwnQ9wLAAvY z`zr+bQavO3yrx8rCg{kp-TiJZAa+AS1sWFn+=5`0(Kp+MDEq7q0EL<=&4xndahW(M;_tb-f!T zSnZ*CJ|-rnPGvQ93(Bd8`;wCxm#7F7{ejsn>x_WdOZ)`~RlfB8j4!gOZ~BaDN8^aM z0n|LOBaLe`Tec`i?p`ebfTUv(Kw~5b2Pb>$h&S##$m^64RPh5iY9uE#7LYkAs(lkD z71WnbFMU1K*>sVUv%O#$Wo=0gZGVDsi+9Lkm+d6XJL`JcNdS*{AfJBhrR}oEj(o%X zmElXEOB%qQKW5N*Lx!IF8(tK&$e&4M#347#p^hj?Tke?9`JhUmbxN z0VXs0$8jpW0WEm(sLD!)*=Pp!o7$>Xe`PGRAo&LB($`B133+1wmNBQfo3ocdK&hS1>aXT2i2nzUVXL-ubd)*r)5leb`+g<_mAL&gS4l+i!`H7y$oD9D zCrrOaft)q^&I0a*6*c`UI}T+?DsnIHfBff4JaAADYqTru5{ub)qMG&;z?Tb~?)z|@Ay zA($i0b}GEM#ZDW|x)-G}A{LwxuigOxAYy6@Pr;e3H%-SX^~{F3)ALsRY>NA8p7 ztUajJ$1nzchA-}=U65}A9Y5|!y>;s>+fNTl=aa_?`3)HXuz)kj;w+!&#)5#>5ovJqutQ#}g&<{1s}BBEd>OD#Z|^3r+nErpV{!x(7L%3@qjCQP=k2yY4DNO;<QM94AO~JyK(;rxVVs$dRKXftT2dtjKA#-R+od3O-iu;IG z%=uMdJR+!B&P9DJ#d*uZ8Z@rb))W1x3p-rZrMDJ$sQqFX)u~`eHPs;F?YAsK14-Sv zzU*yAJ;xFvW_kJ2kCyN5;U$!`HKASh()K0!i7l~7!F^Z0j*aRq=+|FuZvVmmz=8fV zXt}_s6B0DhrH0LZ>2};nU_K?(b4F|f(OA;#F9H|C_rT;AV}@Xsp0nf+ZEkHXXFlJN z-(gg{$*Ltn)hmtGChnP(Lz?}d{9J)Y&e|rrhYaHmgfq@(>E1{GM(b{C+ZhCMC^?7< z+a;85R6%O*A`{MAQ%nMs1kF}y6O%rtvGP)yjOAHRYgdu~EuJ%a*i(@r@eH8+VL^o$W) zV(d}j_?7K4CV8z|7x@U47@=Y+p{S-nWS7G+-D)|yU3p0#50A{n%ce<6=rWjdH)fSn#Ngjg8|xrttz2N6@?9$L6Kx<|R1Esh6>~!DjALt*#aT zY!6L+8N$S|W?4M-TX8`_sCvLbd%^^?%hcRQ-;Z3ArQCbm?Y@fW)uN-TxaDhr%%}Sh zC0z=kh)W@k;^U)(GC)TEw>=dfx~>AbMg*7TUh<|*9@k9~_1x7V8&8gtzl5JOeVO#d zP(D*l3F>)q`u8KUXoJ02WzK>oGh8m(wmp0}(@DKvy1+L>h?0hmgHGKA4k6>q{k=7{ zN`It;(o8bkip6Ssj(Uq+&8I_2*XT`DKQUnk@O~5FrMgi$lI^rj*+(7uX)o-4lC{n} z%9Lq<(^9I9?{>8=o!iJ;%|I{kYQh%8#BR|IdO9R zqqD{`yO$9rRvU;c;YCCQqZ{vy->8COCYGtZIL;8bq5kExAi`NanI1MRlg)CMxnRx_ zb#a->dOl_Z=ul?p_iv__V^z)t0rBtpZ|jck#xMrJr3L@t2hN%sHdOI9%RO&7$Ha8& zvDt*!2nql7E9eP4+2~QMS4$HfvivX`c?Ed+U-f`zb&{4i&Ux5R2V!RtnVROkFoqFjjdX1~qDKKFWzR zSNizSw-rwB2&hiy6GsWPviJGP#kJ2lSL95Dt2baj>_c#P>+fo0S`yZ`x0^xIWjrcW zowbBmbNy0i0WCQzU*_(<5+aQ2*Bqo?X;$w1FOrv_;qv$pCjG>OZwo;@&Yax$lYE0x zL48xUeF1rX23G!YOKcORvS*y`@_142-@EtEQv15(`FSXLu|6T;a{vW@4ez%wKKlBb zHoHXm&uEj@%0NHcoQA2A2TRRSdic!|BDz&W=SUEdz~h#~kP>xU%OXSy8ZFjHSV-)9 zUg!Ri_zX+*Pk_FEX;6q>eoea^0s31Q(P0nWX#5G7?rnYaQ-0yJrT4CWy|PY|(c&ZW zs^@5JbySZ&+>9VOec!J`H>Y^WSlHW+iuL|P+Jp#7{H5A|wHm z0-JWGQa)ns9>PmR8Ct%j!W8<3jOL4ZYv}49oL4<>Jm4&0VuZ7>VLisg2J2}{0lDgj z3@aKfj+^Pl#MWC#?B-m!fZozxn!fcmKAU*#roGzN%?`E~E%0{HrL#izHhDf!zF5Ru zF~OKihpFVrOPjLIh*2Y)?Q<|C1h0_(Ly4#b>=GTRsLqjjTbNenanvM4>xy~KoEvuw z3r|VJZjzF|mQO6Rd=CRUvGhee-jr*&Z!aiY%4u^}T1vpo45~ZecMALX;%FQRb7_;S zxxcG%)S|BNeBj~f8R<{el%x{pL^Ic>G%mO>yTX^i2p8pTZtun-pMGQHg2NHuzB2)T z$Zpk;f(s)0b{I{mH~&@97C-Zx4knejMa$&_Qkbq~tHhkJmppz>fM{S~Qnb;H<9@71 z0alqgI9e<>X<`eHjx`&TSMVQJclb!HOgA9C0F%Yb9u?hRexaTodARHpl3mtcAstk` zH@Q*oX-xlH;?AFpA)5B(9$Azwskxw~@72jcjbbku$GX@b%1KvQ%_#UaPa@&uB%^+F)e7X(^#S+&VIWpVC z`RHpG$9=iL${+~TUXYh6k&7+b2uBA8^9$W=o{>HJz++Cpo_}=bF|i!V+4YN?^_$1S z3~o9)AV5$W!&UGz+v+RWL40fJ{B&<0pZwPzns#2SD<5d(yKQiW)M|4IUcD$^G2#Bs z^8qhn4AxAz>&PAdwoZtUro&{}Kqck}$j$Ewz&22F0&I<2qCu{1XVRWx`sw#T9mBhL zW#1uqxVWnO$3ISVi&UrALtMA%{qKheMpKN*3M9~17>EmsuNZzIaGT<>VBx|-Y+i7g zueTB~6m97lvC7h~GuDBxIUBb3kA0?Sz=&7vddxb4tBg?wCY&*5!c(*lzmKtTWTkGv zF#ef4>xT>}7uq9o;#{vWkD|k_U54g!5%<>Et%E#1Z#RxFQ~Um#INtDQBhzd&-dwEn(%xHldC`~4h<1{r2Pf&g;3b4GD2$!Yd#xAg;2RT_ zZ-%|{ZR?}8NfEs)+t>Z$TofG6$^(Jr`!ViVqe@4y1G&of|0Gmr(PhJ2TCDFrIT9j| zLL4bTCBV2S*Vl?s&-*?4%=|~zDv@~!E6x=`|6kNGC`Di}k7u1Y@11x|0J}_bZuITj zmw4{&D!aTMGKiJE9ZQRomut>d%xUxMc`0y7=QPwHn2%qmoOeH0amtn8j)F&-6Yi=> z)W(vP9w#*>XEcy*oF{XKL^^kU`HH30<6*q{+lxES^gLN7nyC`Aj!A5h9uYQy zsdsy=F860*92d~46W;&w!u&YK^pA_i$zr^nLSb`4h&idbONkO*0)^u`0{oooUx~o6 zxx`p+uh+T;V^adNDCcDBRyM654Gk#&dr zVxz(67tJs@KH%OE+q#vY|3F#~rWY1jgd!8#m=ELQ4;KbWDVlQ*%;7(?I*~@a=2Mhe z-4e;tm=!cZEtrIn)Yt93Gy?3{cU%`R%vr<^cU6X(L@ZZhQ|l?`LyXc3P<0a~h$hW$ zL7<_Hq{SKsF?yRTp`uEZJBw{il;kd|P9GiBH}1kJmJ(I)6E09C)_!pI?^tQcZ4B$9 zZXWnZ;`5GMCQ3^3E>Y35)skE>8}~vE%|iGs>Q#c!rZ`;U^*C|zq`?EpwV7_kb-2q5 z<1l<*1_t2WNW+ADLP@@E88(A9|JKxMl()MG;vMBa0D{d+rZ>JaK4!4z*y8DIRU&3h zAfdEu*)rfFOUpa_9*OgsUS#FQVI#gEQZZ2}9OhFL$+XGaCgs8dIxA-OuF}Fp{F%%< zcZ4v9z6VASrv-e8c}$~N}*ruDBe}}uNj+TMk+>OrDOZ) z7M+{Rn0YEf>WyZAy@#ge&);&mYbpT6x2`8=$2D4Zi);de^nA+dfywVl-HKzjpzfc= zr-R~f42NhO=jM&>q}{y3#CGY6%U+IE;{NO>=-Nc@i_)bgenqSo0j5$Y=&L{tI*J+w z=mPrVc&p_}u>=@`;Aj0eYgSjny(nt&xhg|fA-woCFaJmkpTZe(cN&*zA3evSDa7*pXaeSF)fFttj&qn z%Py0esq&hEX|o}u59;!Kib4t9oWI?U8PM2NdVt}jz!{DPLkzIb?_;hmvv7YGoK?v5 z{j5PdbJl33<75V3CV~=9;L1(*mg0k7AD@LiVpu#?Z03ZtduX!S8pSDq!Lj!A;0|Jo z3wjj8GTvNrER3bpuW}%* zZETXWJcP}^%LxK)06oDtl!Ec}b&92wFOPFbd}xXgAqfXOE?c%Lyy9L_(Vro%L(&12 zh&lQ~a@ALD@-9m0(#`o{sAFBfjlQ61g8oh@GILdvaXF!Xd0UqLPm-ucdg_}&Dj}%v zrG5SW!H(4(aI!B}91^xGO5lqJ;4Mrw843BSq(@I>GQsgKwc~itaW46|nx2}PS_fx^ zvg852OnG@7QCxW7G++OFVzwEl$D~vJLf(5hN}1xs)%9| zJjRF@QZ#9O*!}&;C0te3(szjSOFVY$m=9B#4=eJc|c`0%ZnW)DpuCwUQU3wItbLRmJdL84ee z(bfx{GF)dRjF!>+DhWALaUJ!>4MD_x^|V*5%-f!#se5JX3jKN4UET=UB)~4(Am|0O z*d#51w>(>}zv=VY%VXlDh%zO*k`=vb`j#c9Ux9!vwTj7m6*JybK0fvT0?YF|<`D6? zaq$jMF>$((kGE(D@Db2Tjhq|JXz&^jr z+|5xwFXw>@pnE->ve_IC$J#kGHxsfQw_@8>S4nfFy^&A(k35p0xRiUuEonC4SGF9T z8Xy0P6E93(KH`=5uz>M2<)Q1{|L7D8e{43Mna68-t%kE%Xyz90Yx<6kLNuaNji zd(U&9cv#@@+S!@hmT2yH!MneWtere{mgjUe&GPnkjVVQ+)JuGFDCCUm%}pAF3}-`$ z0uand)@+71h%@yuS@}yvJxn=kiEB*WBam6vdM+6dJI-M|2`c^y(W&k;JNAnC^c+sM z@B(Cj_uDLa$sEpPm5l^ z=LqGUHc_p9l~&m1s!zH*$UqWnO+pn%f-^h&x+)kveSm`R9aN7qxz<1%d)KdVe7#JO zsNXJ`6#jO~*qHPys#5y4K#Hzx2?B?1%ZCr+%929J5NL7{24_1G25ei_@;YpcExu6J zby65TFFY?%+FdX=OW>R`O22^meMz$S0bWxS8S4}iflOd>?X+;$WFb);ha zOvZ`S&El3mD82$|9$oap`sa~tR0=+lCVn_G`1hp~G3;>TV~;=dPV5=V@h z*PJ878{lB8G&UW2$d zOuZ&Rh?CKtmBf32x$@Jk5S~rc^^7=8YP9)1;#$c-ug@B~jAN$5d7Twqke=u38e+q_ z9?2?HtR7fJ4Jr4zND(|L-=D4Wx(tPyVWRRf;ms6ud?D#|@TT1JpAf)Sbgi@?jxDY) zTeB>QE)o4fRbF0yva)3a5fQbw{_|JQ!#C{hc>FNuJ{-YbAxPDcvzD%1&k-g#UfRrn z@D1H+6s`aKem2_L+D=qgSF^d}?dYUi{L0FN#?O5=3L9H>P&=J)V>ja$EUnSZ^gU_A z7BK16x7kCM1)9YNO<1v4@DM>Wj29{NdAz4rg;4~Blz9jX+Ihsrycxu*f5P_G{C_S0 zOT-$Q^tJFgwM@{Bz%QZJXs)xD1YTlppVQ0l!Qrcp$*RZtz0%;IvRCbKaP>$oc?S0} zSIcFHv3~KbCpjOK^-yPr-CEZbL=Zn$By90%AfpwlOU~q@Krgk3pq!n8o9qy>O>d4PU`eba}(u4qJ6zU z-G&lAhi9{=rhmk{@bD%aqYD{igfIzN4x|Kq#8?lgDq8B z$P`gQTy{FfB+AZ2sE;ocg_Mk z3jLG;L%wch=z-pU8Wn@HS?MI?%{zACC9=LpD@379-Z%@Fr(58hf5F6ga`H<6si!_) z%PuA>hj~-HjTG5>lU%_kc&&zt!ly>F>CX<^^~bjrcL+cQhnDe%J^kLj)u5?A93S86 zSYm_^{R;R;y{i#YP~<1v0oGu?S*#ZGga6y}&QIjecWg!gCnbYt_56$STq+tQGCX*g z)kozq!}$f06$65_-joMM*NCm{;ZyCXbeCsvG8#$g(eQcwoMjcZ|7Z-BaWTdGC&#{u z;hX9xy;W|_^`^jIvRc5fPkyMyh%oB8$F$!A8FJ>hJOzglg9A1{U++A>q-&JkKr|xtgGMMOqHRV*u?t5Rg$y!5313z zu#dXUx@i7#1yYz-zR>#5U28jfIMOgiY#w}2vigI^jrrfC_@+u>3s8gJgxWt zIWb$0(3H~eTw{aI!+MKfoghZlK|sXtWfCEGy zZ2LBT&RCOBCM2SwREkPP$qXfFu@#}k5+waUL;yC@Qc}mUEnHwp>z-*0EafXWpU>K4 z=KxNu^Wo#icWZA-jdd@RJB-dC-?nER;fXZq<))tKr)XmmqRmX&8lwgW=`wX!o(O}R z?C>v>*Ae!fud}`3l3WPQcsll$Tg0fRYKU4v)7PgsfWA2wP4QMjXkX;qb)u@Y>PvN0 zM~w1TJG_k?b+G=Y9%{fI@BO(c=%qSi8CDz7)h;?pRZA1LGDKVSkjOJApineI$j`w_ zqSCD}x%80vVA|(@DsC)k}7_e{=qxKRQ>6d^zh;|GTp^L_By#b z^5={HSQVX@zfwm0Dwb~nNn%w;QDp!orGs`mtf)DpgvOMs?Jk3Q5?&^B5w|#5<_pBeyLfYN$8Qa<#(d z<&JYYLkYgOzoocG&_MiW=cyr9zx^HyW{wDas7i;|9Z9wKABzkWG0kdO0p||o>XK(Qr?H$;kiURyj3oJaZ`l|g;kR^bhX%kjvPb3g^m~4Ej56cA}-0LI!PBQzR}5Me(+27WxuuS zO@?4k*bguG8ibnb5VVGKP%!l{^QZEc(iWGGJHvfjjls?A0Efi;PqXpkK()2wO0w>BQd>BMdKi)m}^bEIUz#eTC+`e`Ebf=jVqx; z&<3u{Vq@ePsH?7ubMjMkXCSbxqh;zLd(|(Na{_b_aIEdbZ}E%=UoBN< zFVQP^2@e&hY}WhM>jCKC+I=R?rkBz*$&s};Kek8e;ThbvV1q{4;B@!tjIfTe zouxJ?NHMN}3`fr8&|l+SaWUW7&j&Hp361DfEB^j`+h0h5T~9MEwvE4Oc>L*s&A~MM`z%6cbTmKdL!#oG}$sqFqx@ReWKM8*3hJD1=^kVd3yznLvW?iY25Pqivl75lA6Qzvu_F`%# zhTg@0r!6AZ|Fl3HT(9o3?ngh)2{Fe8T5%|ruhnP>N7;2K6YNqz#n+jye6}X0Eh?-u+ZW(fUVXdt&JVWf5Kt08R za#cp^Jha>>)v+!MnQUY#L;kXCePzu0=^wv-y__4=zC^~jKPiq0n>I#xy@gF4Z93cQ zhgs~_Afot>Wc^iQ z%osJK;PF|dmP3i>P-txI zuVo&lD8OBU6!TwH%pY)dv;-QQD_PH+AHs0H14a^Hz=+mi1_Tx<()MrNWhfV}5_^zAzlX_t~apO%SZ&rIf_?_Rr zN4yv}@Iy{+Zf_mLo^k1+=uT1!3K{|+pp2MzV!^${%vmCaz|$_AXIAV2GN|Z>5eK8{ zq>ZYqdm@*qeZER-YS=w~aK}3C+SQm+*RercVIz!JrwCgUQFf!4TORy9CEmmMdcE88 z?p%%;Yx3(Wx)pbqwe@xbqybKn1LRTE%nz^SdAlP%FP_KRX>A>s;PKjurd~x-4kcc! zYIA+`MP%Qb1qjyJtMAuo(Rgj;K{Jrj)xE^~cKjbElPYxw*KvbI0jbL{)_G6_}dA#?)vb z)tVK#a>V}L6TPUN3u5((KN$fhjS|xN#N2_Y3}1Yl9b&}D6$F_%tHx?L_k_t&OoQ=d zjL=wY&BM{@Yoqm2Qm0VEQZ-G3lT$OtN@GO~FKAAm{*Y!#v>`&7ZXAuRiZ2y*x{W}^ zXtI?Vhf4X!*6wwg%SWM+HqIMdf^SJ_tzYLl_+AdfbNm7qZ$-CzGWGZ}27Z zQbtbI*b;mq)W6FTvS%zwo5FCu)d8J-l^5uL^6-3Zb*3U{4`ppws}m)b+l*{l5B%4f zHNOZ8Le$gcGMkRi-o{&?@s5NoeNVPzG~9W@

j6#`L}_T|j0)BGrg!V_5Etm|SEU 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

F~`3~0kv8c|TR?n$tz!5!`P7)j% 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 + + +124356S$T111111LTRT diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java index 476694958..01505f39c 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java @@ -49,7 +49,6 @@ public abstract class BaseGLActivity extends Activity implements ActivityCompat. private static final int AUDIO_PERM_REQUEST = 1002; protected SharedPreferences prefs; - protected float[][] vjoy_d_cached; // Used for VJoy editing private AudioBackend audioBackend; protected Handler handler = new Handler(); private boolean audioPermissionRequested = false; @@ -406,7 +405,7 @@ else if (requestCode == STORAGE_PERM_REQUEST) { } //setup mic - if (Emulator.micPluggedIn()) + if (InputDeviceManager.isMicPluggedIn()) requestRecordAudioPermission(); } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/Emulator.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/Emulator.java index 28d8ba14e..5a089d2bf 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/Emulator.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/Emulator.java @@ -10,7 +10,8 @@ import androidx.appcompat.app.AppCompatDelegate; import com.flycast.emulator.config.Config; -import com.flycast.emulator.emu.JNIdc; +import com.flycast.emulator.emu.VGamepad; +import com.flycast.emulator.periph.InputDeviceManager; public class Emulator extends Application { private static Context context; @@ -18,32 +19,14 @@ public class Emulator extends Application { private WifiManager wifiManager = null; private WifiManager.MulticastLock multicastLock = null; - // see MapleDeviceType in hw/maple/maple_devs.h - public static final int MDT_Microphone = 2; - public static final int MDT_None = 8; - public static int vibrationPower = 80; - public static int[] maple_devices = { - MDT_None, - MDT_None, - MDT_None, - MDT_None - }; - public static int[][] maple_expansion_devices = { - { MDT_None, MDT_None }, - { MDT_None, MDT_None }, - { MDT_None, MDT_None }, - { MDT_None, MDT_None }, - }; - /** * Load the settings from native code * */ public void getConfigurationPrefs() { - Emulator.vibrationPower = JNIdc.getVirtualGamepadVibration(); - JNIdc.getControllers(maple_devices, maple_expansion_devices); + Emulator.vibrationPower = VGamepad.getVibrationPower(); } /** @@ -54,26 +37,17 @@ public void SaveAndroidSettings(String homeDirectory) { Log.i("flycast", "SaveAndroidSettings: saving preferences"); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - Emulator.vibrationPower = JNIdc.getVirtualGamepadVibration(); - JNIdc.getControllers(maple_devices, maple_expansion_devices); + Emulator.vibrationPower = VGamepad.getVibrationPower(); prefs.edit() .putString(Config.pref_home, homeDirectory).apply(); - if (micPluggedIn() && currentActivity instanceof BaseGLActivity) { + if (InputDeviceManager.isMicPluggedIn() && currentActivity instanceof BaseGLActivity) { + Log.i("flycast", "SaveAndroidSettings: MIC PLUGGED IN"); ((BaseGLActivity)currentActivity).requestRecordAudioPermission(); } } - public static boolean micPluggedIn() { - JNIdc.getControllers(maple_devices, maple_expansion_devices); - for (int[] maple_expansion_device : maple_expansion_devices) - if (maple_expansion_device[0] == MDT_Microphone - || maple_expansion_device[1] == MDT_Microphone) - return true; - return false; - } - @Override public void onCreate() { super.onCreate(); diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java index 4f84b6e2b..a729a92b6 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java @@ -16,10 +16,8 @@ import androidx.annotation.Nullable; -import com.flycast.emulator.emu.JNIdc; import com.flycast.emulator.emu.NativeGLView; import com.flycast.emulator.periph.InputDeviceManager; -import com.flycast.emulator.periph.VJoy; public final class NativeGLActivity extends BaseGLActivity { @@ -61,38 +59,35 @@ public boolean isSurfaceReady() { return mView != null && mView.isSurfaceReady(); } - // Called from native code - private void VJoyStartEditing() { - vjoy_d_cached = VJoy.readCustomVjoyValues(getApplicationContext()); - JNIdc.showVirtualGamepad(); - mView.setEditVjoyMode(true); + @Override + public void onGameStateChange(boolean started) { + super.onGameStateChange(started); + runOnUiThread(new Runnable() { + public void run() { + if (started) + mView.showVGamepad(); + } + }); } + // Called from native code - private void VJoyResetEditing() { - VJoy.resetCustomVjoyValues(getApplicationContext()); - mView.readCustomVjoyValues(); - mView.resetEditMode(); + private void VJoyStartEditing() { handler.post(new Runnable() { @Override public void run() { - mView.requestLayout(); + mView.setEditVjoyMode(true); } }); } // Called from native code - private void VJoyStopEditing(final boolean canceled) { + private void VJoyStopEditing() { handler.post(new Runnable() { @Override public void run() { - if (canceled) - mView.restoreCustomVjoyValues(vjoy_d_cached); mView.setEditVjoyMode(false); } }); } - private void VJoyEnableControls(boolean[] state) { - mView.enableVjoy(state); - } // On-screen keyboard borrowed from SDL core android code class ShowTextInputTask implements Runnable { diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java new file mode 100644 index 000000000..422dfd145 --- /dev/null +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java @@ -0,0 +1,111 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +package com.flycast.emulator.emu; + +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; + +import androidx.annotation.NonNull; + +public class EditVirtualJoystickDelegate implements TouchEventHandler +{ + private View view; + private ScaleGestureDetector scaleGestureDetector; + private int currentElement = -1; + private float lastX, lastY; + + public EditVirtualJoystickDelegate(View view) { + this.view = view; + scaleGestureDetector = new ScaleGestureDetector(view.getContext(), new EditVirtualJoystickDelegate.ScaleGestureListener()); + } + + @Override + public void stop() { + } + @Override + public void show() { + VGamepad.show(); + } + + @Override + public boolean onTouchEvent(MotionEvent event, int width, int height) + { + scaleGestureDetector.onTouchEvent(event); + if (scaleGestureDetector.isInProgress()) + return true; + + int actionMasked = event.getActionMasked(); + int actionIndex = event.getActionIndex(); + switch (actionMasked) + { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + currentElement = -1; + break; + + case MotionEvent.ACTION_DOWN: + lastX = event.getX(actionIndex) / view.getWidth(); + lastY = event.getY(actionIndex) / view.getHeight(); + currentElement = VGamepad.layoutHitTest(lastX, lastY); + return currentElement != -1; + + case MotionEvent.ACTION_MOVE: + if (currentElement != -1 && event.getPointerCount() == 1) + { + float x = event.getX(actionIndex) / view.getWidth(); + float y = event.getY(actionIndex) / view.getHeight(); + VGamepad.translateElement(currentElement, x - lastX, y - lastY); + lastX = x; + lastY = y; + return true; + } + break; + + default: + break; + } + + return false; + } + + private class ScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener { + private int elemId = -1; + @Override + public boolean onScaleBegin(@NonNull ScaleGestureDetector detector) + { + elemId = VGamepad.layoutHitTest(detector.getFocusX() / view.getWidth(), detector.getFocusY() / view.getHeight()); + return elemId != -1; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) + { + if (elemId == -1) + return false; + VGamepad.scaleElement(elemId, detector.getScaleFactor()); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + elemId = -1; + } + } +} diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java index 45aecceea..ff06fd237 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java @@ -18,12 +18,7 @@ public final class JNIdc public static native void rendinitNative(Surface surface, int w, int h); - public static native void vjoy(int id, float x, float y, float w, float h); - - public static native void getControllers(int[] controllers, int[][] peripherals); - public static native void setupMic(SipEmulator sip); - public static native int getVirtualGamepadVibration(); public static native void screenCharacteristics(float screenDpi, float refreshRate); public static native void guiOpenSettings(); @@ -31,6 +26,4 @@ public final class JNIdc public static native boolean guiIsContentBrowser(); public static native void guiSetInsets(int left, int right, int top, int bottom); - public static native void showVirtualGamepad(); - public static native void hideVirtualGamepad(); } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java index b70cda441..da10864e1 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java @@ -10,6 +10,7 @@ import android.util.Log; import android.view.Display; import android.view.DisplayCutout; +import android.view.InputDevice; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -24,11 +25,7 @@ public class NativeGLView extends SurfaceView implements SurfaceHolder.Callback { private boolean surfaceReady = false; private boolean paused = false; - VirtualJoystickDelegate vjoyDelegate; - - public void restoreCustomVjoyValues(float[][] vjoy_d_cached) { - vjoyDelegate.restoreCustomVjoyValues(vjoy_d_cached); - } + private TouchEventHandler vjoyDelegate = null; public NativeGLView(Context context) { this(context, null); @@ -63,20 +60,21 @@ public void onSystemUiVisibilityChange(int visibility) { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - vjoyDelegate = new VirtualJoystickDelegate(this); + if (InputDeviceManager.getInstance().hasTouchscreen()) + vjoyDelegate = new VirtualJoystickDelegate(this); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - vjoyDelegate.stop(); + if (vjoyDelegate != null) + vjoyDelegate.stop(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - vjoyDelegate.layout(getWidth(), getHeight()); DisplayMetrics dm = getContext().getResources().getDisplayMetrics(); Display d; @@ -101,10 +99,6 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto } } - public void resetEditMode() { - vjoyDelegate.resetEditMode(); - } - @Override public boolean onTouchEvent(final MotionEvent event) { @@ -113,13 +107,13 @@ public boolean onTouchEvent(final MotionEvent event) InputDeviceManager.getInstance().mouseEvent(Math.round(event.getX()), Math.round(event.getY()), event.getButtonState()); return true; } - else + if (vjoyDelegate != null && (event.getSource() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN) return vjoyDelegate.onTouchEvent(event, getWidth(), getHeight()); + return false; } @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { - } @Override @@ -179,14 +173,18 @@ public void onWindowFocusChanged(boolean hasFocus) { } } - public void readCustomVjoyValues() { - vjoyDelegate.readCustomVjoyValues(); + public void setEditVjoyMode(boolean editVjoyMode) + { + if (!InputDeviceManager.getInstance().hasTouchscreen()) + return; + if (editVjoyMode && !(vjoyDelegate instanceof EditVirtualJoystickDelegate)) + vjoyDelegate = new EditVirtualJoystickDelegate(this); + else if (!editVjoyMode && !(vjoyDelegate instanceof VirtualJoystickDelegate)) + vjoyDelegate = new VirtualJoystickDelegate(this); } - public void setEditVjoyMode(boolean editVjoyMode) { - vjoyDelegate.setEditVjoyMode(editVjoyMode); - } - public void enableVjoy(boolean[] state) { - vjoyDelegate.enableVjoy(state); + public void showVGamepad() { + if (vjoyDelegate != null) + vjoyDelegate.show(); } } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/TouchEventHandler.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/TouchEventHandler.java new file mode 100644 index 000000000..40f351337 --- /dev/null +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/TouchEventHandler.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +package com.flycast.emulator.emu; + +import android.view.MotionEvent; + +public interface TouchEventHandler { + boolean onTouchEvent(MotionEvent event, int width, int height); + void stop(); + void show(); +} diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VGamepad.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VGamepad.java new file mode 100644 index 000000000..4b85c8ce6 --- /dev/null +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VGamepad.java @@ -0,0 +1,35 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +package com.flycast.emulator.emu; + +public class VGamepad +{ + static { System.loadLibrary("flycast"); } + + public static native int getVibrationPower(); + + public static native void show(); + public static native void hide(); + public static native int hitTest(float x, float y); + public static native float getControlWidth(int controlId); + + public static native int layoutHitTest(float x, float y); + public static native void scaleElement(int elemId, float scale); + public static native void translateElement(int elemId, float x, float y); +} diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java index 813ef7981..a0276225f 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java @@ -1,409 +1,273 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ package com.flycast.emulator.emu; import android.content.Context; -import android.content.res.Configuration; import android.os.Handler; -import android.view.InputDevice; import android.view.MotionEvent; -import android.view.ScaleGestureDetector; import android.view.View; import com.flycast.emulator.periph.InputDeviceManager; -import com.flycast.emulator.periph.VJoy; import com.flycast.emulator.periph.VibratorThread; -import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; -public class VirtualJoystickDelegate { - private VibratorThread vibratorThread; - - private boolean editVjoyMode = false; - private int selectedVjoyElement = VJoy.ELEM_NONE; - private ScaleGestureDetector scaleGestureDetector; +public class VirtualJoystickDelegate implements TouchEventHandler +{ + private static final int CTLID_ANARING = 11; + private static final int CTLID_ANASTICK = 12; + private VibratorThread vibratorThread; private Handler handler = new Handler(); private Runnable hideVGamepadRunnable = new Runnable() { @Override public void run() { - JNIdc.hideVirtualGamepad(); + VGamepad.hide(); } }; - - private float[][] vjoy_d_custom; - private boolean[] vjoy_enabled; - - private static final float[][] vjoy = VJoy.baseVJoy(); - private Context context; private View view; + private int joyPointerId = -1; + private float joyBiasX, joyBiasY; + private Map pidToControlId = new HashMap<>(); + private int mouseButtons = 0; + private int[] mousePos = { -32768, -32768 }; + private int mousePid = -1; public VirtualJoystickDelegate(View view) { this.view = view; this.context = view.getContext(); vibratorThread = VibratorThread.getInstance(); - - readCustomVjoyValues(); - vjoy_enabled = new boolean[VJoy.VJoyCount + 4]; // include diagonals - Arrays.fill(vjoy_enabled, true); - scaleGestureDetector = new ScaleGestureDetector(context, new OscOnScaleGestureListener()); } + @Override public void stop() { vibratorThread.stopThread(); vibratorThread = null; } - public void readCustomVjoyValues() { - vjoy_d_custom = VJoy.readCustomVjoyValues(context); - } - - public void restoreCustomVjoyValues(float[][] vjoy_d_cached) { - vjoy_d_custom = vjoy_d_cached; - VJoy.writeCustomVjoyValues(vjoy_d_cached, context); - - resetEditMode(); - view.requestLayout(); - } - - private void reset_analog() - { - - int j=11; - vjoy[j+1][0]=vjoy[j][0]+vjoy[j][2]/2-vjoy[j+1][2]/2; - vjoy[j+1][1]=vjoy[j][1]+vjoy[j][3]/2-vjoy[j+1][3]/2; - JNIdc.vjoy(j+1, vjoy[j+1][0], vjoy[j+1][1], vjoy[j+1][2], vjoy[j+1][3]); - } - - private int get_anal(int j, int axis) - { - return (int) (((vjoy[j+1][axis]+vjoy[j+1][axis+2]/2) - vjoy[j][axis] - vjoy[j][axis+2]/2)*254/vjoy[j][axis+2]); - } - - private float vbase(float p, float m, float scl) - { - return (int) ( m - (m -p)*scl); - } - - private float vbase(float p, float scl) + private boolean touchMouseEvent(MotionEvent event) { - return (int) (p*scl ); - } - - private boolean isTablet() { - return (context.getResources().getConfiguration().screenLayout - & Configuration.SCREENLAYOUT_SIZE_MASK) - >= Configuration.SCREENLAYOUT_SIZE_LARGE; - } - - public void layout(int width, int height) - { - //dcpx/cm = dcpx/px * px/cm - float magic = isTablet() ? 0.8f : 0.7f; - float scl = 480.0f / height * context.getResources().getDisplayMetrics().density * magic; - float scl_dc = height / 480.0f; - float tx = (width - 640.0f * scl_dc) / 2 / scl_dc; - - float a_x = -tx + 24 * scl; - float a_y = -24 * scl; - - // Not sure how this can happen - if (vjoy_d_custom == null) - return; - - float[][] vjoy_d = VJoy.getVjoy_d(vjoy_d_custom); - - for (int i=0;i vjoy[j][0] && x <= (vjoy[j][0] + vjoy[j][2]) - && y > vjoy[j][1] && y <= (vjoy[j][1] + vjoy[j][3])) - { - if (vjoy[j][4] >= VJoy.BTN_RTRIG) { - // Not for analog - if (vjoy[j][5] == 0) - if (!editVjoyMode) { - vibratorThread.click(); - } - vjoy[j][5] = 2; - } - - - if (vjoy[j][4] == VJoy.BTN_ANARING) { - if (editVjoyMode) { - selectedVjoyElement = VJoy.ELEM_ANALOG; - resetEditMode(); - } else { - vjoy[j + 1][0] = x - vjoy[j + 1][2] / 2; - vjoy[j + 1][1] = y - vjoy[j + 1][3] / 2; - - JNIdc.vjoy(j + 1, vjoy[j + 1][0], vjoy[j + 1][1], vjoy[j + 1][2], vjoy[j + 1][3]); - anal_id = event.getPointerId(i); - } - } else if (vjoy[j][4] != VJoy.BTN_ANAPOINT) { - if (vjoy[j][4] == VJoy.BTN_LTRIG) { - if (editVjoyMode) { - selectedVjoyElement = VJoy.ELEM_LTRIG; - resetEditMode(); - } else { - left_trigger = 255; - lt_id = event.getPointerId(i); - } - } else if (vjoy[j][4] == VJoy.BTN_RTRIG) { - if (editVjoyMode) { - selectedVjoyElement = VJoy.ELEM_RTRIG; - resetEditMode(); - } else { - right_trigger = 255; - rt_id = event.getPointerId(i); - } - } else { - if (editVjoyMode) { - selectedVjoyElement = getElementIdFromButtonId(j); - resetEditMode(); - } else if (vjoy[j][4] == VJoy.key_CONT_FFORWARD) - fastForward = true; - else - rv &= ~(int)vjoy[j][4]; - } - } - } - } - } else if (vjoy_enabled[11]) { - // Analog stick - if (x < vjoy[11][0]) - x = vjoy[11][0]; - else if (x > (vjoy[11][0] + vjoy[11][2])) - x = vjoy[11][0] + vjoy[11][2]; - - if (y < vjoy[11][1]) - y = vjoy[11][1]; - else if (y > (vjoy[11][1] + vjoy[11][3])) - y = vjoy[11][1] + vjoy[11][3]; - - int j = 11; - vjoy[j + 1][0] = x - vjoy[j + 1][2] / 2; - vjoy[j + 1][1] = y - vjoy[j + 1][3] / 2; - - JNIdc.vjoy(j + 1, vjoy[j + 1][0], vjoy[j + 1][1], vjoy[j + 1][2], vjoy[j + 1][3]); - - } - } - - for (int j = 0; j < vjoy.length; j++) { - if (vjoy[j][5] == 2) - vjoy[j][5] = 1; - else if (vjoy[j][5] == 1) - vjoy[j][5] = 0; - } - } - - switch(aid) + int actionIndex = event.getActionIndex(); + switch (event.getActionMasked()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: - selectedVjoyElement = -1; - reset_analog(); - anal_id = -1; - rv = 0xFFFFFFFF; - fastForward = false; - right_trigger = 0; - left_trigger = 0; - lt_id = -1; - rt_id = -1; - for (int j= 0 ;j < vjoy.length; j++) - vjoy[j][5] = 0; - mouse_btns = 0; + // Release all + pidToControlId.clear(); + joyPointerId = -1; + InputDeviceManager.getInstance().virtualReleaseAll(); break; - case MotionEvent.ACTION_POINTER_UP: - if (event.getPointerId(event.getActionIndex())==anal_id) - { - reset_analog(); - anal_id = -1; - } - else if (event.getPointerId(event.getActionIndex())==lt_id) - { - left_trigger = 0; - lt_id = -1; - } - else if (event.getPointerId(event.getActionIndex())==rt_id) + case MotionEvent.ACTION_DOWN: + // First release all + pidToControlId.clear(); + joyPointerId = -1; + InputDeviceManager.getInstance().virtualReleaseAll(); + // Release the mouse too + mousePid = -1; + mouseButtons = 0; + InputDeviceManager.getInstance().mouseEvent(mousePos[0], mousePos[1], mouseButtons); + // Then fall through + case MotionEvent.ACTION_POINTER_DOWN: + { + Point p = new Point(event.getX(actionIndex), event.getY(actionIndex)); + p = translateCoords(p, new Point(width, height)); + int control = VGamepad.hitTest(p.x, p.y); + if (control != -1) { - right_trigger = 0; - rt_id = -1; + int pid = event.getPointerId(actionIndex); + if (control == CTLID_ANARING || control == CTLID_ANASTICK) + { + if (joyPointerId == -1) + { + // Analog stick down + joyPointerId = pid; + joyBiasX = p.x; + joyBiasY = p.y; + InputDeviceManager.getInstance().virtualJoystick(0, 0); + return true; + } + } + else + { + // Button down + InputDeviceManager.getInstance().virtualButtonInput(control, true); + pidToControlId.put(pid, control); + vibratorThread.click(); + return true; + } } break; + } - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_DOWN: - if (event.getPointerCount() != 1) - { - mouse_btns = 0; - } - else + case MotionEvent.ACTION_MOVE: + for (int i = 0; i < event.getPointerCount(); i++) { - mouse_pos[0] = Math.round(event.getX()); - mouse_pos[1] = Math.round(event.getY()); - mouse_btns = MotionEvent.BUTTON_PRIMARY; // Mouse left button down + int pid = event.getPointerId(i); + Point p = new Point(event.getX(i), event.getY(i)); + p = translateCoords(p, new Point(width, height)); + if (joyPointerId == pid) + { + // Analog stick + float dx = p.x - joyBiasX; + float dy = p.y - joyBiasY; + float sz = VGamepad.getControlWidth(CTLID_ANASTICK); + dx = Math.max(Math.min(1.f, dx / sz), -1.f); + dy = Math.max(Math.min(1.f, dy / sz), -1.f); + InputDeviceManager.getInstance().virtualJoystick(dx, dy); + continue; + } + // Buttons + int control = VGamepad.hitTest(p.x, p.y); + int oldControl = pidToControlId.containsKey(pid) ? pidToControlId.get(pid) : -1; + if (oldControl == control) + // same button still pressed, or none at all + continue; + if (oldControl != -1) { + // Previous button up + InputDeviceManager.getInstance().virtualButtonInput(oldControl, false); + pidToControlId.remove(pid); + } + if (control != -1 && control != CTLID_ANARING && control != CTLID_ANASTICK) + { + // New button down + InputDeviceManager.getInstance().virtualButtonInput(control, true); + pidToControlId.put(pid, control); + vibratorThread.click(); + } } break; - case MotionEvent.ACTION_MOVE: - if (event.getPointerCount() == 1) + case MotionEvent.ACTION_POINTER_UP: + { + int pid = event.getPointerId(actionIndex); + if (joyPointerId == pid) + { + // Analog up + InputDeviceManager.getInstance().virtualJoystick(0, 0); + joyPointerId = -1; + return true; + } + if (pidToControlId.containsKey(pid)) { - mouse_pos[0] = Math.round(event.getX()); - mouse_pos[1] = Math.round(event.getY()); + // Button up + int controlId = pidToControlId.get(pid); + InputDeviceManager.getInstance().virtualButtonInput(controlId, false); + return true; } break; - } - int joyx = get_anal(11, 0); - int joyy = get_anal(11, 1); - InputDeviceManager.getInstance().virtualGamepadEvent(rv, joyx, joyy, left_trigger, right_trigger, fastForward); - // Only register the mouse event if no virtual gamepad button is down - if (!editVjoyMode && ((rv == 0xFFFFFFFF && left_trigger == 0 && right_trigger == 0 && joyx == 0 && joyy == 0 && !fastForward) - || JNIdc.guiIsOpen())) - InputDeviceManager.getInstance().mouseEvent(mouse_pos[0], mouse_pos[1], mouse_btns); - return(true); - } - - public void setEditVjoyMode(boolean editVjoyMode) { - this.editVjoyMode = editVjoyMode; - selectedVjoyElement = -1; - if (editVjoyMode) { - this.handler.removeCallbacks(hideVGamepadRunnable); - Arrays.fill(vjoy_enabled, true); - } - resetEditMode(); - } - - public void enableVjoy(boolean[] state) { - vjoy_enabled = state; - } - - private class OscOnScaleGestureListener extends - ScaleGestureDetector.SimpleOnScaleGestureListener { - - @Override - public boolean onScale(ScaleGestureDetector detector) { - if (editVjoyMode && selectedVjoyElement != -1) { - vjoy_d_custom[selectedVjoyElement][2] *= detector.getScaleFactor(); - view.requestLayout(); - - return true; } - - return false; } + return touchMouseEvent(event); + } - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - selectedVjoyElement = -1; - } + @Override + public void show() + { + VGamepad.show(); + this.handler.removeCallbacks(hideVGamepadRunnable); + this.handler.postDelayed(hideVGamepadRunnable, 10000); } } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java index a8078f063..09947d944 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java @@ -24,6 +24,8 @@ public final class InputDeviceManager implements InputManager.InputDeviceListene private InputManager inputManager; private int maple_port = 0; + private boolean hasTouchscreen = false; + private static class VibrationParams { float power; float inclination; @@ -39,9 +41,10 @@ public InputDeviceManager() public void startListening(Context applicationContext) { maple_port = 0; - if (applicationContext.getPackageManager().hasSystemFeature("android.hardware.touchscreen")) - joystickAdded(VIRTUAL_GAMEPAD_ID, "Virtual Gamepad", 0, "virtual_gamepad_uid", - new int[0], new int[0], getVibrator(VIRTUAL_GAMEPAD_ID) != null); + hasTouchscreen = applicationContext.getPackageManager().hasSystemFeature("android.hardware.touchscreen"); + if (hasTouchscreen) + joystickAdded(VIRTUAL_GAMEPAD_ID, null, 0, null, + null, null, getVibrator(VIRTUAL_GAMEPAD_ID) != null); int[] ids = InputDevice.getDeviceIds(); for (int id : ids) onInputDeviceAdded(id); @@ -202,12 +205,18 @@ public void stopRumble() } } + public boolean hasTouchscreen() { + return hasTouchscreen; + } + public static InputDeviceManager getInstance() { return INSTANCE; } public native void init(); - public native void virtualGamepadEvent(int kcode, int joyx, int joyy, int lt, int rt, boolean fastForward); + public native void virtualReleaseAll(); + public native void virtualJoystick(float x, float y); + public native void virtualButtonInput(int key, boolean pressed); public native boolean joystickButtonEvent(int id, int button, boolean pressed); public native boolean joystickAxisEvent(int id, int button, int value); public native void mouseEvent(int xpos, int ypos, int buttons); @@ -216,4 +225,5 @@ public static InputDeviceManager getInstance() { private native void joystickRemoved(int id); public native boolean keyboardEvent(int key, boolean pressed); public native void keyboardText(int c); + public static native boolean isMicPluggedIn(); } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/VJoy.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/VJoy.java deleted file mode 100644 index c94950aed..000000000 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/VJoy.java +++ /dev/null @@ -1,219 +0,0 @@ -package com.flycast.emulator.periph; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -public class VJoy { - - public static final int key_CONT_C = 0x0001; - public static final int key_CONT_B = 0x0002; - public static final int key_CONT_A = 0x0004; - public static final int key_CONT_START = 0x0008; - public static final int key_CONT_DPAD_UP = 0x0010; - public static final int key_CONT_DPAD_DOWN = 0x0020; - public static final int key_CONT_DPAD_LEFT = 0x0040; - public static final int key_CONT_DPAD_RIGHT = 0x0080; - public static final int key_CONT_Y = 0x0200; - public static final int key_CONT_X = 0x0400; - public static final int key_CONT_FFORWARD = 0x3000002; - - public static final int BTN_LTRIG = -1; - public static final int BTN_RTRIG = -2; - public static final int BTN_ANARING = -3; - public static final int BTN_ANAPOINT = -4; - - public static final int ELEM_NONE = -1; - public static final int ELEM_DPAD = 0; - public static final int ELEM_BUTTONS = 1; - public static final int ELEM_START = 2; - public static final int ELEM_LTRIG = 3; - public static final int ELEM_RTRIG = 4; - public static final int ELEM_ANALOG = 5; - public static final int ELEM_FFORWARD = 6; - - public static int VJoyCount = 14; - - public static float[][] baseVJoy() { - return new float[][] { - new float[] { 24, 24+64, 64,64, key_CONT_DPAD_LEFT, 0}, - new float[] { 24+64, 24, 64,64, key_CONT_DPAD_UP, 0}, - new float[] { 24+128, 24+64, 64,64, key_CONT_DPAD_RIGHT, 0}, - new float[] { 24+64, 24+128, 64,64, key_CONT_DPAD_DOWN, 0}, - - new float[] { 440, 280+64, 64,64, key_CONT_X, 0}, - new float[] { 440+64, 280, 64,64, key_CONT_Y, 0}, - new float[] { 440+128, 280+64, 64,64, key_CONT_B, 0}, - new float[] { 440+64, 280+128,64,64, key_CONT_A, 0}, - - new float[] { 320-32, 360+32, 64,64, key_CONT_START, 0}, - - new float[] { 440, 200, 90,64, BTN_LTRIG, 0}, // LT - new float[] { 542, 200, 90,64, BTN_RTRIG, 0}, // RT - - new float[] { 0, 128+224,128,128,BTN_ANARING, 0}, // Analog ring - new float[] { 32, 128+256,64,64, BTN_ANAPOINT, 0}, // Analog point - - new float[] { 320-32, 12, 64,64, key_CONT_FFORWARD, 0}, // Fast-forward - - new float[] { 20, 288, 64,64, key_CONT_DPAD_LEFT|key_CONT_DPAD_UP, 0}, // DPad diagonals - new float[] { 20+128, 288, 64,64, key_CONT_DPAD_RIGHT|key_CONT_DPAD_UP, 0}, - new float[] { 20, 288+128,64,64, key_CONT_DPAD_LEFT|key_CONT_DPAD_DOWN, 0}, - new float[] { 20+128, 288+128,64,64, key_CONT_DPAD_RIGHT|key_CONT_DPAD_DOWN, 0}, - }; - } - - public static float[][] readCustomVjoyValues(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - return new float[][] { - // x-shift, y-shift, sizing-factor - new float[] { prefs.getFloat("touch_x_shift_dpad", 0), - prefs.getFloat("touch_y_shift_dpad", 0), - prefs.getFloat("touch_scale_dpad", 1) - }, // DPAD - new float[] { prefs.getFloat("touch_x_shift_buttons", 0), - prefs.getFloat("touch_y_shift_buttons", 0), - prefs.getFloat("touch_scale_buttons", 1) - }, // X, Y, B, A Buttons - new float[] { prefs.getFloat("touch_x_shift_start", 0), - prefs.getFloat("touch_y_shift_start", 0), - prefs.getFloat("touch_scale_start", 1) - }, // Start - new float[] { prefs.getFloat("touch_x_shift_left_trigger", 0), - prefs.getFloat("touch_y_shift_left_trigger", 0), - prefs.getFloat("touch_scale_left_trigger", 1) - }, // Left Trigger - new float[] { prefs.getFloat("touch_x_shift_right_trigger", 0), - prefs.getFloat("touch_y_shift_right_trigger", 0), - prefs.getFloat("touch_scale_right_trigger", 1) - }, // Right Trigger - new float[] { prefs.getFloat("touch_x_shift_analog", 0), - prefs.getFloat("touch_y_shift_analog", 0), - prefs.getFloat("touch_scale_analog", 1) - }, // Analog Stick - new float[] { prefs.getFloat("touch_x_shift_fforward", 0), - prefs.getFloat("touch_y_shift_fforward", 0), - prefs.getFloat("touch_scale_fforward", 1) - } // Fast-forward - }; - } - - public static float[][] getVjoy_d(float[][] vjoy_d_custom) { - return new float[][] { - // LEFT, UP, RIGHT, DOWN - new float[] { 20+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+64*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_LEFT}, - new float[] { 20+64*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_UP}, - new float[] { 20+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+64*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_RIGHT}, - new float[] { 20+64*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_DOWN}, - - // X, Y, B, A - new float[] { 448+0*vjoy_d_custom[1][2]+vjoy_d_custom[1][0], 288+64*vjoy_d_custom[1][2]+vjoy_d_custom[1][1], - 64*vjoy_d_custom[1][2],64*vjoy_d_custom[1][2], key_CONT_X}, - new float[] { 448+64*vjoy_d_custom[1][2]+vjoy_d_custom[1][0], 288+0*vjoy_d_custom[1][2]+vjoy_d_custom[1][1], - 64*vjoy_d_custom[1][2],64*vjoy_d_custom[1][2], key_CONT_Y}, - new float[] { 448+128*vjoy_d_custom[1][2]+vjoy_d_custom[1][0], 288+64*vjoy_d_custom[1][2]+vjoy_d_custom[1][1], - 64*vjoy_d_custom[1][2],64*vjoy_d_custom[1][2], key_CONT_B}, - new float[] { 448+64*vjoy_d_custom[1][2]+vjoy_d_custom[1][0], 288+128*vjoy_d_custom[1][2]+vjoy_d_custom[1][1], - 64*vjoy_d_custom[1][2],64*vjoy_d_custom[1][2], key_CONT_A}, - - // START - new float[] { 320-32+vjoy_d_custom[2][0], 288+128+vjoy_d_custom[2][1], - 64*vjoy_d_custom[2][2],64*vjoy_d_custom[2][2], key_CONT_START}, - - // LT, RT - new float[] { 440+vjoy_d_custom[3][0], 200+vjoy_d_custom[3][1], - 90*vjoy_d_custom[3][2],64*vjoy_d_custom[3][2], -1}, - new float[] { 542+vjoy_d_custom[4][0], 200+vjoy_d_custom[4][1], - 90*vjoy_d_custom[4][2],64*vjoy_d_custom[4][2], -2}, - - // Analog ring and point - new float[] { 16+vjoy_d_custom[5][0], 24+32+vjoy_d_custom[5][1], - 128*vjoy_d_custom[5][2],128*vjoy_d_custom[5][2],-3}, - new float[] { 48+vjoy_d_custom[5][0], 24+64+vjoy_d_custom[5][1], - 64*vjoy_d_custom[5][2],64*vjoy_d_custom[5][2], -4}, - - // Fast-forward - new float[] { 320-32+vjoy_d_custom[6][0], 12+vjoy_d_custom[6][1], - 64*vjoy_d_custom[6][2],64*vjoy_d_custom[6][2], -5}, - - // DPad diagonals - new float[] { 20+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_LEFT|key_CONT_DPAD_UP}, - new float[] { 20+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_RIGHT|key_CONT_DPAD_UP}, - new float[] { 20+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_LEFT|key_CONT_DPAD_DOWN}, - new float[] { 20+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_RIGHT|key_CONT_DPAD_DOWN}, - }; - } - - public static void writeCustomVjoyValues(float[][] vjoy_d_custom, Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - prefs.edit().putFloat("touch_x_shift_dpad", vjoy_d_custom[0][0]).apply(); - prefs.edit().putFloat("touch_y_shift_dpad", vjoy_d_custom[0][1]).apply(); - prefs.edit().putFloat("touch_scale_dpad", vjoy_d_custom[0][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_buttons", vjoy_d_custom[1][0]).apply(); - prefs.edit().putFloat("touch_y_shift_buttons", vjoy_d_custom[1][1]).apply(); - prefs.edit().putFloat("touch_scale_buttons", vjoy_d_custom[1][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_start", vjoy_d_custom[2][0]).apply(); - prefs.edit().putFloat("touch_y_shift_start", vjoy_d_custom[2][1]).apply(); - prefs.edit().putFloat("touch_scale_start", vjoy_d_custom[2][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_left_trigger", vjoy_d_custom[3][0]).apply(); - prefs.edit().putFloat("touch_y_shift_left_trigger", vjoy_d_custom[3][1]).apply(); - prefs.edit().putFloat("touch_scale_left_trigger", vjoy_d_custom[3][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_right_trigger", vjoy_d_custom[4][0]).apply(); - prefs.edit().putFloat("touch_y_shift_right_trigger", vjoy_d_custom[4][1]).apply(); - prefs.edit().putFloat("touch_scale_right_trigger", vjoy_d_custom[4][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_analog", vjoy_d_custom[5][0]).apply(); - prefs.edit().putFloat("touch_y_shift_analog", vjoy_d_custom[5][1]).apply(); - prefs.edit().putFloat("touch_scale_analog", vjoy_d_custom[5][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_fforward", vjoy_d_custom[6][0]).apply(); - prefs.edit().putFloat("touch_y_shift_fforward", vjoy_d_custom[6][1]).apply(); - prefs.edit().putFloat("touch_scale_fforward", vjoy_d_custom[6][2]).apply(); - } - - public static void resetCustomVjoyValues(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - prefs.edit().remove("touch_x_shift_dpad").apply(); - prefs.edit().remove("touch_y_shift_dpad").apply(); - prefs.edit().remove("touch_scale_dpad").apply(); - - prefs.edit().remove("touch_x_shift_buttons").apply(); - prefs.edit().remove("touch_y_shift_buttons").apply(); - prefs.edit().remove("touch_scale_buttons").apply(); - - prefs.edit().remove("touch_x_shift_start").apply(); - prefs.edit().remove("touch_y_shift_start").apply(); - prefs.edit().remove("touch_scale_start").apply(); - - prefs.edit().remove("touch_x_shift_left_trigger").apply(); - prefs.edit().remove("touch_y_shift_left_trigger").apply(); - prefs.edit().remove("touch_scale_left_trigger").apply(); - - prefs.edit().remove("touch_x_shift_right_trigger").apply(); - prefs.edit().remove("touch_y_shift_right_trigger").apply(); - prefs.edit().remove("touch_scale_right_trigger").apply(); - - prefs.edit().remove("touch_x_shift_analog").apply(); - prefs.edit().remove("touch_y_shift_analog").apply(); - prefs.edit().remove("touch_scale_analog").apply(); - - prefs.edit().remove("touch_x_shift_fforward").apply(); - prefs.edit().remove("touch_y_shift_fforward").apply(); - prefs.edit().remove("touch_scale_fforward").apply(); - } -} diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index 3fa0fabc8..50bbd45e3 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -1,12 +1,8 @@ #include "types.h" -#include "hw/maple/maple_cfg.h" -#include "hw/maple/maple_devs.h" -#include "hw/maple/maple_if.h" #include "hw/naomi/naomi_cart.h" #include "audio/audiostream.h" #include "imgread/common.h" #include "ui/gui.h" -#include "ui/vgamepad.h" #include "rend/osd.h" #include "cfg/cfg.h" #include "log/LogManager.h" @@ -39,24 +35,14 @@ namespace jni thread_local JVMAttacher jvm_attacher; } -#include "android_gamepad.h" -#include "android_keyboard.h" #include "http_client.h" -extern "C" JNIEXPORT jint JNICALL Java_com_flycast_emulator_emu_JNIdc_getVirtualGamepadVibration(JNIEnv *env, jobject obj) -{ - return (jint)config::VirtualGamepadVibration; -} - extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_screenCharacteristics(JNIEnv *env, jobject obj, jfloat screenDpi, jfloat refreshRate) { settings.display.dpi = screenDpi; settings.display.refreshRate = refreshRate; } -std::shared_ptr mouse; -std::shared_ptr keyboard; - static bool game_started; //stuff for saving prefs @@ -65,12 +51,10 @@ jmethodID saveAndroidSettingsMid; static ANativeWindow *g_window = 0; // Activity -static jobject g_activity; -static jmethodID VJoyStartEditingMID; -static jmethodID VJoyStopEditingMID; -static jmethodID VJoyResetEditingMID; -static jmethodID VJoyEnableControlsMID; -static jmethodID showScreenKeyboardMid; +jobject g_activity; +extern jmethodID VJoyStartEditingMID; +extern jmethodID VJoyStopEditingMID; +extern jmethodID showScreenKeyboardMid; static jmethodID onGameStateChangeMid; static void emuEventCallback(Event event, void *) @@ -353,42 +337,6 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_rendinitNa } } -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_vjoy(JNIEnv * env, jobject obj,int id,float x, float y, float w, float h) -{ - vgamepad::setPosition(static_cast(id), x, y, w, h); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_showVirtualGamepad(JNIEnv * env, jobject obj) -{ - vgamepad::show(); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_hideVirtualGamepad(JNIEnv * env, jobject obj) -{ - vgamepad::hide(); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_getControllers(JNIEnv *env, jobject obj, jintArray controllers, jobjectArray peripherals) -{ - // might be called before JNIdc.initEnvironment() - if (g_jvm == NULL) - env->GetJavaVM(&g_jvm); - - jni::IntArray jcontrollers(controllers, false); - std::vector devs; - for (u32 i = 0; i < config::MapleMainDevices.size(); i++) - devs.push_back((MapleDeviceType)config::MapleMainDevices[i]); - jcontrollers.setData(devs.data()); - - jni::ObjectArray jperipherals(peripherals, false); - int obj_len = jperipherals.size(); - for (int i = 0; i < obj_len; ++i) - { - std::vector devs { (MapleDeviceType)config::MapleExpansionDevices[i][0], (MapleDeviceType)config::MapleExpansionDevices[i][1] }; - jperipherals[i].setData(devs.data()); - } -} - extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_guiOpenSettings(JNIEnv *env, jobject obj) { gui_open_settings(); @@ -495,92 +443,6 @@ void SaveAndroidSettings() jni::env()->CallVoidMethod(g_emulator, saveAndroidSettingsMid, (jstring)homeDirectory); } -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_init(JNIEnv *env, jobject obj) -{ - input_device_manager = env->NewGlobalRef(obj); - input_device_manager_rumble = env->GetMethodID(env->GetObjectClass(obj), "rumble", "(IFFI)Z"); - // FIXME Don't connect it by default or any screen touch will register as button A press - mouse = std::make_shared(-1); - GamepadDevice::Register(mouse); - keyboard = std::make_shared(); - GamepadDevice::Register(keyboard); - gui_setOnScreenKeyboardCallback([](bool show) { - if (g_activity != nullptr) - jni::env()->CallVoidMethod(g_activity, showScreenKeyboardMid, show); - }); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickAdded(JNIEnv *env, jobject obj, jint id, jstring name, - jint maple_port, jstring junique_id, jintArray fullAxes, jintArray halfAxes, jboolean hasRumble) -{ - std::string joyname = jni::String(name, false); - std::string unique_id = jni::String(junique_id, false); - std::vector full = jni::IntArray(fullAxes, false); - std::vector half = jni::IntArray(halfAxes, false); - - std::shared_ptr gamepad = std::make_shared(maple_port, id, joyname.c_str(), unique_id.c_str(), full, half); - AndroidGamepadDevice::AddAndroidGamepad(gamepad); - gamepad->setRumbleEnabled(hasRumble); -} -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickRemoved(JNIEnv *env, jobject obj, jint id) -{ - std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); - if (device != NULL) - AndroidGamepadDevice::RemoveAndroidGamepad(device); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_virtualGamepadEvent(JNIEnv *env, jobject obj, jint kcode, jint joyx, jint joyy, jint lt, jint rt, jboolean fastForward) -{ - std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(AndroidGamepadDevice::VIRTUAL_GAMEPAD_ID); - if (device != NULL) - device->virtual_gamepad_event(kcode, joyx, joyy, lt, rt, fastForward); -} - -extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickButtonEvent(JNIEnv *env, jobject obj, jint id, jint key, jboolean pressed) -{ - std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); - if (device != NULL) - return device->gamepad_btn_input(key, pressed); - else - return false; - -} - -extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_keyboardEvent(JNIEnv *env, jobject obj, jint key, jboolean pressed) -{ - keyboard->input(key, pressed); - return true; -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_keyboardText(JNIEnv *env, jobject obj, jint c) -{ - gui_keyboard_input((u16)c); -} - -static std::map, jint> previous_axis_values; - -extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickAxisEvent(JNIEnv *env, jobject obj, jint id, jint key, jint value) -{ - std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); - if (device != nullptr) - return device->gamepad_axis_input(key, value); - else - return false; -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_mouseEvent(JNIEnv *env, jobject obj, jint xpos, jint ypos, jint buttons) -{ - mouse->setAbsPos(xpos, ypos, settings.display.width, settings.display.height); - mouse->setButton(Mouse::LEFT_BUTTON, (buttons & 1) != 0); - mouse->setButton(Mouse::RIGHT_BUTTON, (buttons & 2) != 0); - mouse->setButton(Mouse::MIDDLE_BUTTON, (buttons & 4) != 0); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_mouseScrollEvent(JNIEnv *env, jobject obj, jint scrollValue) -{ - mouse->setWheel(scrollValue); -} - extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_BaseGLActivity_register(JNIEnv *env, jobject obj, jobject activity) { if (g_activity != nullptr) { @@ -592,40 +454,12 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_BaseGLActivity_regis g_activity = env->NewGlobalRef(activity); jclass actClass = env->GetObjectClass(activity); VJoyStartEditingMID = env->GetMethodID(actClass, "VJoyStartEditing", "()V"); - VJoyStopEditingMID = env->GetMethodID(actClass, "VJoyStopEditing", "(Z)V"); - VJoyResetEditingMID = env->GetMethodID(actClass, "VJoyResetEditing", "()V"); - VJoyEnableControlsMID = env->GetMethodID(actClass, "VJoyEnableControls", "([Z)V"); + VJoyStopEditingMID = env->GetMethodID(actClass, "VJoyStopEditing", "()V"); showScreenKeyboardMid = env->GetMethodID(actClass, "showScreenKeyboard", "(Z)V"); onGameStateChangeMid = env->GetMethodID(actClass, "onGameStateChange", "(Z)V"); } } -namespace vgamepad -{ - -void startEditing() { - enableAllControls(); - jni::env()->CallVoidMethod(g_activity, VJoyStartEditingMID); -} -void pauseEditing() { - stopEditing(false); -} -void resetEditing() { - jni::env()->CallVoidMethod(g_activity, VJoyResetEditingMID); -} -void stopEditing(bool canceled) { - jni::env()->CallVoidMethod(g_activity, VJoyStopEditingMID, canceled); -} - -void setEnabledControls(bool enabled[_Count]) -{ - jni::BooleanArray jb{_Count}; - jb.setData(enabled, 0, _Count); - jni::env()->CallVoidMethod(g_activity, VJoyEnableControlsMID, (jbooleanArray)jb); -} - -} - void enableNetworkBroadcast(bool enable) { JNIEnv *env = jni::env(); diff --git a/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h b/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h index 2208b741c..79d1cf985 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h +++ b/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h @@ -20,6 +20,7 @@ #include "input/gamepad_device.h" #include "input/mouse.h" +#include "input/virtual_gamepad.h" #include "jni_util.h" #include @@ -97,26 +98,18 @@ class AndroidGamepadDevice : public GamepadDevice public: AndroidGamepadDevice(int maple_port, int id, const char *name, const char *unique_id, const std::vector& fullAxes, const std::vector& halfAxes) - : GamepadDevice(maple_port, "Android", id != VIRTUAL_GAMEPAD_ID), android_id(id), + : GamepadDevice(maple_port, "Android"), android_id(id), fullAxes(fullAxes), halfAxes(halfAxes) { _name = name; _unique_id = unique_id; INFO_LOG(INPUT, "Android: Opened joystick %d on port %d: '%s' descriptor '%s'", id, maple_port, _name.c_str(), _unique_id.c_str()); - if (id == VIRTUAL_GAMEPAD_ID) - { - input_mapper = std::make_shared(); - // hasAnalogStick = true; // TODO has an analog stick but input mapping isn't persisted - } - else - { - loadMapping(); - save_mapping(); - hasAnalogStick = !fullAxes.empty(); - } + + loadMapping(); + save_mapping(); + hasAnalogStick = !fullAxes.empty(); } - ~AndroidGamepadDevice() override - { + ~AndroidGamepadDevice() override { INFO_LOG(INPUT, "Android: Joystick '%s' on port %d disconnected", _name.c_str(), maple_port()); } @@ -247,66 +240,6 @@ class AndroidGamepadDevice : public GamepadDevice GamepadDevice::Unregister(gamepad); }; - void virtual_gamepad_event(int kcode, int joyx, int joyy, int lt, int rt, bool fastForward) - { - // No virtual gamepad when the GUI is open: touch events only - if (gui_is_open() && gui_state != GuiState::VJoyEdit) - { - kcode = 0xffffffff; - joyx = joyy = rt = lt = 0; - } - if (settings.platform.isArcade()) - { - if (rt > 0) - { - if ((kcode & DC_BTN_A) == 0) - // RT + A -> D (coin) - kcode &= ~DC_BTN_D; - if ((kcode & DC_BTN_B) == 0) - // RT + B -> Service - kcode &= ~DC_DPAD2_UP; - if ((kcode & DC_BTN_X) == 0) - // RT + X -> Test - kcode &= ~DC_DPAD2_DOWN; - } - // arcade mapping: X -> btn2, Y -> btn3 - if ((kcode & DC_BTN_X) == 0) - { - kcode &= ~DC_BTN_C; - kcode |= DC_BTN_X; - } - if ((kcode & DC_BTN_Y) == 0) - { - kcode &= ~DC_BTN_X; - kcode |= DC_BTN_Y; - } - if (rt > 0) - // naomi btn4 - kcode &= ~DC_BTN_Y; - if (lt > 0) - // naomi btn5 - kcode &= ~DC_BTN_Z; - } - u32 changes = kcode ^ previous_kcode; - for (int i = 0; i < 32; i++) - if (changes & (1 << i)) - gamepad_btn_input(1 << i, (kcode & (1 << i)) == 0); - if (joyx >= 0) - gamepad_axis_input(DC_AXIS_RIGHT, joyx | (joyx << 8)); - else - gamepad_axis_input(DC_AXIS_LEFT, -joyx | (-joyx << 8)); - if (joyy >= 0) - gamepad_axis_input(DC_AXIS_DOWN, joyy | (joyy << 8)); - else - gamepad_axis_input(DC_AXIS_UP, -joyy | (-joyy << 8)); - gamepad_axis_input(DC_AXIS_LT, lt == 0 ? 0 : 0x7fff); - gamepad_axis_input(DC_AXIS_RT, rt == 0 ? 0 : 0x7fff); - previous_kcode = kcode; - if (fastForward != previousFastForward) - gamepad_btn_input(EMU_BTN_FFORWARD, fastForward); - previousFastForward = fastForward; - } - void rumble(float power, float inclination, u32 duration_ms) override { power *= rumblePower / 100.f; @@ -317,8 +250,6 @@ class AndroidGamepadDevice : public GamepadDevice this->rumbleEnabled = rumbleEnabled; } - bool is_virtual_gamepad() override { return android_id == VIRTUAL_GAMEPAD_ID; } - bool hasHalfAxis(int axis) const { return std::find(halfAxes.begin(), halfAxes.end(), axis) != halfAxes.end(); } bool hasFullAxis(int axis) const { return std::find(fullAxes.begin(), fullAxes.end(), axis) != fullAxes.end(); } @@ -336,13 +267,9 @@ class AndroidGamepadDevice : public GamepadDevice input_mapper = std::make_shared>(*this); } - static const int VIRTUAL_GAMEPAD_ID = 0x12345678; // must match the Java definition - private: int android_id; static std::map> android_gamepads; - u32 previous_kcode = 0xffffffff; - bool previousFastForward = false; std::vector fullAxes; std::vector halfAxes; }; @@ -481,3 +408,19 @@ class AndroidMouse : public SystemMouse } }; +class AndroidVirtualGamepad : public VirtualGamepad +{ +public: + AndroidVirtualGamepad(bool rumbleEnabled) : VirtualGamepad("Android") { + this->rumbleEnabled = rumbleEnabled; + } + + void rumble(float power, float inclination, u32 duration_ms) override + { + power *= rumblePower / 100.f; + jboolean has_vibrator = jni::env()->CallBooleanMethod(input_device_manager, input_device_manager_rumble, GAMEPAD_ID, power, inclination, duration_ms); + rumbleEnabled = has_vibrator; + } + + static constexpr int GAMEPAD_ID = 0x12345678; // must match the Java definition +}; diff --git a/shell/android-studio/flycast/src/main/jni/src/android_input.cpp b/shell/android-studio/flycast/src/main/jni/src/android_input.cpp new file mode 100644 index 000000000..2fde7d1a7 --- /dev/null +++ b/shell/android-studio/flycast/src/main/jni/src/android_input.cpp @@ -0,0 +1,225 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#include "android_gamepad.h" +#include "android_keyboard.h" +#include "ui/vgamepad.h" +#include "cfg/option.h" +#include "hw/maple/maple_if.h" + +std::shared_ptr mouse; +std::shared_ptr keyboard; +std::shared_ptr virtualGamepad; + +extern jobject g_activity; +jmethodID VJoyStartEditingMID; +jmethodID VJoyStopEditingMID; +jmethodID VJoyEnableControlsMID; +jmethodID showScreenKeyboardMid; + +// +// VGamepad +// +extern "C" JNIEXPORT jint JNICALL Java_com_flycast_emulator_emu_VGamepad_getVibrationPower(JNIEnv *env, jobject obj) { + return (jint)config::VirtualGamepadVibration; +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_VGamepad_show(JNIEnv * env, jobject obj) { + vgamepad::show(); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_VGamepad_hide(JNIEnv * env, jobject obj) { + vgamepad::hide(); +} + +extern "C" JNIEXPORT jint JNICALL Java_com_flycast_emulator_emu_VGamepad_hitTest(JNIEnv * env, jobject obj, + jfloat x, jfloat y) { + return vgamepad::hitTest(x, y); +} + +extern "C" JNIEXPORT jfloat JNICALL Java_com_flycast_emulator_emu_VGamepad_getControlWidth(JNIEnv * env, jobject obj, + jint controlId) { + return vgamepad::getControlWidth(static_cast(controlId)); +} + +extern "C" JNIEXPORT jint JNICALL Java_com_flycast_emulator_emu_VGamepad_layoutHitTest(JNIEnv * env, jobject obj, + jfloat x, jfloat y) { + return vgamepad::layoutHitTest(x, y); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_VGamepad_scaleElement(JNIEnv * env, jobject obj, + jint elemId, jfloat scale) { + vgamepad::scaleElement(static_cast(elemId), scale); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_VGamepad_translateElement(JNIEnv * env, jobject obj, + jint elemId, jfloat x, jfloat y) { + vgamepad::translateElement(static_cast(elemId), x, y); +} + +namespace vgamepad +{ + +void startEditing() +{ + // FIXME code dup with vgamepad.cpp + enableAllControls(); + show(); + jni::env()->CallVoidMethod(g_activity, VJoyStartEditingMID); +} +void pauseEditing() { + // needed? could be used by iOS to avoid relying on gui state + jni::env()->CallVoidMethod(g_activity, VJoyStopEditingMID); +} +void stopEditing(bool canceled) +{ + // FIXME code dup with vgamepad.cpp + jni::env()->CallVoidMethod(g_activity, VJoyStopEditingMID); + if (canceled) + loadLayout(); + else + saveLayout(); +} + +} + +// +// InputDeviceManager +// +extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_isMicPluggedIn(JNIEnv *env, jobject obj) +{ + for (const auto& devices : config::MapleExpansionDevices) + if (static_cast(devices[0]) == MDT_Microphone + || static_cast(devices[1]) == MDT_Microphone) + return true; + + return false; +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_init(JNIEnv *env, jobject obj) +{ + input_device_manager = env->NewGlobalRef(obj); + input_device_manager_rumble = env->GetMethodID(env->GetObjectClass(obj), "rumble", "(IFFI)Z"); + // FIXME Don't connect it by default or any screen touch will register as button A press + mouse = std::make_shared(-1); + GamepadDevice::Register(mouse); + keyboard = std::make_shared(); + GamepadDevice::Register(keyboard); + gui_setOnScreenKeyboardCallback([](bool show) { + if (g_activity != nullptr) + jni::env()->CallVoidMethod(g_activity, showScreenKeyboardMid, show); + }); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickAdded(JNIEnv *env, jobject obj, + jint id, jstring name, jint maple_port, jstring junique_id, jintArray fullAxes, jintArray halfAxes, jboolean hasRumble) +{ + if (id == AndroidVirtualGamepad::GAMEPAD_ID) { + virtualGamepad = std::make_shared(hasRumble); + GamepadDevice::Register(virtualGamepad); + } + else + { + std::string joyname = jni::String(name, false); + std::string unique_id = jni::String(junique_id, false); + std::vector full = jni::IntArray(fullAxes, false); + std::vector half = jni::IntArray(halfAxes, false); + + std::shared_ptr gamepad = std::make_shared(maple_port, id, joyname.c_str(), unique_id.c_str(), full, half); + AndroidGamepadDevice::AddAndroidGamepad(gamepad); + gamepad->setRumbleEnabled(hasRumble); + } +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickRemoved(JNIEnv *env, jobject obj, + jint id) +{ + if (id == AndroidVirtualGamepad::GAMEPAD_ID) { + GamepadDevice::Unregister(virtualGamepad); + virtualGamepad.reset(); + } + else { + std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); + if (device) + AndroidGamepadDevice::RemoveAndroidGamepad(device); + } +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_virtualReleaseAll(JNIEnv *env, jobject obj) { + if (virtualGamepad) + virtualGamepad->releaseAll(); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_virtualJoystick(JNIEnv *env, jobject obj, + jfloat x, jfloat y) { + if (virtualGamepad) + virtualGamepad->joystickInput(x, y); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_virtualButtonInput(JNIEnv *env, jobject obj, + jint controlId, jboolean pressed) { + if (virtualGamepad) + virtualGamepad->buttonInput(static_cast(controlId), pressed); +} + +extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickButtonEvent(JNIEnv *env, jobject obj, + jint id, jint key, jboolean pressed) +{ + std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); + if (device != NULL) + return device->gamepad_btn_input(key, pressed); + else + return false; +} + +extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_keyboardEvent(JNIEnv *env, jobject obj, + jint key, jboolean pressed) { + keyboard->input(key, pressed); + return true; +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_keyboardText(JNIEnv *env, jobject obj, + jint c) { + gui_keyboard_input((u16)c); +} + +static std::map, jint> previous_axis_values; + +extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickAxisEvent(JNIEnv *env, jobject obj, + jint id, jint key, jint value) +{ + std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); + if (device != nullptr) + return device->gamepad_axis_input(key, value); + else + return false; +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_mouseEvent(JNIEnv *env, jobject obj, + jint xpos, jint ypos, jint buttons) +{ + mouse->setAbsPos(xpos, ypos, settings.display.width, settings.display.height); + mouse->setButton(Mouse::LEFT_BUTTON, (buttons & 1) != 0); + mouse->setButton(Mouse::RIGHT_BUTTON, (buttons & 2) != 0); + mouse->setButton(Mouse::MIDDLE_BUTTON, (buttons & 4) != 0); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_mouseScrollEvent(JNIEnv *env, jobject obj, + jint scrollValue) { + mouse->setWheel(scrollValue); +} diff --git a/shell/android-studio/flycast/src/main/jni/src/jni_util.h b/shell/android-studio/flycast/src/main/jni/src/jni_util.h index 2fc0b992a..73e57034d 100644 --- a/shell/android-studio/flycast/src/main/jni/src/jni_util.h +++ b/shell/android-studio/flycast/src/main/jni/src/jni_util.h @@ -107,7 +107,7 @@ class Object bool isNull() const { return object == nullptr; } operator jobject() const { return object; } - Class getClass() const; + inline Class getClass() const; template T globalRef() { diff --git a/shell/apple/emulator-ios/emulator/PadViewController.mm b/shell/apple/emulator-ios/emulator/PadViewController.mm index 19b678ee5..97dedb34f 100644 --- a/shell/apple/emulator-ios/emulator/PadViewController.mm +++ b/shell/apple/emulator-ios/emulator/PadViewController.mm @@ -65,7 +65,7 @@ - (void)showController:(UIView *)parentView - (void)hideController { - [self resetTouch]; + [self resetAnalog]; [hideTimer invalidate]; [self.view removeFromSuperview]; } @@ -89,12 +89,10 @@ -(void)hideTimer { vgamepad::hide(); } -- (void)resetTouch +- (void)resetAnalog { joyTouch = nil; - virtualGamepad->gamepad_axis_input(DC_AXIS_LEFT, 0); - virtualGamepad->gamepad_axis_input(DC_AXIS_UP, 0); - vgamepad::setAnalogStick(0, 0); + virtualGamepad->joystickInput(0, 0); } static CGPoint translateCoords(const CGPoint& pos, const CGSize& size) @@ -114,20 +112,20 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; CGPoint point = [touch locationInView:self.view]; point = translateCoords(point, self.view.bounds.size); vgamepad::ControlId control = vgamepad::hitTest(point.x, point.y); - if (joyTouch == nil && control == vgamepad::AnalogArea) + if (joyTouch == nil && (control == vgamepad::AnalogArea || control == vgamepad::AnalogStick)) { - [self resetTouch]; + [self resetAnalog]; joyTouch = touch; joyBias = point; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; if (control != vgamepad::None && control != vgamepad::AnalogArea - && touchToButton[key] == nil) + && control != vgamepad::AnalogStick && touchToButton[key] == nil) { touchToButton[key] = [NSNumber numberWithInt:control]; // button down - virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey(control), true); + virtualGamepad->buttonInput(control, true); } } [super touchesBegan:touches withEvent:event]; @@ -138,7 +136,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; for (UITouch *touch in touches) { if (touch == joyTouch) { - [self resetTouch]; + [self resetAnalog]; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; @@ -146,7 +144,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; if (control != nil) { [touchToButton removeObjectForKey:key]; // button up - virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey((vgamepad::ControlId)control.intValue), false); + virtualGamepad->buttonInput(static_cast(control.intValue), false); } } [super touchesEnded:touches withEvent:event]; @@ -166,17 +164,7 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; double sz = vgamepad::getControlWidth(vgamepad::AnalogStick); point.x = std::max(std::min(1.0, point.x / sz), -1.0); point.y = std::max(std::min(1.0, point.y / sz), -1.0); - vgamepad::setAnalogStick(point.x, point.y); - point.x *= 32767.0; - point.y *= 32767.0; - if (point.x >= 0) - virtualGamepad->gamepad_axis_input(DC_AXIS_RIGHT, (int)std::round(point.x)); - else - virtualGamepad->gamepad_axis_input(DC_AXIS_LEFT, -(int)std::round(point.x)); - if (point.y >= 0) - virtualGamepad->gamepad_axis_input(DC_AXIS_DOWN, (int)std::round(point.y)); - else - virtualGamepad->gamepad_axis_input(DC_AXIS_UP, -(int)std::round(point.y)); + virtualGamepad->joystickInput(point.x, point.y); continue; } vgamepad::ControlId control = vgamepad::hitTest(point.x, point.y); @@ -186,10 +174,10 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; continue; if (prevControl != nil && prevControl.intValue != vgamepad::None && prevControl.intValue != vgamepad::AnalogArea) { // button up - virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey((vgamepad::ControlId)prevControl.intValue), false); + virtualGamepad->buttonInput(static_cast(prevControl.intValue), false); } // button down - virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey(control), true); + virtualGamepad->buttonInput(control, true); touchToButton[key] = [NSNumber numberWithInt:control]; } [super touchesMoved:touches withEvent:event]; @@ -199,7 +187,7 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; { for (UITouch *touch in touches) { if (touch == joyTouch) { - [self resetTouch]; + [self resetAnalog]; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; @@ -207,7 +195,7 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; if (control != nil) { [touchToButton removeObjectForKey:key]; // button up - virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey((vgamepad::ControlId)control.intValue), false); + virtualGamepad->buttonInput(static_cast(control.intValue), false); } } [super touchesCancelled:touches withEvent:event]; diff --git a/shell/apple/emulator-ios/emulator/ios_gamepad.h b/shell/apple/emulator-ios/emulator/ios_gamepad.h index 214744c3e..e3aa0683a 100644 --- a/shell/apple/emulator-ios/emulator/ios_gamepad.h +++ b/shell/apple/emulator-ios/emulator/ios_gamepad.h @@ -23,6 +23,7 @@ #include #include "input/gamepad_device.h" #include "input/mouse.h" +#include "input/virtual_gamepad.h" #include "ui/gui.h" enum IOSButton { @@ -480,110 +481,30 @@ class IOSGamepad : public GamepadDevice static std::map> controllers; }; -class IOSVirtualGamepad : public GamepadDevice +class IOSVirtualGamepad : public VirtualGamepad { public: - IOSVirtualGamepad() : GamepadDevice(0, "iOS", false) { - _name = "Virtual Gamepad"; - _unique_id = "ios-virtual-gamepad"; - input_mapper = std::make_shared(); - //hasAnalogStick = true; // TODO has an analog stick but input mapping isn't persisted + IOSVirtualGamepad() : VirtualGamepad("iOS") { } - bool is_virtual_gamepad() override { return true; } - - std::shared_ptr getDefaultMapping() override { - return std::make_shared>(); - } - - bool gamepad_btn_input(u32 code, bool pressed) override + bool handleButtonInput(u32& state, u32 key, bool pressed) override { - if (pressed) - buttonState |= code; - else - buttonState &= ~code; - switch (code) + if (!pressed + || (key != DC_DPAD_UP && key != DC_DPAD_DOWN && key != DC_DPAD_LEFT && key != DC_DPAD_RIGHT)) + return false; + if (((state | key) & (DC_DPAD_UP | DC_DPAD_DOWN)) == (DC_DPAD_UP | DC_DPAD_DOWN) + || ((state | key) & (DC_DPAD_LEFT | DC_DPAD_RIGHT)) == (DC_DPAD_LEFT | DC_DPAD_RIGHT)) { - case DC_AXIS_LT: - gamepad_axis_input(DC_AXIS_LT, pressed ? 0x7fff : 0); - if (settings.platform.isArcade()) - GamepadDevice::gamepad_btn_input(DC_BTN_Z, pressed); // btn5 - return true; - case DC_AXIS_RT: - if (!pressed && maple_port() >= 0 && maple_port() <= 3) - kcode[maple_port()] |= DC_DPAD2_UP | DC_BTN_D | DC_DPAD2_DOWN; - gamepad_axis_input(DC_AXIS_RT, pressed ? 0x7fff : 0); - if (settings.platform.isArcade()) - GamepadDevice::gamepad_btn_input(DC_BTN_Y, pressed); // btn4 - return true; - default: - if ((buttonState & (DC_DPAD_UP | DC_DPAD_DOWN)) == (DC_DPAD_UP | DC_DPAD_DOWN) - || (buttonState & (DC_DPAD_LEFT | DC_DPAD_RIGHT)) == (DC_DPAD_LEFT | DC_DPAD_RIGHT)) - { - GamepadDevice::gamepad_btn_input(DC_DPAD_UP, false); - GamepadDevice::gamepad_btn_input(DC_DPAD_DOWN, false); - GamepadDevice::gamepad_btn_input(DC_DPAD_LEFT, false); - GamepadDevice::gamepad_btn_input(DC_DPAD_RIGHT, false); - buttonState = 0; - gui_open_settings(); - return true; - } - if (settings.platform.isArcade() && maple_port() >= 0 && maple_port() <= 3) - { - u32& keycode = kcode[maple_port()]; - if ((buttonState & DC_AXIS_RT) != 0) - { - switch (code) { - case DC_BTN_A: - // RT + A -> D (coin) - keycode = pressed ? keycode & ~DC_BTN_D : keycode | DC_BTN_D; - break; - case DC_BTN_B: - // RT + B -> Service - keycode = pressed ? keycode & ~DC_DPAD2_UP : keycode | DC_DPAD2_UP; - break; - case DC_BTN_X: - // RT + X -> Test - keycode = pressed ? keycode & ~DC_DPAD2_DOWN : keycode | DC_DPAD2_DOWN; - break; - default: - break; - } - } - // arcade mapping: X -> btn2, Y -> btn3 - if (code == DC_BTN_X) - code = DC_BTN_C; // btn2 - if (code == DC_BTN_Y) - code = DC_BTN_X; // btn3 - } - switch (code) - { - case DC_DPAD_UP | DC_DPAD_RIGHT: - GamepadDevice::gamepad_btn_input(DC_DPAD_UP, pressed); - code = DC_DPAD_RIGHT; - break; - case DC_DPAD_DOWN | DC_DPAD_RIGHT: - GamepadDevice::gamepad_btn_input(DC_DPAD_DOWN, pressed); - code = DC_DPAD_RIGHT; - break; - case DC_DPAD_DOWN | DC_DPAD_LEFT: - GamepadDevice::gamepad_btn_input(DC_DPAD_DOWN, pressed); - code = DC_DPAD_LEFT; - break; - case DC_DPAD_UP | DC_DPAD_LEFT: - GamepadDevice::gamepad_btn_input(DC_DPAD_UP, pressed); - code = DC_DPAD_LEFT; - break; - default: - break; - } - - return GamepadDevice::gamepad_btn_input(code, pressed); + gamepad_btn_input(DC_DPAD_UP, false); + gamepad_btn_input(DC_DPAD_DOWN, false); + gamepad_btn_input(DC_DPAD_LEFT, false); + gamepad_btn_input(DC_DPAD_RIGHT, false); + state = 0; + gui_open_settings(); + return true; } + return false; } - -private: - u32 buttonState = 0; }; class IOSTouchMouse : public SystemMouse From 92c5892d4c3c390540a74b2968d43e8b4ee9ae72 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 4 Dec 2024 18:17:11 +0100 Subject: [PATCH 42/81] android: fix scaling when editing vgamepad. vgamepad code clean up --- core/ui/vgamepad.cpp | 19 +++++++----- core/ui/vgamepad.h | 5 +-- .../flycast/emulator/NativeGLActivity.java | 13 ++------ .../emu/EditVirtualJoystickDelegate.java | 6 ++-- .../flycast/emulator/emu/NativeGLView.java | 6 ++-- .../flycast/src/main/jni/src/Android.cpp | 6 ++-- .../src/main/jni/src/android_input.cpp | 25 ++------------- .../emulator/FlycastViewController.mm | 31 ++++++++++++++----- 8 files changed, 49 insertions(+), 62 deletions(-) diff --git a/core/ui/vgamepad.cpp b/core/ui/vgamepad.cpp index 53867fdf5..c96c8dbe6 100644 --- a/core/ui/vgamepad.cpp +++ b/core/ui/vgamepad.cpp @@ -37,6 +37,8 @@ namespace vgamepad { +static void stopEditing(bool canceled); +static void loadLayout(); struct Control { @@ -576,7 +578,7 @@ void applyUiScale() { element.applyUiScale(); } -void loadLayout() +static void loadLayout() { for (auto& element : Layout) { element.reset(); @@ -585,7 +587,7 @@ void loadLayout() applyLayout(); } -void saveLayout() +static void saveLayout() { cfgSetAutoSave(false); for (auto& element : Layout) @@ -637,7 +639,7 @@ void loadImage(const std::string& path) } } -void enableAllControls() +static void enableAllControls() { for (auto& control : Controls) control.disabled = false; @@ -881,25 +883,26 @@ void resetEditing() { resetLayout(); } -#ifndef __ANDROID__ - -void startEditing() { +void startEditing() +{ enableAllControls(); show(); + setEditMode(true); } void pauseEditing() { + setEditMode(false); } -void stopEditing(bool canceled) +static void stopEditing(bool canceled) { + setEditMode(false); if (canceled) loadLayout(); else saveLayout(); } -#endif } // namespace vgamepad #endif // __ANDROID__ || TARGET_IPHONE diff --git a/core/ui/vgamepad.h b/core/ui/vgamepad.h index d97fb21f0..df5869802 100644 --- a/core/ui/vgamepad.h +++ b/core/ui/vgamepad.h @@ -78,13 +78,12 @@ class ImguiVGamepadTexture : public ImguiTexture #if defined(__ANDROID__) || defined(TARGET_IPHONE) -void enableAllControls(); void show(); void hide(); void draw(); void startEditing(); void pauseEditing(); -void stopEditing(bool canceled); +void setEditMode(bool editing); void resetEditing(); void displayCommands(); void loadImage(const std::string& path); @@ -100,8 +99,6 @@ void applyUiScale(); Element layoutHitTest(float x, float y); void translateElement(Element element, float dx, float dy); void scaleElement(Element element, float factor); -void loadLayout(); -void saveLayout(); #else diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java index a729a92b6..72bd5f74b 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java @@ -71,20 +71,11 @@ public void run() { } // Called from native code - private void VJoyStartEditing() { + public void setVGamepadEditMode(boolean editing) { handler.post(new Runnable() { @Override public void run() { - mView.setEditVjoyMode(true); - } - }); - } - // Called from native code - private void VJoyStopEditing() { - handler.post(new Runnable() { - @Override - public void run() { - mView.setEditVjoyMode(false); + mView.setVGamepadEditMode(editing); } }); } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java index 422dfd145..1cad6c1a9 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java @@ -48,8 +48,10 @@ public void show() { public boolean onTouchEvent(MotionEvent event, int width, int height) { scaleGestureDetector.onTouchEvent(event); - if (scaleGestureDetector.isInProgress()) + if (scaleGestureDetector.isInProgress()) { + currentElement = -1; return true; + } int actionMasked = event.getActionMasked(); int actionIndex = event.getActionIndex(); @@ -64,7 +66,7 @@ public boolean onTouchEvent(MotionEvent event, int width, int height) lastX = event.getX(actionIndex) / view.getWidth(); lastY = event.getY(actionIndex) / view.getHeight(); currentElement = VGamepad.layoutHitTest(lastX, lastY); - return currentElement != -1; + return true; // must return true if we want the scale gesture detector to work case MotionEvent.ACTION_MOVE: if (currentElement != -1 && event.getPointerCount() == 1) diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java index da10864e1..739afdfd9 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java @@ -173,13 +173,13 @@ public void onWindowFocusChanged(boolean hasFocus) { } } - public void setEditVjoyMode(boolean editVjoyMode) + public void setVGamepadEditMode(boolean editing) { if (!InputDeviceManager.getInstance().hasTouchscreen()) return; - if (editVjoyMode && !(vjoyDelegate instanceof EditVirtualJoystickDelegate)) + if (editing && !(vjoyDelegate instanceof EditVirtualJoystickDelegate)) vjoyDelegate = new EditVirtualJoystickDelegate(this); - else if (!editVjoyMode && !(vjoyDelegate instanceof VirtualJoystickDelegate)) + else if (!editing && !(vjoyDelegate instanceof VirtualJoystickDelegate)) vjoyDelegate = new VirtualJoystickDelegate(this); } diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index 50bbd45e3..89b74c756 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -52,10 +52,9 @@ static ANativeWindow *g_window = 0; // Activity jobject g_activity; -extern jmethodID VJoyStartEditingMID; -extern jmethodID VJoyStopEditingMID; extern jmethodID showScreenKeyboardMid; static jmethodID onGameStateChangeMid; +extern jmethodID setVGamepadEditModeMid; static void emuEventCallback(Event event, void *) { @@ -453,10 +452,9 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_BaseGLActivity_regis { g_activity = env->NewGlobalRef(activity); jclass actClass = env->GetObjectClass(activity); - VJoyStartEditingMID = env->GetMethodID(actClass, "VJoyStartEditing", "()V"); - VJoyStopEditingMID = env->GetMethodID(actClass, "VJoyStopEditing", "()V"); showScreenKeyboardMid = env->GetMethodID(actClass, "showScreenKeyboard", "(Z)V"); onGameStateChangeMid = env->GetMethodID(actClass, "onGameStateChange", "(Z)V"); + setVGamepadEditModeMid = env->GetMethodID(actClass, "setVGamepadEditMode", "(Z)V"); } } diff --git a/shell/android-studio/flycast/src/main/jni/src/android_input.cpp b/shell/android-studio/flycast/src/main/jni/src/android_input.cpp index 2fde7d1a7..074663ca2 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_input.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/android_input.cpp @@ -27,10 +27,8 @@ std::shared_ptr keyboard; std::shared_ptr virtualGamepad; extern jobject g_activity; -jmethodID VJoyStartEditingMID; -jmethodID VJoyStopEditingMID; -jmethodID VJoyEnableControlsMID; jmethodID showScreenKeyboardMid; +jmethodID setVGamepadEditModeMid; // // VGamepad @@ -75,25 +73,8 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_VGamepad_transla namespace vgamepad { -void startEditing() -{ - // FIXME code dup with vgamepad.cpp - enableAllControls(); - show(); - jni::env()->CallVoidMethod(g_activity, VJoyStartEditingMID); -} -void pauseEditing() { - // needed? could be used by iOS to avoid relying on gui state - jni::env()->CallVoidMethod(g_activity, VJoyStopEditingMID); -} -void stopEditing(bool canceled) -{ - // FIXME code dup with vgamepad.cpp - jni::env()->CallVoidMethod(g_activity, VJoyStopEditingMID); - if (canceled) - loadLayout(); - else - saveLayout(); +void setEditMode(bool editing) { + jni::env()->CallVoidMethod(g_activity, setVGamepadEditModeMid, editing); } } diff --git a/shell/apple/emulator-ios/emulator/FlycastViewController.mm b/shell/apple/emulator-ios/emulator/FlycastViewController.mm index 6bfdf8377..c70ed41cf 100644 --- a/shell/apple/emulator-ios/emulator/FlycastViewController.mm +++ b/shell/apple/emulator-ios/emulator/FlycastViewController.mm @@ -44,7 +44,6 @@ #include "ios_mouse.h" #include "oslib/oslib.h" -//@import AltKit; #import "AltKit-Swift.h" static std::string iosJitStatus; @@ -691,13 +690,6 @@ - (void)update - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { #if !TARGET_OS_TV - if ((gui_state == GuiState::VJoyEdit) != [self.editPadController isControllerVisible]) - { - if (gui_state == GuiState::VJoyEdit) - [self.editPadController showController:self.view]; - else - [self.editPadController hideController]; - } if (emu.running() != [self.padController isControllerVisible] && !IOSGamepad::controllerConnected()) { if (emu.running()) @@ -708,6 +700,20 @@ - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect #endif mainui_rend_frame(); } + +- (void)setVGamepadEditMode:(BOOL)editing +{ +#if !TARGET_OS_TV + if (editing != [self.editPadController isControllerVisible]) + { + if (editing) + [self.editPadController showController:self.view]; + else + [self.editPadController hideController]; + } +#endif +} + /* - (void)pickIosFile { @@ -762,3 +768,12 @@ void pickIosFile() } return iosJitStatus.c_str(); } + +namespace vgamepad +{ + +void setEditMode(bool editing) { + [flycastViewController setVGamepadEditMode:editing]; +} + +} From dea8e741826801e36bf7358df292bddb00a7bb0f Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 4 Dec 2024 18:19:34 +0100 Subject: [PATCH 43/81] better CI job names --- .github/workflows/android.yml | 1 + .github/workflows/switch.yml | 2 +- .github/workflows/uwp.yml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index e09b39298..e10f9cb92 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -4,6 +4,7 @@ on: [push, pull_request] jobs: build: + name: Android runs-on: ubuntu-latest steps: diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 4d6398e44..07f873a57 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - name: ${{ matrix.config.name }} + name: Switch ${{ matrix.config.name }} runs-on: ubuntu-latest container: devkitpro/devkita64:latest diff --git a/.github/workflows/uwp.yml b/.github/workflows/uwp.yml index 8c7450a1b..485827325 100644 --- a/.github/workflows/uwp.yml +++ b/.github/workflows/uwp.yml @@ -4,6 +4,7 @@ on: [push, pull_request] jobs: build: + name: UWP runs-on: windows-latest steps: From 5656370388418e79765cedce02a34cb3813f5846 Mon Sep 17 00:00:00 2001 From: scribam Date: Sat, 7 Dec 2024 10:45:27 +0100 Subject: [PATCH 44/81] deps: update sdl to version 2.30.10 --- core/deps/SDL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/deps/SDL b/core/deps/SDL index c98c4fbff..9c821dc21 160000 --- a/core/deps/SDL +++ b/core/deps/SDL @@ -1 +1 @@ -Subproject commit c98c4fbff6d8f3016a3ce6685bf8f43433c3efcc +Subproject commit 9c821dc21ccbd69b2bda421fdb35cb4ae2da8f5e From fb64fc418e5e5e7ec6a679c542e5480df4b8452f Mon Sep 17 00:00:00 2001 From: scribam Date: Sat, 7 Dec 2024 10:50:31 +0100 Subject: [PATCH 45/81] ci: update freebsd and openbsd versions --- .github/workflows/bsd.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 71a6218a1..580bbe90e 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -13,13 +13,13 @@ jobs: architecture: [ arm64, x86-64 ] include: - operating_system: freebsd - version: '14.1' + version: '14.2' pkginstall: sudo pkg install -y alsa-lib ccache cmake evdev-proto git libao libevdev libudev-devd libzip lua54 miniupnpc ninja pkgconf pulseaudio sdl2 - operating_system: netbsd version: '10.0' pkginstall: sudo pkgin update && sudo pkgin -y install alsa-lib ccache cmake gcc12 git libao libzip lua54 miniupnpc ninja-build pkgconf pulseaudio SDL2 && export PATH=/usr/pkg/gcc12/bin:$PATH - operating_system: openbsd - version: '7.5' + version: '7.6' pkginstall: sudo pkg_add ccache cmake git libao libzip miniupnpc ninja pkgconf pulseaudio sdl2 exclude: - architecture: arm64 @@ -36,7 +36,7 @@ jobs: key: ccache-${{ matrix.operating_system }}-${{ matrix.architecture }}-${{ github.sha }} restore-keys: ccache-${{ matrix.operating_system }}-${{ matrix.architecture }}- - - uses: cross-platform-actions/action@v0.25.0 + - uses: cross-platform-actions/action@v0.26.0 with: operating_system: ${{ matrix.operating_system }} architecture: ${{ matrix.architecture }} From 1507097bcd30486ec8de163f296b8a431e3e63f5 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 9 Dec 2024 16:02:30 +0100 Subject: [PATCH 46/81] naomi: set offscreen sensor when reloading. fix wldkicks PCB inputs Most games using analog inputs for lightguns also have an offscreen sensor (lupinsho, mok, hotd2, confmiss). Activate it when reloading or shooting offscreen. Fix World Kicks PCB inputs. --- core/hw/maple/maple_jvs.cpp | 44 ++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/core/hw/maple/maple_jvs.cpp b/core/hw/maple/maple_jvs.cpp index 284bd16b7..2cd83cc9d 100644 --- a/core/hw/maple/maple_jvs.cpp +++ b/core/hw/maple/maple_jvs.cpp @@ -190,6 +190,15 @@ class jvs_io_board continue; if (keycode & NAOMI_RELOAD_KEY) keycode |= NAOMI_BTN0_KEY; + if (lightgun_as_analog && (keycode & NAOMI_BTN0_KEY)) + { + const MapleInputState& inputState = mapleInputState[player]; + if (inputState.absPos.x < 0 || inputState.absPos.x > 639 + || inputState.absPos.y < 0 || inputState.absPos.y > 479 + || (keycode & NAOMI_RELOAD_KEY)) { + keycode |= NAOMI_BTN1_KEY; // offscreen sensor, not used by deathcox + } + } // P1 mapping (only for P2) if (player == 1) @@ -271,6 +280,7 @@ class jvs_io_board u32 output_count = 0; bool init_in_progress = false; maple_naomi_jamma *parent; + u8 first_player; private: void init_mappings() @@ -302,7 +312,6 @@ class jvs_io_board } u8 node_id; - u8 first_player; std::array cur_mapping; std::array p1_mapping; @@ -1095,15 +1104,16 @@ class jvs_namco_v226_pcb : public jvs_io_board void read_digital_in(const u32 *buttons, u32 *v) override { jvs_io_board::read_digital_in(buttons, v); - for (u32 player = 0; player < player_count; player++) + for (u32 player = first_player; player < first_player + player_count && player < 4; player++) { u8 trigger = mapleInputState[player].halfAxes[PJTI_R] >> 10; + const u32 idx = player - first_player; // Ball button - v[player] = ((trigger & 0x20) << 3) | ((trigger & 0x10) << 5) | ((trigger & 0x08) << 7) + v[idx] = ((trigger & 0x20) << 3) | ((trigger & 0x10) << 5) | ((trigger & 0x08) << 7) | ((trigger & 0x04) << 9) | ((trigger & 0x02) << 11) | ((trigger & 0x01) << 13) // other buttons - | (v[player] & (NAOMI_SERVICE_KEY | NAOMI_TEST_KEY | NAOMI_START_KEY)) - | ((v[player] & NAOMI_BTN0_KEY) >> 4); // remap button4 to button0 (change button) + | (v[idx] & (NAOMI_SERVICE_KEY | NAOMI_TEST_KEY | NAOMI_START_KEY)) + | ((v[idx] & NAOMI_BTN0_KEY) >> 4); // remap button4 to button0 (change button) } } @@ -1120,17 +1130,31 @@ class jvs_namco_v226_pcb : public jvs_io_board return std::min(0xff, 0x80 - axis_y) << 8; } - u16 read_analog_axis(int player_num, int player_axis, bool inverted) override { + u16 read_analog_axis(int player_num, int player_axis, bool inverted) override + { switch (player_axis) { + // P1 case 0: - return read_joystick_x(0); + return read_joystick_x(player_num + 0); case 1: - return read_joystick_y(0); + return read_joystick_y(player_num + 0); + // P2 & P4 case 4: - return read_joystick_x(1); + return read_joystick_x(player_num + 1); case 5: - return read_joystick_y(1); + return read_joystick_y(player_num + 1); + // P3 + case 8: + if (player_num == 0) + return read_joystick_x(player_num + 2); + else + return 0x8000; + case 9: + if (player_num == 0) + return read_joystick_y(player_num + 2); + else + return 0x8000; default: return 0x8000; } From 94f35ca54f74383ec10331a484b33efca5a65f11 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 9 Dec 2024 16:09:45 +0100 Subject: [PATCH 47/81] arcade: add input descriptors for most games --- core/hw/naomi/naomi_cart.h | 2 +- core/hw/naomi/naomi_roms.cpp | 404 +++++++++++++------------------ core/hw/naomi/naomi_roms_input.h | 273 +++++++++++++++------ 3 files changed, 370 insertions(+), 309 deletions(-) diff --git a/core/hw/naomi/naomi_cart.h b/core/hw/naomi/naomi_cart.h index 350e0e0c8..ea7440c2a 100644 --- a/core/hw/naomi/naomi_cart.h +++ b/core/hw/naomi/naomi_cart.h @@ -171,7 +171,7 @@ struct AxisDescriptor struct InputDescriptors { ButtonDescriptor buttons[18]; - AxisDescriptor axes[8]; + AxisDescriptor axes[16]; }; extern InputDescriptors *NaomiGameInputs; diff --git a/core/hw/naomi/naomi_roms.cpp b/core/hw/naomi/naomi_roms.cpp index e987380a8..d8a0fdece 100644 --- a/core/hw/naomi/naomi_roms.cpp +++ b/core/hw/naomi/naomi_roms.cpp @@ -275,7 +275,7 @@ const Game Games[] = // Kick '4' Cash (Export) { "kick4csh", - NULL, + nullptr, "Kick '4' Cash (Export)", 0x9000000, 0x820857c9, @@ -305,25 +305,22 @@ const Game Games[] = //ROM_REGION(0x200, "some_eeprom", 0) //ROM_LOAD( "25lc040.ic13s", 0, 0x200, CRC(1576366a) SHA1(3e8bf3dbc8a248a6863242b78d5c6e53a869e951) ) - // TODO Need emulation of 837-14438 board on serial port //ROM_REGION(0x220000, "hopper_board", 0) //ROM_LOAD( "fpr-24150.ic6", 0x0000000, 0x200000, CRC(3845c34c) SHA1(027b17bac64482ee152773d5fab30fcbc6e2bcb7) ) // SH4 code //ROM_LOAD( "6372a.ic3", 0x0200000, 0x020000, CRC(f30839ad) SHA1(ea1a32c4da1ed9745300bcdd7964a7c0964e3221) ) // FPGA config - - { NULL, 0, 0 }, }, - NULL, - NULL, + nullptr, + &kick4csh_inputs, kick4csh_eeprom_dump }, // Marvel vs. Capcom 2 New Age of Heroes (Export, Korea, Rev A) { "mvsc2", - NULL, + nullptr, "Marvel vs. Capcom 2 New Age of Heroes (Export, Korea)", 0x08800000, 0xc18b6e7c, - NULL, + "naomi", M1, ROT0, { @@ -351,10 +348,8 @@ const Game Games[] = //ROM_REGION(0x200, "some_eeprom", 0) //ROM_LOAD( "25lc040.ic13s", 0x000000, 0x200, CRC(dc449637) SHA1(6cab09f61be1498271a36bff6a114a4eeeb00e1a) ) - - { NULL, 0, 0 }, }, - NULL, + nullptr, &mvsc2_inputs, mvsc2_eeprom_dump, }, @@ -420,7 +415,6 @@ const Game Games[] = { "mpr-23224.ic30s", 0x7000002, 0x0800000, 0x0000000, InterleavedWord }, { "mpr-23225.ic31", 0x8000000, 0x0800000, 0x0000000, InterleavedWord }, { "mpr-23226.ic32s", 0x8000002, 0x0800000, 0x0000000, InterleavedWord }, - { NULL, 0, 0 }, } }, // Shootout Pool @@ -534,11 +528,9 @@ const Game Games[] = //ROM_REGION(0x200, "some_eeprom", 0) //ROM_LOAD( "25lc040.ic13s", 0, 0x200, CRC(6291605c) SHA1(44f757da4814b08108d1a4f431c9a39c38acecb2) ) - - { NULL, 0, 0 }, }, nullptr, - nullptr, + &service_btns_inputs, tduno2_eeprom_dump, }, // Virtua Tennis 2 / Power Smash 2 (Rev A) @@ -572,7 +564,6 @@ const Game Games[] = { "mpr-22323.ic33", 0x9000000, 0x800000, 0x0000000, InterleavedWord }, { "mpr-22324.ic34s", 0x9000002, 0x800000, 0x0000000, InterleavedWord }, { "copy", 0x0400000, 0xc00000, 0x0000000, Copy, 0x1000000 }, // changed - { NULL, 0, 0 }, }, nullptr, &shot12_inputs @@ -768,7 +759,6 @@ const Game Games[] = { "mpr-22181.ic18s", 0x9000000, 0x800000 }, { "mpr-22182.ic19s", 0x9800000, 0x800000 }, { "mpr-22183.ic20s", 0xa000000, 0x800000 }, - { NULL, 0, 0 }, }, NULL, &_18wheelr_inputs, @@ -820,8 +810,6 @@ const Game Games[] = // 18 Wheeler motor controller 838-13992, code is for a TMPZ84C015 which is Z80 compatible //ROM_REGION( 0x10000, "motorio", ROMREGION_ERASEFF) //ROM_LOAD( "epr-23000.ic8", 0x000000, 0x010000, CRC(e3b162f7) SHA1(52c7ad759c3c4a3148764e14d77ba5006bc8af48) ) - - { NULL, 0, 0 }, }, NULL, &_18wheelr_inputs, @@ -873,8 +861,6 @@ const Game Games[] = // 18 Wheeler motor controller 838-13992, code is for a TMPZ84C015 which is Z80 compatible //ROM_REGION( 0x10000, "motorio", ROMREGION_ERASEFF) //ROM_LOAD( "epr-23000.ic8", 0x000000, 0x010000, CRC(e3b162f7) SHA1(52c7ad759c3c4a3148764e14d77ba5006bc8af48) ) - - { NULL, 0, 0 }, }, NULL, &_18wheelr_inputs, @@ -923,8 +909,6 @@ const Game Games[] = // 18 Wheeler motor controller 838-13992, code is for a TMPZ84C015 which is Z80 compatible //ROM_REGION( 0x10000, "motorio", ROMREGION_ERASEFF) //ROM_LOAD( "epr-23000.ic8", 0x000000, 0x010000, CRC(e3b162f7) SHA1(52c7ad759c3c4a3148764e14d77ba5006bc8af48) ) - - { NULL, 0, 0 }, }, NULL, &_18wheelr_inputs, @@ -972,8 +956,6 @@ const Game Games[] = // 840-0023 2000 317-0273-COM Naomi //ROM_PARAMETER( ":rom_board:segam2crypt:key", "2807cf54" ) - - { NULL, 0, 0 }, }, NULL, &_18wheelr_inputs, // no issue with wheel range on this version @@ -1034,7 +1016,6 @@ const Game Games[] = { "mpr-21736.ic9", 0x4800000, 0x800000 }, { "mpr-21737.ic10", 0x5000000, 0x800000 }, { "mpr-21738.ic11", 0x5800000, 0x800000 }, - { NULL, 0, 0 }, }, NULL, &alpilot_inputs, @@ -1058,7 +1039,6 @@ const Game Games[] = { "mpr-23583.ic3", 0x2800000, 0x1000000 }, { "mpr-23584.ic4", 0x3800000, 0x1000000 }, { "mpr-23585.ic5", 0x4800000, 0x1000000 }, - { NULL, 0, 0 }, }, NULL, &alienfnt_inputs, @@ -1081,7 +1061,6 @@ const Game Games[] = { "mpr-23583.ic3", 0x2800000, 0x1000000 }, { "mpr-23584.ic4", 0x3800000, 0x1000000 }, { "mpr-23585.ic5", 0x4800000, 0x1000000 }, - { NULL, 0, 0 }, }, NULL, &alienfnt_inputs, @@ -1254,7 +1233,6 @@ const Game Games[] = { "mpr-21681.ic13s", 0x6800000, 0x800000 }, { "mpr-21682.ic14s", 0x7000000, 0x800000 }, { "mpr-21683.ic15s", 0x7800000, 0x800000 }, - { NULL, 0, 0 }, }, NULL, &crzytaxi_inputs, @@ -1280,9 +1258,9 @@ const Game Games[] = { "mpr-23425.ic6", 0x3000000, 0x0800000 }, { "mpr-23426.ic7", 0x3800000, 0x0800000 }, { "mpr-23427.ic8", 0x4000000, 0x0800000 }, - { NULL, 0, 0 }, - } - // SMASH, JUMP, udlr + }, + nullptr, + &csmash_inputs, }, // Cosmic Smash { @@ -1304,8 +1282,9 @@ const Game Games[] = { "mpr-23425.ic6", 0x3000000, 0x0800000 }, { "mpr-23426.ic7", 0x3800000, 0x0800000 }, { "mpr-23427.ic8", 0x4000000, 0x0800000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &csmash_inputs, }, // Cannon Spike / Gun Spike { @@ -1331,7 +1310,6 @@ const Game Games[] = { "mpr-23207.ic10", 0x5000000, 0x0800000 }, { "mpr-23208.ic11", 0x5800000, 0x0800000 }, { "mpr-23209.ic12s", 0x6000000, 0x0800000 }, - { NULL, 0, 0 }, }, NULL, &cspike_inputs @@ -1358,10 +1336,9 @@ const Game Games[] = { "mpr-23521.ic8", 0x4000000, 0x0800000 }, { "mpr-23522.ic9", 0x4800000, 0x0800000 }, { "mpr-23523.ic10", 0x5000000, 0x0800000 }, - { NULL, 0, 0 }, }, NULL, - &trigger_inputs + &lightgun_inputs }, // Death Crimson OX (USA) { @@ -1385,10 +1362,9 @@ const Game Games[] = { "mpr-23521.ic8", 0x4000000, 0x0800000, 0xcf8674b8 }, { "mpr-23522.ic9", 0x4800000, 0x0800000, 0x7ae6716e }, { "mpr-23523.ic10",0x5000000, 0x0800000, 0xc91efb67 }, - }, NULL, - &trigger_inputs + &lightgun_inputs }, // Death Crimson OX { @@ -1412,10 +1388,9 @@ const Game Games[] = { "mpr-23521.ic8", 0x4000000, 0x0800000 }, { "mpr-23522.ic9", 0x4800000, 0x0800000 }, { "mpr-23523.ic10", 0x5000000, 0x0800000 }, - { NULL, 0, 0 }, }, NULL, - &trigger_inputs + &lightgun_inputs }, // Derby Owners Club 2000 (Rev A) { @@ -1799,7 +1774,6 @@ const Game Games[] = { "mpr-22138.ic17s", 0x8800000, 0x0800000 }, { "mpr-22139.ic18s", 0x9000000, 0x0800000 }, { "mpr-22140.ic19s", 0x9800000, 0x0800000 }, - { NULL, 0, 0 }, }, nullptr, &wsbb_inputs, @@ -1838,7 +1812,6 @@ const Game Games[] = { "mpr-21572.ic19s", 0x9800000, 0x0800000 }, { "mpr-21573.ic20s", 0xa000000, 0x0800000 }, { "mpr-21574.ic21s", 0xa800000, 0x0800000 }, - { NULL, 0, 0 }, }, nullptr, &wsbb_inputs, @@ -1882,8 +1855,6 @@ const Game Games[] = //ROM_REGION( 0x10000, "drivebd", 0 ) // drive board ROM //ROM_LOAD( "epr-21867p.bin", 0x000000, 0x010000, CRC(6143b911) SHA1(360ebc53696da7a29e6404376c82947563274835) ) // prototype preview //ROM_LOAD( "epr-21867.bin", 0x000000, 0x010000, CRC(4f93a2a0) SHA1(875907e7fcfc44850e2c60c12268ac61c742f217) ) - - { NULL, 0, 0 }, }, nullptr, &f355_inputs, @@ -1923,7 +1894,6 @@ const Game Games[] = { "mpr-22845.ic19s", 0x9800000, 0x800000, 0x3327aed1 }, { "mpr-22846.ic20s", 0xa000000, 0x800000, 0xd4148f39 }, { "mpr-22847.ic21s", 0xa800000, 0x800000, 0x955ad42e }, - { NULL, 0, 0 }, }, nullptr, &f355_inputs, @@ -1963,7 +1933,6 @@ const Game Games[] = { "rom19.ic19s", 0x9800000, 0x800000, 0x3327aed1 }, { "rom20.ic20s", 0xa000000, 0x800000, 0xd4148f39 }, { "rom21.ic21s", 0xa800000, 0x800000, 0x955ad42e }, - { NULL, 0, 0 }, }, nullptr, &f355_inputs, @@ -2004,7 +1973,6 @@ const Game Games[] = { "mpr-23396.ic19s", 0x9800000, 0x800000 }, { "mpr-23397.ic20s", 0xa000000, 0x800000 }, { "mpr-23398.ic21s", 0xa800000, 0x800000 }, - { NULL, 0, 0 }, }, nullptr, &f355_inputs, @@ -2036,7 +2004,6 @@ const Game Games[] = { "mpr-21829.ic9", 0x4800000, 0x0800000 }, { "mpr-21830.ic10", 0x5000000, 0x0800000 }, { "mpr-21831.ic11", 0x5800000, 0x0800000 }, - { NULL, 0, 0 }, }, nullptr, &giant_gram_inputs, @@ -2067,7 +2034,6 @@ const Game Games[] = { "mpr-23353.ic12s", 0x6000000, 0x0800000 }, { "mpr-23354.ic13s", 0x6800000, 0x0800000 }, { "mpr-23355.ic14s", 0x7000000, 0x0800000 }, - { NULL, 0, 0 }, }, nullptr, &ggx_inputs, @@ -2095,7 +2061,6 @@ const Game Games[] = { "mpr-23635.ic8", 0x7800000, 0x1000000 }, { "mpr-23636.ic9", 0x8800000, 0x1000000 }, { "mpr-23637.ic10", 0x9800000, 0x1000000 }, - { NULL, 0, 0 }, }, NULL, &shot1234_inputs, @@ -2127,7 +2092,6 @@ const Game Games[] = { "bhf1ma13.6n", 0xd000000, 0x1000000 }, { "bhf1ma14.6m", 0xe000000, 0x1000000 }, { "bhf1ma15.6l", 0xf000000, 0x1000000 }, - { NULL, 0, 0 }, }, NULL, &gunsur2_inputs, @@ -2159,7 +2123,6 @@ const Game Games[] = { "bhf1ma13.6n", 0xd000000, 0x1000000 }, { "bhf1ma14.6m", 0xe000000, 0x1000000 }, { "bhf1ma15.6l", 0xf000000, 0x1000000 }, - { NULL, 0, 0 }, }, NULL, &gunsur2_inputs, @@ -2183,7 +2146,6 @@ const Game Games[] = { "mpr-22273.ic3", 0x2800000, 0x1000000 }, { "mpr-22274.ic4", 0x3800000, 0x1000000 }, { "mpr-22275.ic5", 0x4800000, 0x1000000 }, - { NULL, 0, 0 }, }, nullptr, &shot12_inputs, @@ -2211,7 +2173,6 @@ const Game Games[] = { "mpr-23713.ic9", 0x4800000, 0x0800000 }, { "mpr-23714.ic10", 0x5000000, 0x0800000 }, { "mpr-23715.ic11", 0x5800000, 0x0800000 }, - { NULL, 0, 0 }, }, NULL, &hmgeo_inputs, @@ -2250,10 +2211,9 @@ const Game Games[] = { "mpr-21403.ic18s", 0x9000000, 0x800000 }, { "mpr-21404.ic19s", 0x9800000, 0x800000 }, { "mpr-21405.ic20s", 0xa000000, 0x800000 }, - { NULL, 0, 0 }, }, nullptr, - nullptr, + &lightgun_inputs, // no free play with eeprom }, // The House of the Dead 2 @@ -2289,10 +2249,9 @@ const Game Games[] = { "mpr-21403.ic18s", 0x9000000, 0x800000 }, { "mpr-21404.ic19s", 0x9800000, 0x800000 }, { "mpr-21405.ic20s", 0xa000000, 0x800000 }, - { NULL, 0, 0 }, }, nullptr, - nullptr, + &lightgun_inputs, // no free play with eeprom }, // The House of the Dead 2 (Export) @@ -2328,10 +2287,9 @@ const Game Games[] = { "mpr-21403.ic18s", 0x9000000, 0x800000 }, { "mpr-21404.ic19s", 0x9800000, 0x800000 }, { "mpr-21405.ic20s", 0xa000000, 0x800000 }, - { NULL, 0, 0 }, }, nullptr, - nullptr, + &lightgun_inputs, // no free play with eeprom }, // The House of the Dead 2 (prototype) @@ -2370,7 +2328,7 @@ const Game Games[] = { "rom21.ic21s", 0xa800000, 0x800000, 0x256603d7 }, }, nullptr, - nullptr, + &lightgun_inputs, // no free play with eeprom }, // Inu No Osanpo / Dog Walking (Japan, Export, Rev A) @@ -2401,7 +2359,6 @@ const Game Games[] = { "rom14.ic14s", 0x7000000, 0x800000 }, { "rom15.ic15s", 0x7800000, 0x800000 }, { "rom16.ic16s", 0x8000000, 0x800000 }, - { NULL, 0, 0 }, } }, // Jambo! Safari (Rev A) @@ -2424,7 +2381,6 @@ const Game Games[] = { "mpr-22823.ic6", 0x3000000, 0x800000 }, { "mpr-22824.ic7", 0x3800000, 0x800000 }, { "mpr-22825.ic8", 0x4000000, 0x800000 }, - { NULL, 0, 0 }, }, NULL, &jambo_inputs @@ -2457,7 +2413,9 @@ const Game Games[] = { "mpr-22990.ic13s", 0x6800000, 0x800000 }, { "mpr-22991.ic14s", 0x7000000, 0x800000 }, { "mpr-22992.ic15s", 0x7800000, 0x800000 }, - } + }, + nullptr, + nullptr, // TODO }, // Mazan: Flash of the Blade (MAZ2 Ver. A) { @@ -2629,7 +2587,6 @@ const Game Games[] = //ROM_REGION(0x84, "some_eeprom", 0) //ROM_LOAD("sflash.ic37", 0x000000, 0x000084, CRC(37a66f3c) SHA1(df6cd2cdc2813caa5da4dc9f171998485bcbdc44)) - { NULL, 0, 0 }, }, NULL, &mvsc2_inputs, @@ -2660,7 +2617,6 @@ const Game Games[] = { "nja1ma10.4b", 0xa000000, 0x1000000 }, //ROM_REGION( 0x20000, "jyu_io", 0 ) // H8/3334-based I/O board ROM, eventually should be separated out //ROM_LOAD( "jyu1_prg0a.ic3", 0x000000, 0x020000, CRC(aec4dbc1) SHA1(bddd4f345baf7f594998a39c09da18b3834f0ac2) ) - { NULL, 0, 0 }, }, NULL, &ninjaslt_inputs, @@ -2692,7 +2648,6 @@ const Game Games[] = //ROM_REGION( 0x20000, "jyu_io", 0 ) // H8/3334-based I/O board ROM, eventually should be separated out //ROM_LOAD( "jyu1_prg0a.ic3", 0x000000, 0x020000, CRC(aec4dbc1) SHA1(bddd4f345baf7f594998a39c09da18b3834f0ac2) ) - { NULL, 0, 0 }, }, NULL, &ninjaslt_inputs, @@ -2724,8 +2679,6 @@ const Game Games[] = //ROM_REGION( 0x20000, "jyu_io", 0 ) // H8/3334-based I/O board ROM, eventually should be separated out //ROM_LOAD( "jyu1_prg0a.ic3", 0x000000, 0x020000, CRC(aec4dbc1) SHA1(bddd4f345baf7f594998a39c09da18b3834f0ac2) ) - - { NULL, 0, 0 }, }, NULL, &ninjaslt_inputs, @@ -2757,8 +2710,6 @@ const Game Games[] = //ROM_REGION( 0x20000, "jyu_io", 0 ) // H8/3334-based I/O board ROM, eventually should be separated out //ROM_LOAD( "jyu1_prg0a.ic3", 0x000000, 0x020000, CRC(aec4dbc1) SHA1(bddd4f345baf7f594998a39c09da18b3834f0ac2) ) - - { NULL, 0, 0 }, }, NULL, &ninjaslt_inputs, @@ -2781,7 +2732,6 @@ const Game Games[] = { "mpr-24056.ic3", 0x2800000, 0x1000000 }, { "mpr-24057.ic4", 0x3800000, 0x1000000 }, { "mpr-24058.ic5", 0x4800000, 0x1000000 }, - { NULL, 0, 0 }, } }, // OutTrigger (JPN, USA, EXP, KOR, AUS) @@ -2817,12 +2767,9 @@ const Game Games[] = { "mpr-22160.ic19s",0x9800000, 0x0800000 }, //ROM_REGION( 0x10000, "io_board", 0) //ROM_LOAD("epr-22084.ic3", 0x0000, 0x10000, CRC(18cf58bb) SHA1(1494f8215231929e41bbe2a133658d01882fbb0f) ) - - { NULL, 0, 0 }, }, nullptr, - // TRIGGER, CHANGE, JUMP - nullptr, + &otrigger_inputs, otrigger_eeprom_dump, }, // Moero! Justice Gakuen (JPN) / Project Justice (USA, EXP, KOR, AUS) (Rev A) @@ -2848,7 +2795,6 @@ const Game Games[] = { "mpr-23545.ic9", 0x8800000, 0x1000000 }, { "mpr-23546.ic10", 0x9800000, 0x1000000 }, { "mpr-23547.ic11", 0xa800000, 0x1000000 }, - { NULL, 0, 0 }, }, nullptr, &capcom_4btn_inputs, @@ -2874,7 +2820,6 @@ const Game Games[] = { "mpr-21594.ic6", 0x3000000, 0x0800000 }, { "mpr-21595.ic7", 0x3800000, 0x0800000 }, { "mpr-21596.ic8", 0x4000000, 0x0800000 }, - { NULL, 0, 0 }, }, nullptr, &pstone_inputs, @@ -2900,7 +2845,6 @@ const Game Games[] = { "mpr-23124.ic7", 0x3800000, 0x0800000 }, { "mpr-23125.ic8", 0x4000000, 0x0800000 }, { "mpr-23126.ic9", 0x4800000, 0x0800000 }, - { NULL, 0, 0 }, }, nullptr, &pstone2_inputs, @@ -2926,7 +2870,6 @@ const Game Games[] = { "07.ic8", 0x3800000, 0x0800000 }, { "08.ic9", 0x4000000, 0x0800000 }, { "09.ic10", 0x4800000, 0x0800000 }, - { NULL, 0, 0 }, }, nullptr, &pstone2_inputs, @@ -2963,8 +2906,9 @@ const Game Games[] = { "mpr-22203.ic18s", 0x9000000, 0x800000 }, { "mpr-22204.ic19s", 0x9800000, 0x800000 }, { "mpr-22205.ic20s", 0xa000000, 0x800000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &puyoda_inputs, }, // Ring Out 4x4 (Rev A) { @@ -2988,7 +2932,6 @@ const Game Games[] = { "mpr-21768.ic8", 0x4000000, 0x800000 }, { "mpr-21769.ic9", 0x4800000, 0x800000 }, { "mpr-21770.ic10", 0x5000000, 0x800000 }, - { NULL, 0, 0 }, }, NULL, &ringout_inputs, @@ -3015,7 +2958,6 @@ const Game Games[] = { "mpr-21768.ic8", 0x4000000, 0x800000 }, { "mpr-21769.ic9", 0x4800000, 0x800000 }, { "mpr-21770.ic10", 0x5000000, 0x800000 }, - { NULL, 0, 0 }, }, NULL, &ringout_inputs, @@ -3048,8 +2990,9 @@ const Game Games[] = { "mpr-22963.ic14s",0x7000000, 0x0800000 }, { "mpr-22964.ic15s",0x7800000, 0x0800000 }, { "mpr-22965.ic16s",0x8000000, 0x0800000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &samba_inputs, }, // Samba De Amigo (Rev A) { @@ -3079,8 +3022,9 @@ const Game Games[] = { "mpr-22963.ic14s", 0x7000000, 0x0800000 }, { "mpr-22964.ic15s", 0x7800000, 0x0800000 }, { "mpr-22965.ic16s", 0x8000000, 0x0800000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &samba_inputs, }, // Samba De Amigo (USA, prototype) { @@ -3108,8 +3052,9 @@ const Game Games[] = { "rom12.ic12s", 0x06000000, 0x00800000 }, { "rom13.ic13s", 0x06800000, 0x00800000 }, { "rom14.ic14s", 0x07000000, 0x00800000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &samba_inputs, }, //Samba de Amigo ver. 2000 (Japan) { @@ -3134,8 +3079,9 @@ const Game Games[] = { "mpr-23597.ic9", 0x08800000, 0x1000000 }, { "mpr-23598.ic10", 0x09800000, 0x1000000 }, { "mpr-23599.ic11", 0x0a800000, 0x1000000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &samba_inputs, }, // Sega Tetris { @@ -3156,9 +3102,9 @@ const Game Games[] = { "mpr-22913.ic4", 0x2000000, 0x800000 }, { "mpr-22914.ic5", 0x2800000, 0x800000 }, { "mpr-22915.ic6", 0x3000000, 0x800000 }, - { NULL, 0, 0 }, - } - // SW1/2 + }, + nullptr, + &sgtetris_inputs, }, // Dengen Tenshi Taisen Janshi Shangri-la (JPN, USA, EXP, KOR, AUS) { @@ -3184,7 +3130,6 @@ const Game Games[] = { "mpr-22070.ic10", 0x5000000, 0x0800000 }, { "mpr-22071.ic11", 0x5800000, 0x0800000 }, { "mpr-22072.ic12s", 0x6000000, 0x0800000 }, - { NULL, 0, 0 }, } }, // Star Horse (satellite) @@ -3209,8 +3154,6 @@ const Game Games[] = //ROM_REGION(0x84, "some_eeprom", 0) //ROM_LOAD( "sflash.ic46", 0x000000, 0x000084, CRC(4929e940) SHA1(f8c4277ca0ae5e36b2eed033cc731b8fc4fccafc) ) - - { NULL, 0, 0 }, } }, // Star Horse (live and backup) @@ -3247,8 +3190,6 @@ const Game Games[] = //ROM_REGION(0x84, "some_eeprom", 0) //ROM_LOAD( "sflash.ic37", 0x000000, 0x000084, CRC(1557297e) SHA1(41e8a7a8eaf5076b124d378afdf97e328d100e72) ) - - { NULL, 0, 0 }, } }, // Star Horse (main screens) @@ -3270,8 +3211,6 @@ const Game Games[] = //ROM_REGION(0x84, "some_eeprom", 0) //ROM_LOAD( "sflash.ic46", 0x000000, 0x000084, CRC(951684e4) SHA1(0beaf5827064252293223b946c04b8698e7207bb) ) - - { NULL, 0, 0 }, } }, // Star Horse Progress (satellite, Rev A) @@ -3293,7 +3232,6 @@ const Game Games[] = { "mpr-24127.ic5", 0x4800000, 0x1000000 }, { "mpr-24128.ic6", 0x5800000, 0x1000000 }, { "mpr-24129.ic7", 0x6800000, 0x1000000 }, - { NULL, 0, 0 }, } }, // Star Horse Progress (backup data) @@ -3312,8 +3250,6 @@ const Game Games[] = //ROM_REGION(0x84, "some_eeprom", 0) //ROM_LOAD( "sflash.ic37", 0x000000, 0x000084, CRC(fe8f8f5c) SHA1(839461ab736e0228dec7e2512e1692d6ecc4e664) ) - - { NULL, 0, 0 }, } }, // Star Horse Progress (live, Rev A) @@ -3339,7 +3275,6 @@ const Game Games[] = { "mpr-24116.ic9", 0x08800000, 0x01000000 }, { "mpr-24117.ic10", 0x09800000, 0x01000000 }, { "mpr-24118.ic11", 0x0a800000, 0x01000000 }, - { NULL, 0, 0 }, } }, // Star Horse Progress (main screens) @@ -3360,7 +3295,6 @@ const Game Games[] = { "mpr-24091.ic4", 0x03800000, 0x01000000 }, { "mpr-24092.ic5", 0x04800000, 0x01000000 }, { "mpr-24093.ic6", 0x05800000, 0x01000000 }, - { NULL, 0, 0 }, } }, // Star Horse Progress (sound & backup) @@ -3381,7 +3315,6 @@ const Game Games[] = { "mpr-24101.ic4", 0x03800000, 0x01000000 }, { "mpr-24102.ic5", 0x04800000, 0x01000000 }, { "mpr-24103.ic6", 0x05800000, 0x01000000 }, - { NULL, 0, 0 }, } }, // Star Horse 2001 (satellite, Rev B) @@ -3403,7 +3336,6 @@ const Game Games[] = { "mpr-23744.ic5", 0x04800000, 0x01000000 }, { "mpr-23745.ic6", 0x05800000, 0x01000000 }, { "mpr-23746.ic7", 0x06800000, 0x01000000 }, - { NULL, 0, 0 }, } }, // Star Horse 2002 (main screens) @@ -3523,7 +3455,6 @@ const Game Games[] = { "mpr-22056.ic19s", 0x9800000, 0x800000 }, { "mpr-22057.ic20s", 0xa000000, 0x800000 }, { "mpr-22058.ic21s", 0xa800000, 0x800000 }, - { NULL, 0, 0 }, }, NULL, &wsbb_inputs, @@ -3557,7 +3488,6 @@ const Game Games[] = { "mpr-23338.ic15s",0x7800000, 0x0800000 }, { "mpr-23339.ic16s",0x8000000, 0x0800000 }, { "mpr-23340.ic17s",0x8800000, 0x0800000 }, - { NULL, 0, 0 }, }, NULL, &slashout_inputs, @@ -3587,7 +3517,6 @@ const Game Games[] = { "mpr-22217.ic10", 0x5000000, 0x0800000 }, { "mpr-22218.ic11", 0x5800000, 0x0800000 }, { "mpr-22219.ic12s", 0x6000000, 0x0800000 }, - { NULL, 0, 0 }, }, NULL, &marine_fishing_inputs, @@ -3614,7 +3543,6 @@ const Game Games[] = { "mpr-22974.ic8", 0x4000000, 0x0800000 }, { "mpr-22975.ic9", 0x4800000, 0x0800000 }, { "mpr-22976.ic10", 0x5000000, 0x0800000 }, - { NULL, 0, 0 }, }, NULL, &shot1234_inputs, @@ -3652,7 +3580,6 @@ const Game Games[] = { "mpr-23319.ic18s", 0x9000000, 0x800000 }, { "mpr-23320.ic19s", 0x9800000, 0x800000 }, { "mpr-23321.ic20s", 0xa000000, 0x800000 }, - { NULL, 0, 0 }, }, nullptr, &sstrkfgt_inputs, @@ -3691,7 +3618,6 @@ const Game Games[] = { "mpr-23319.ic18s", 0x9000000, 0x800000 }, { "mpr-23320.ic19s", 0x9800000, 0x800000 }, { "mpr-23321.ic20s", 0xa000000, 0x800000 }, - { NULL, 0, 0 }, }, nullptr, &sstrkfgt_inputs, @@ -3725,7 +3651,6 @@ const Game Games[] = { "mpr-21991.ic12s",0x6000000, 0x0800000 }, { "mpr-21992.ic13s",0x6800000, 0x0800000 }, { "mpr-21993.ic14s",0x7000000, 0x0400000 }, - { NULL, 0, 0 }, } }, // Touch de Uno! / Unou Nouryoku Check Machine (Japan) @@ -3753,7 +3678,7 @@ const Game Games[] = { "x76f100.ic37", 0x000000, 0x000084, 0xc79251d5, Eeprom }, }, nullptr, - nullptr, + &service_btns_inputs, tduno_eeprom_dump, }, // Tokyo Bus (Rev A) @@ -3786,7 +3711,6 @@ const Game Games[] = { "rom16.ic16s", 0x8000000, 0x0800000 }, { "rom17.ic17s", 0x8800000, 0x0800000 }, { "rom18.ic18s", 0x9000000, 0x0800000 }, - { NULL, 0, 0 }, }, NULL, &tokyobus_inputs, @@ -3825,7 +3749,6 @@ const Game Games[] = { "mpr-23019.ic19s", 0x9800000, 0x0800000 }, { "mpr-23020.ic20s", 0xa000000, 0x0800000 }, // IC21s not populated - { NULL, 0, 0 }, }, nullptr, nullptr, @@ -3864,7 +3787,6 @@ const Game Games[] = { "mpr-23019.ic19s", 0x9800000, 0x0800000 }, { "mpr-23020.ic20s", 0xa000000, 0x0800000 }, // IC21S not populated - { NULL, 0, 0 }, }, nullptr, nullptr, @@ -3898,7 +3820,6 @@ const Game Games[] = { "trf1ma13.6n", 0xd000000, 0x1000000 }, { "trf1ma14.6m", 0xe000000, 0x1000000 }, { "trf1ma15.6l", 0xf000000, 0x1000000 }, - { NULL, 0, 0 }, }, nullptr, &toukon4_inputs, @@ -3925,7 +3846,6 @@ const Game Games[] = { "mpr-22032.ic8", 0x4000000, 0x0800000 }, { "mpr-22033.ic9", 0x4800000, 0x0800000 }, { "mpr-22034.ic10",0x5000000, 0x0800000 }, - { NULL, 0, 0 }, }, nullptr, &toyfight_inputs, @@ -3964,9 +3884,9 @@ const Game Games[] = { "mpr-22946.ic19s", 0x9800000, 0x0800000 }, { "mpr-22947.ic20s", 0xa000000, 0x0800000 }, { "mpr-22948.ic21s", 0xa800000, 0x0800000 }, - { NULL, 0, 0 }, - } - // PASS, SHOOT + }, + nullptr, + &virnba_inputs, }, // Virtua NBA { @@ -4001,8 +3921,9 @@ const Game Games[] = { "mpr-22946.ic19s", 0x9800000, 0x0800000 }, { "mpr-22947.ic20s", 0xa000000, 0x0800000 }, { "mpr-22948.ic21s", 0xa800000, 0x0800000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &virnba_inputs, }, // Virtua NBA (prototype) { @@ -4038,6 +3959,8 @@ const Game Games[] = { "rom20.ic20s", 0xa000000, 0x0800000, 0x9a08015e }, { "rom21.ic21s", 0xa800000, 0x0800000, 0x194594f2 }, }, + nullptr, + &virnba_inputs, }, // Virtua NBA (prototype, 15.11) { @@ -4072,8 +3995,9 @@ const Game Games[] = { "rom19.ic19s", 0x9800000, 0x0800000 }, { "rom20.ic20s", 0xa000000, 0x0800000 }, { "rom21.ic21s", 0xa800000, 0x0800000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &virnba_inputs, }, // Virtual On Oratorio Tangram M.S.B.S. ver5.66 2000 Edition { @@ -4100,7 +4024,6 @@ const Game Games[] = { "mpr-23192.ic11", 0x5800000, 0x800000 }, { "mpr-23193.ic12s", 0x6000000, 0x800000 }, { "mpr-23194.ic13s", 0x6800000, 0x800000 }, - { NULL, 0, 0 }, }, NULL, &vonot_inputs, @@ -4133,9 +4056,9 @@ const Game Games[] = { "mpr-21921.ic13s",0x6800000, 0x0800000 }, { "mpr-21922.ic14s",0x7000000, 0x0800000 }, { "mpr-21923.ic15s",0x7800000, 0x0400000 }, - { NULL, 0, 0 }, - } - // LONG PASS, SHOOT, SHORT PASS + }, + nullptr, + &vs2_2k_inputs, }, // Virtua Striker 2 Ver. 2000 { @@ -4164,8 +4087,9 @@ const Game Games[] = { "mpr-21921.ic13s", 0x6800000, 0x0800000 }, { "mpr-21922.ic14s", 0x7000000, 0x0800000 }, { "mpr-21923.ic15s", 0x7800000, 0x0400000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &vs2_2k_inputs, }, // Virtua Tennis (USA, EXP, KOR, AUS) / Power Smash (JPN) { @@ -4190,7 +4114,6 @@ const Game Games[] = { "mpr-22924.ic9", 0x4800000, 0x0800000 }, { "mpr-22925.ic10",0x5000000, 0x0800000 }, { "mpr-22926.ic11",0x5800000, 0x0800000 }, - { NULL, 0, 0 }, }, nullptr, &shot12_inputs, @@ -4214,7 +4137,6 @@ const Game Games[] = { "mpr-23722.ic4", 0x3800000, 0x1000000 }, { "mpr-23723.ic5", 0x4800000, 0x1000000 }, { "mpr-23724.ic6", 0x5800000, 0x1000000 }, - { NULL, 0, 0 }, }, NULL, &wrungp_inputs, @@ -4243,8 +4165,6 @@ const Game Games[] = // 838-12801 DRIVE BD WAVERUNNER //ROM_REGION( 0x10000, "motorio", ROMREGION_ERASEFF) //ROM_LOAD( "epr-19250.ic8", 0x000000, 0x010000, CRC(542d3836) SHA1(128cb0bfaf05791d219437653002f6bb948a4ad5) ) - - { NULL, 0, 0 }, }, NULL, &wrungp_inputs, @@ -4272,9 +4192,9 @@ const Game Games[] = { "wk1ma8.4e", 0x8000000, 0x1000000 }, { "wk1ma9.4d", 0x9000000, 0x1000000 }, { "wk1ma10.4c", 0xa000000, 0x1000000 }, - { NULL, 0, 0 }, - } - // BUTTON, (n/a, n/a, ENTER) + }, + nullptr, + &wldkicks_inputs, // TODO some default eeprom/nvram would help }, // World Kicks (Japan, WK1 Ver.A) { @@ -4298,8 +4218,9 @@ const Game Games[] = { "wk1ma8.4e", 0x8000000, 0x1000000 }, { "wk1ma9.4d", 0x9000000, 0x1000000 }, { "wk1ma10.4c", 0xa000000, 0x1000000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &wldkicks_inputs, }, // World Kicks (US, WK3 Ver.A) { @@ -4323,8 +4244,9 @@ const Game Games[] = { "wk1ma8.4e", 0x8000000, 0x1000000 }, { "wk1ma9.4d", 0x9000000, 0x1000000 }, { "wk1ma10.4c", 0xa000000, 0x1000000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &wldkicks_inputs, }, // World Kicks PCB (Japan, WKC1 Ver.A) { @@ -4348,8 +4270,9 @@ const Game Games[] = { "wk1ma8.4e", 0x8000000, 0x1000000 }, { "wk1ma9.4d", 0x9000000, 0x1000000 }, { "wk1ma10.4c", 0xa000000, 0x1000000 }, - { NULL, 0, 0 }, - } + }, + nullptr, + &wldkickspcb_inputs, // TODO some default eeprom/nvram would help }, // World Kicks PCB (World, WKC2 Ver.A) { @@ -4373,9 +4296,9 @@ const Game Games[] = { "wk1ma8.4e", 0x8000000, 0x1000000 }, { "wk1ma9.4d", 0x9000000, 0x1000000 }, { "wk1ma10.4c", 0xa000000, 0x1000000 }, - { NULL, 0, 0 }, - } - // C BUTTON + }, + nullptr, + &wldkickspcb_inputs, }, // WWF Royal Rumble (JPN, USA, EXP, KOR, AUS) { @@ -4397,9 +4320,9 @@ const Game Games[] = { "mpr-22267.ic6", 0x5800000, 0x1000000 }, { "mpr-22268.ic7", 0x6800000, 0x1000000 }, { "mpr-22269.ic8", 0x7800000, 0x1000000 }, - { NULL, 0, 0 }, - } - // ATTACK, GRAPPLE, SUPPORT + }, + nullptr, + &wwfroyal_inputs, }, // Zero Gunner 2 { @@ -4418,7 +4341,6 @@ const Game Games[] = { "mpr-23686.ic3", 0x2800000, 0x1000000 }, { "mpr-23687.ic4", 0x3800000, 0x1000000 }, { "mpr-23688.ic5", 0x4800000, 0x1000000 }, - { NULL, 0, 0 }, }, nullptr, &shot12_inputs, @@ -4455,7 +4377,6 @@ const Game Games[] = { "mpr-21724.ic17s", 0x8800000, 0x0800000 }, { "mpr-21725.ic18s", 0x9000000, 0x0800000 }, { "mpr-21726.ic19s", 0x9800000, 0x0800000 }, - { NULL, 0, 0 }, }, NULL, &zombie_inputs, @@ -4493,7 +4414,6 @@ const Game Games[] = { "mpr-21724.ic17s",0x8800000, 0x0800000 }, { "mpr-21725.ic18s",0x9000000, 0x0800000 }, { "mpr-21726.ic19s",0x9800000, 0x0800000 }, - { NULL, 0, 0 }, }, NULL, &zombie_inputs, @@ -4535,8 +4455,9 @@ const Game Games[] = { "rom9.ic9s", 0x04800000, 0x00800000, 0x032cca1a }, { "rom10.ic10s", 0x05000000, 0x00800000, 0x6d094477 }, { "rom11.ic11s", 0x05800000, 0x00800000, 0x6c803ca0 }, - // ic 12-21 populated, empty }, + nullptr, + &dygolf_inputs, }, // Nittere Shiki! Mirai Yosou Studio / NTV Future Forecast Studio (Japan) { @@ -4589,9 +4510,7 @@ const Game Games[] = { "ic9.bin", 0x4000000, 0x4000000 }, // IC10 and IC11 Populated, Empty { "317-05130-jpn.ic3", 0, 0x800, 0x0000000, Key }, - { NULL, 0, 0 }, }, - // ATTACK1/2/3 nullptr, &ausfache_inputs, ausfache_eeprom_dump, @@ -4612,9 +4531,9 @@ const Game Games[] = { "fpr-24384.ic10", 0x8000000, 0x4000000, 0x2e9116c4 }, { "fpr-24385.ic11", 0xc000000, 0x4000000, 0x2b79f45d }, { "317-0495-com.ic3", 0, 0x800, 0xc229a59b, Key }, - { NULL, 0, 0 }, - } - // PUNCH, KICK, JUMP + }, + nullptr, + &asndynmt_inputs, }, // Asian Dynamite / Dynamite Deka EX (older) { @@ -4632,10 +4551,9 @@ const Game Games[] = { "fpr-24384.ic10", 0x8000000, 0x4000000, 0x2e9116c4 }, { "fpr-24385.ic11", 0xc000000, 0x4000000, 0x2b79f45d }, { "317-0495-com.ic3", 0, 0x800, 0xc229a59b, Key }, - - { NULL, 0, 0 }, - } - // PUNCH, KICK, JUMP + }, + nullptr, + &asndynmt_inputs, }, // Illvelo (Illmatic Envelope) (Japan) { @@ -4652,10 +4570,9 @@ const Game Games[] = { "fpr-24438.ic9", 0x4000000, 0x4000000 }, { "fpr-24439.ic10", 0x8000000, 0x4000000 }, { "317-5131-jpn.ic3", 0, 0x800, 0x0000000, Key }, - { NULL, 0, 0 }, }, - NULL, - NULL, // SHOT/DOLL/SPECIAL + nullptr, + &illvelo_inputs, illvelo_eeprom_dump }, // Manic Panic Ghosts! *** BAD DUMP *** @@ -4698,10 +4615,9 @@ const Game Games[] = { "ic9.bin", 0x4000000, 0x4000000 }, { "ic10.bin", 0x8000000, 0x4000000 }, { "317-5132-jpn.ic3", 0, 0x800, 0x0000000, Key }, - { NULL, 0, 0 }, }, NULL, - &mamonoro_inputs, // SHOT(A)/(B) + &mamonoro_inputs, mamonoro_eeprom_dump }, // Melty Blood Actress Again Version A (Japan, Rev A) @@ -4723,7 +4639,6 @@ const Game Games[] = { "ic12.bin", 0x10000000, 0x4000000 }, { "ic13.bin", 0x14000000, 0x4000000 }, { "317-5133-jpn.ic3", 0, 0x800, 0x0000000, Key }, // pic_readout - { NULL, 0, 0 }, }, nullptr, &meltyb_inputs, @@ -4747,7 +4662,6 @@ const Game Games[] = { "ic12.bin", 0x10000000, 0x4000000 }, { "ic13.bin", 0x14000000, 0x4000000 }, { "317-5133-jpn.ic3", 0, 0x800, 0x0000000, Key }, // pic_readout - { NULL, 0, 0 }, }, nullptr, &meltyb_inputs, @@ -4912,11 +4826,9 @@ const Game Games[] = { "ic8.bin", 0x0000000, 0x4000000 }, { "ic9.bin", 0x4000000, 0x4000000 }, { "317-5138-jpn.ic3", 0, 0x800, 0x0000000, Key }, - { NULL, 0, 0 }, }, NULL, - &radirgyn_inputs, - // SHOT (A)/(B)/(C) + &radirgy_inputs, }, // Rhythm Tengoku { @@ -4934,9 +4846,9 @@ const Game Games[] = { "fpr-24425.ic10", 0x08000000, 0x4000000 }, { "fpr-24426.ic11", 0x0c000000, 0x4000000 }, { "317-0503-jpn.ic3", 0, 0x800, 0x0000000, Key }, - { NULL, 0, 0 }, - } - // SHOT A/B + }, + nullptr, + &rhytngk_inputs, }, // Shooting Love 2007 (Japan) { @@ -4954,10 +4866,9 @@ const Game Games[] = { "fpr-24415.ic10", 0x8000000, 0x4000000 }, { "fpr-24416.ic11", 0xc000000, 0x4000000 }, { "317-5129-jpn.ic3", 0, 0x800, 0x0000000, Key }, - { NULL, 0, 0 }, }, - NULL, - NULL, // PUSH1/2/3 + nullptr, + &sl2007_inputs, sl2007_eeprom_dump }, // Touch De Zunou (Rev A) *** BAD DUMP *** @@ -4977,8 +4888,9 @@ const Game Games[] = //ROM_REGION( 0x800, "pic_readout", 0 ) //ROM_LOAD( "317-0435-jpn.ic3", 0, 0x800, BAD_DUMP CRC(b553d900) SHA1(ed1c3c2053f2c0e98cb5c4d99f93143a66c29e5c) ) { "317-0435-jpn.ic3", 0, 0x800, 0x0000000, Key }, - { NULL, 0, 0 }, - } + }, + nullptr, + nullptr, }, // Star Horse Progress Returns (satellite) { @@ -4993,7 +4905,6 @@ const Game Games[] = { { "fpr-24489.ic8", 0x00000000, 0x4000000 }, { "fpr-24490.ic9", 0x04000000, 0x4000000 }, - { NULL, 0, 0 }, } }, // Naomi GD Roms @@ -5012,7 +4923,8 @@ const Game Games[] = { "317-5091-jpn.pic", 0, 0x4000, 0xb71ede16 }, }, "gdl-0018", - }, + &azumanga_inputs, + }, // Border Down (Rev A) { "bdrdown", @@ -5028,7 +4940,7 @@ const Game Games[] = { "bdrdown-default-eeprom.bin", 0, 0x80, 0x5b19727c, Eeprom }, }, "gdl-0023a", - // SHOT, LASER, SPEED + &bdrdown_inputs, }, // Chaos Field (Japan) { @@ -5045,7 +4957,7 @@ const Game Games[] = { "cfield-default-eeprom.bin", 0, 0x80, 0xa7acb6bf, Eeprom }, }, "gdl-0025", - // TRG1/2/3 + &cfield_inputs, }, // Musapey's Choco Marker (Rev A) { @@ -5061,7 +4973,7 @@ const Game Games[] = { "317-5085-jpn.pic", 0, 0x4000, 0x677fd544 }, }, "gdl-0014a", - // BUTTON A/B + &button12_inputs, }, // Cleopatra Fortune Plus { @@ -5077,7 +4989,7 @@ const Game Games[] = { "317-5083-com.pic", 0, 0x4000, 0x096a0fc2 }, }, "gdl-0012", - // BUTTON 1/2 + &button12_inputs, }, // Confidential Mission { @@ -5093,7 +5005,7 @@ const Game Games[] = { "317-0298-com.pic", 0, 0x4000, 0x15971bf6 }, }, "gds-0001", - nullptr, + &lightgun_inputs, confmiss_eeprom_dump, }, // Capcom vs. SNK Millennium Fight 2000 Pro (Japan) @@ -5228,6 +5140,7 @@ const Game Games[] = { "317-0308-com.pic", 0, 0x4000, 0x5e1ef2c4 }, }, "gds-0009a", + &dygolf_inputs, }, // Guilty Gear XX { @@ -5361,8 +5274,7 @@ const Game Games[] = { "317-5081-jpn.pic", 0, 0x4000, 0x72ca4579 }, }, "gdl-0010", - // SHOT, CHANGE - nullptr, + &ikaruga_inputs, nullptr, }, // Jingi Storm - The Arcade (Japan) @@ -5379,8 +5291,7 @@ const Game Games[] = { "317-5122-jpn.pic", 0, 0x4000, 0x88983220 }, }, "gdl-0037", - // GUARD, PUNCH, KICK - nullptr, + &jingystm_inputs, jingystm_eeprom_dump, }, // Karous (Japan) @@ -5398,7 +5309,7 @@ const Game Games[] = { "karous-default-eeprom.bin", 0, 0x80, 0xb017451c, Eeprom }, }, "gdl-0040", - // SHOT, SWORD, SPECIAL same as radirgy + &radirgy_inputs, }, // La Keyboard { @@ -5430,6 +5341,7 @@ const Game Games[] = { "317-5115-jpn.pic", 0, 0x4000, 0xe5435e85 }, }, "gdl-0034", + nullptr, // TODO }, // Lupin The Third - The Shooting (Rev A) { @@ -5445,7 +5357,7 @@ const Game Games[] = { "317-0325-jpn.pic", 0, 0x4000, 0xf71cb2fc }, }, "gds-0018a", - nullptr, + &lightgun_inputs, lupinsho_eeprom_dump, }, // Lupin The Third - The Shooting @@ -5462,7 +5374,7 @@ const Game Games[] = { "317-0325-jpn.pic", 0, 0x4000, 0xf71cb2fc }, }, "gds-0018", - nullptr, + &lightgun_inputs, lupinsho_eeprom_dump, }, // Lupin The Third - The Typing (Rev A) @@ -5565,6 +5477,7 @@ const Game Games[] = { "moeru-default-eeprom.bin", 0, 0x80, 0x50ca079f, Eeprom }, }, "gdl-0013", + nullptr, // TODO }, // The Maze of the Kings { @@ -5580,7 +5493,7 @@ const Game Games[] = { "317-0333-com.pic", 0, 0x4000, 0x15fb7792 }, }, "gds-0022", - nullptr, + &lightgun_inputs, mok_eeprom_dump, }, // Monkey Ball @@ -5615,8 +5528,7 @@ const Game Games[] = // { "psyvar2-default-eeprom.bin", 0, 0x80, 0x9d8661f3, Eeprom }, }, "gdl-0024", - // SHOT, BOMB - nullptr, + &psyvariar_inputs, psyvar2_eeprom_dump, }, // Puyo Pop Fever (World) @@ -5634,7 +5546,7 @@ const Game Games[] = // { "puyofev-default-eeprom.bin", 0, 0x80, 0x42e5fd40, Eeprom }, }, "gds-0034", - nullptr, + &puyofev_inputs, puyofev_eeprom_dump, }, // Puyo Puyo Fever (Japan) @@ -5652,7 +5564,7 @@ const Game Games[] = // { "puyofev-default-eeprom.bin", 0, 0x80, 0x42e5fd40, Eeprom }, }, "gds-0031", - nullptr, + &puyofev_inputs, puyofev_eeprom_dump, }, // Puyo Puyo Fever (Prototype) @@ -5691,7 +5603,7 @@ const Game Games[] = { "copy", 0x00400000, 0xc00000, 0x0000000, Copy, 0x1000000 }, }, nullptr, - nullptr, + &puyofev_inputs, puyofev_eeprom_dump, }, // Quiz Keitai Q mode @@ -5709,6 +5621,7 @@ const Game Games[] = { "quizqgd-default-eeprom.bin", 0, 0x80, 0x46c10aa3, Eeprom }, }, "gdl-0017", + nullptr, // TODO }, // Radirgy (Japan, Rev A) { @@ -5725,7 +5638,7 @@ const Game Games[] = { "radirgy-default-eeprom.bin", 0, 0x80, 0x8d60a282, Eeprom }, }, "gdl-0032a", - // SHOT, SWORD, SPECIAL same as karous + &radirgy_inputs, }, // Radirgy (Japan) { @@ -5742,6 +5655,7 @@ const Game Games[] = { "radirgy-default-eeprom.bin", 0, 0x80, 0x8d60a282, Eeprom }, }, "gdl-0032", + &radirgy_inputs, }, // Senko No Ronde (Japan, Rev A) { @@ -5758,7 +5672,7 @@ const Game Games[] = // { "senko-default-eeprom.bin", 0, 0x80, 0xb3d3be09, Eeprom }, }, "gdl-0030a", - nullptr, + &senko_inputs, senko_eeprom_dump, }, // Senko No Ronde (Japan) @@ -5776,7 +5690,7 @@ const Game Games[] = // { "senkoo-default-eeprom.bin", 0, 0x80, 0xa2203a7f, Eeprom }, }, "gdl-0030", - nullptr, + &senko_inputs, senko_eeprom_dump, }, // Senko No Ronde Special (Export, Japan) @@ -5877,6 +5791,7 @@ const Game Games[] = { "shikgam2-default-eeprom.bin", 0, 0x80, 0x5fb60e27, Eeprom }, }, "gdl-0021", + &sl2007_inputs, }, // Slashout { @@ -5909,7 +5824,7 @@ const Game Games[] = { "317-0303-com.pic", 0, 0x4000, 0xb42999dd }, }, "gds-0005", - nullptr, // BEAT, CHARGE ,JUMP, SHIFT + &spkrbtl_inputs, spkrbtl_eeprom_dump }, // Sports Jam @@ -6021,8 +5936,7 @@ const Game Games[] = // { "trgheart-default-eeprom.bin", 0, 0x80, 0x7faff313, Eeprom }, }, "gdl-0036a", - // SHOT, ANCHOR, BOMB - nullptr, + &trgheart_inputs, trgheart_eeprom_dump, }, // Trigger Heart Exelica (Japan) @@ -6040,7 +5954,7 @@ const Game Games[] = // { "trgheart-default-eeprom.bin", 0, 0x80, 0x7faff313, Eeprom }, }, "gdl-0036", - nullptr, + &trgheart_inputs, trgheart_eeprom_dump, }, // Trizeal (Japan) @@ -6058,8 +5972,7 @@ const Game Games[] = // { "trizeal-default-eeprom.bin", 0, 0x80, 0xac0847ce, Eeprom }, }, "gdl-0026", - // PUSH1/2/3 - nullptr, + &sl2007_inputs, trizeal_eeprom_dump, }, // Under Defeat (Japan) @@ -6077,8 +5990,7 @@ const Game Games[] = // { "undefeat-default-eeprom.bin", 0, 0x80, 0x9d2b071c, Eeprom }, }, "gdl-0035", - // SHOT, BOMB - nullptr, + &psyvariar_inputs, undefeat_eeprom_dump, }, // Usagi - Yamashiro Mahjong Hen (Japan) @@ -6111,7 +6023,7 @@ const Game Games[] = { "317-0330-com.pic", 0, 0x4000, 0x33ccf2d1 }, }, "gds-0019", - // RUN1, ACTION, RUN2 + &vathlete_inputs, }, // Virtua Tennis 2 / Power Smash 2 (Rev A) { @@ -6127,7 +6039,7 @@ const Game Games[] = { "317-0318-com.pic", 0, 0x4000, 0x83de4047 }, }, "gds-0015a", - nullptr, + &shot12_inputs, vtennis2_eeprom_dump, }, // Virtua Tennis / Power Smash @@ -6403,7 +6315,9 @@ const Game Games[] = { "vera.u17", 0x05000000, 0x01000000, 0xd78389a4 }, { "vera.u14", 0x06000000, 0x01000000, 0x35df044f }, { "vera.u16", 0x07000000, 0x01000000, 0x3590072d }, - } + }, + nullptr, + &basschal_inputs, }, // Sega Bass Fishing Challenge { @@ -6424,7 +6338,9 @@ const Game Games[] = { "610-0811.u17", 0x05000000, 0x01000000, 0xdb799f5a }, { "610-0811.u14", 0x06000000, 0x01000000, 0xf2769383 }, { "vera.u16", 0x07000000, 0x01000000, 0x3590072d }, - } + }, + nullptr, + &basschal_inputs, }, // Block Pong-Pong { @@ -6465,7 +6381,9 @@ const Game Games[] = { "608-2161.u17", 0x5000000, 0x1000100, 0x2f973eb4 }, { "608-2161.u14", 0x6000000, 0x1000100, 0x2e7d966f }, { "608-2161.u16", 0x7000000, 0x1000100, 0x14f8ca87 }, - } + }, + nullptr, + &aw_lightgun_inputs, }, // Demolish Fist { @@ -6509,7 +6427,9 @@ const Game Games[] = { "695-0014.u17", 0x5000000, 0x1000000, 0x16bb5992 }, { "695-0014.u14", 0x6000000, 0x1000000, 0x55470242 }, { "695-0014.u16", 0x7000000, 0x1000000, 0x730180a4 }, - } + }, + nullptr, + &aw_shot123_inputs, }, // Dolphin Blue { @@ -6819,7 +6739,9 @@ const Game Games[] = { "ax1603m01.ic13", 0x3000000, 0x1000000, 0x7c0aa241 }, { "ax1604m01.ic14", 0x4000000, 0x1000000, 0xd2369144 }, { "ax1605m01.ic15", 0x5000000, 0x1000000, 0x0c11c1f9 }, - } + }, + nullptr, + &aw_lightgun_inputs, }, // The Rumble Fish { @@ -6946,7 +6868,9 @@ const Game Games[] = { "ax1405m01.ic15", 0x5000000, 0x1000000, 0xb548446f }, { "ax1406m01.ic16", 0x6000000, 0x1000000, 0x437673e6 }, { "ax1407m01.ic17", 0x7000000, 0x1000000, 0x6b6acc0a }, - } + }, + nullptr, + nullptr, // TODO }, // Samurai Shodown VI / Samurai Spirits Tenkaichi Kenkakuden { @@ -6987,7 +6911,9 @@ const Game Games[] = { "ax0102m01.ic12", 0x2000000, 0x1000000, 0x700764d1 }, { "ax0103m01.ic13", 0x3000000, 0x1000000, 0x6144e7a8 }, { "ax0104m01.ic14", 0x4000000, 0x1000000, 0xccb72150 }, - } + }, + nullptr, + &aw_lightgun_inputs, // PUMP is mercury sensor when gun is pointing down }, // Sushi Bar { @@ -7045,11 +6971,6 @@ const Game Games[] = { { "u3", 0x0000000, 0x1000000, 0x7acfb499 }, { "u1", 0x1000000, 0x1000000, 0xb3c1c3bb }, - // garbage data not used by this game, match anmlbskta U4 - //{ "u4", 0x2000000, 0x1000000, 0x646e9773 }, - // garbage data not used by this game, match anmlbskta U2 - //{ "u2", 0x3000000, 0x1000000, 0xb9162d97 }, - // U14-U17 not populated } }, // Extreme Hunting @@ -7070,7 +6991,9 @@ const Game Games[] = { "ax2404m01.ic14", 0x4000000, 0x1000000, 0x759ef5cb }, { "ax2405m01.ic15", 0x5000000, 0x1000000, 0x940d77f1 }, { "ax2406m01.ic16", 0x6000000, 0x1000000, 0xcbcf2c5d }, - } + }, + nullptr, + &aw_lightgun_inputs, }, // Extreme Hunting 2 { @@ -7095,7 +7018,9 @@ const Game Games[] = //ROM_REGION( 0x1400000, "network", 0) // network board //ROM_LOAD( "fpr-24330a.ic2", 0x000000, 0x400000, CRC(8d89877e) SHA1(6caafc49114eb0358e217bc2d1a3ab58a93c8d19) ) //ROM_LOAD( "flash128.ic4s", 0x400000, 0x1000000, CRC(866ed675) SHA1(2c4c06935b7ab1876e640cede51713b841833567) ) - } + }, + nullptr, + &aw_lightgun_inputs, }, // // Naomi 2 @@ -7122,7 +7047,9 @@ const Game Games[] = { "mpr-23660.ic9", 0x8800000, 0x1000000, 0xe49e65f5 }, { "mpr-23661.ic10", 0x9800000, 0x1000000, 0x7d44dc74 }, { "mpr-23662.ic11", 0xa800000, 0x0800000, 0xd6ef7d68 }, - } + }, + nullptr, + &vs2_2k_inputs, }, { "vstrik3c", @@ -7146,7 +7073,9 @@ const Game Games[] = { "mpr-23660.ic9", 0x8800000, 0x1000000, 0xe49e65f5 }, { "mpr-23661.ic10", 0x9800000, 0x1000000, 0x7d44dc74 }, { "mpr-23662.ic11", 0xa800000, 0x0800000, 0xd6ef7d68 }, - } + }, + nullptr, + &vs2_2k_inputs, }, { "wldrider", @@ -7675,6 +7604,7 @@ const Game Games[] = { "317-0304-com.bin", 0, 0x4000, 0x8e82d17a }, }, "gds-0006", + &vs2_2k_inputs, }, { "vf4o", @@ -7737,6 +7667,7 @@ const Game Games[] = { "317-0317-com.pic", 0, 0x4000, 0xef65fe73 }, }, "gds-0014", + &beachspi_inputs, }, { "initd", @@ -8323,6 +8254,8 @@ const Game Games[] = { "ic63", 0x04000000, 0x4000000, 0xcb946213 }, { "317-0604-com.ic15", 0, 0x800, 0xa46dfd47, Key }, }, + nullptr, + &tetgiant_inputs, }, { "unomedal", @@ -8497,6 +8430,7 @@ const Game Games[] = { "317-0604-com.ic15", 0, 0x800, 0xa46dfd47, Key }, }, "mda-c0076", + &tetgiant_inputs, }, { nullptr diff --git a/core/hw/naomi/naomi_roms_input.h b/core/hw/naomi/naomi_roms_input.h index 2b21962fa..7151bfb4d 100644 --- a/core/hw/naomi/naomi_roms_input.h +++ b/core/hw/naomi/naomi_roms_input.h @@ -30,15 +30,25 @@ { NAOMI_TEST_KEY, "" }, \ { NAOMI_SERVICE_KEY, "" }, #define NAO_START_DESC { NAOMI_START_KEY, "" }, +#define NAO_DPAD_DESC { NAOMI_UP_KEY, "" }, \ + { NAOMI_DOWN_KEY, "" }, \ + { NAOMI_LEFT_KEY, "" }, \ + { NAOMI_RIGHT_KEY, "" }, \ + +#define INPUT_1_BUTTON(btn0) { \ + { \ + { NAOMI_BTN0_KEY, btn0 }, \ + NAO_DPAD_DESC \ + NAO_START_DESC \ + NAO_BASE_BTN_DESC \ + } \ +} #define INPUT_2_BUTTONS(btn0, btn1) { \ { \ { NAOMI_BTN0_KEY, btn0 }, \ { NAOMI_BTN1_KEY, btn1 }, \ - { NAOMI_UP_KEY, "" }, \ - { NAOMI_DOWN_KEY, "" }, \ - { NAOMI_LEFT_KEY, "" }, \ - { NAOMI_RIGHT_KEY, "" }, \ + NAO_DPAD_DESC \ NAO_START_DESC \ NAO_BASE_BTN_DESC \ } \ @@ -49,10 +59,7 @@ { NAOMI_BTN0_KEY, btn0 }, \ { NAOMI_BTN1_KEY, btn1 }, \ { NAOMI_BTN2_KEY, btn2 }, \ - { NAOMI_UP_KEY, "" }, \ - { NAOMI_DOWN_KEY, "" }, \ - { NAOMI_LEFT_KEY, "" }, \ - { NAOMI_RIGHT_KEY, "" }, \ + NAO_DPAD_DESC \ NAO_START_DESC \ NAO_BASE_BTN_DESC \ } \ @@ -64,10 +71,7 @@ { NAOMI_BTN1_KEY, btn1 }, \ { NAOMI_BTN2_KEY, btn2 }, \ { NAOMI_BTN3_KEY, btn3 }, \ - { NAOMI_UP_KEY, "" }, \ - { NAOMI_DOWN_KEY, "" }, \ - { NAOMI_LEFT_KEY, "" }, \ - { NAOMI_RIGHT_KEY, "" }, \ + NAO_DPAD_DESC \ NAO_START_DESC \ NAO_BASE_BTN_DESC \ } \ @@ -80,15 +84,18 @@ { NAOMI_BTN2_KEY, btn2 }, \ { NAOMI_BTN3_KEY, btn3 }, \ { NAOMI_BTN5_KEY, btn4, NAOMI_BTN4_KEY }, \ - { NAOMI_UP_KEY, "" }, \ - { NAOMI_DOWN_KEY, "" }, \ - { NAOMI_LEFT_KEY, "" }, \ - { NAOMI_RIGHT_KEY, "" }, \ + NAO_DPAD_DESC \ NAO_START_DESC \ NAO_BASE_BTN_DESC \ } \ } +static InputDescriptors service_btns_inputs = { + { + NAO_BASE_BTN_DESC + } +}; + static InputDescriptors _18wheelr_inputs = { { { NAOMI_BTN0_KEY, "HORN" }, @@ -145,10 +152,7 @@ static InputDescriptors capcom_4btn_inputs = { { NAOMI_BTN1_KEY, "HEAVY PUNCH" }, { NAOMI_BTN3_KEY, "LIGHT KICK" }, { NAOMI_BTN4_KEY, "HEAVY KICK" }, - { NAOMI_UP_KEY, "" }, - { NAOMI_DOWN_KEY, "" }, - { NAOMI_LEFT_KEY, "" }, - { NAOMI_RIGHT_KEY, "" }, + NAO_DPAD_DESC \ NAO_START_DESC NAO_BASE_BTN_DESC }, @@ -162,10 +166,7 @@ static InputDescriptors capcom_6btn_inputs = { { NAOMI_BTN3_KEY, "LIGHT KICK" }, { NAOMI_BTN4_KEY, "MEDIUM KICK" }, { NAOMI_BTN5_KEY, "HEAVY KICK" }, - { NAOMI_UP_KEY, "" }, - { NAOMI_DOWN_KEY, "" }, - { NAOMI_LEFT_KEY, "" }, - { NAOMI_RIGHT_KEY, "" }, + NAO_DPAD_DESC \ NAO_START_DESC NAO_BASE_BTN_DESC }, @@ -193,9 +194,10 @@ static InputDescriptors toyfight_inputs = INPUT_3_BUTTONS("Punch", "Kick", "Dodg static InputDescriptors ausfache_inputs = INPUT_3_BUTTONS("Weak Attack", "Medium Attack", "Strong Attack"); -static InputDescriptors trigger_inputs = { +static InputDescriptors lightgun_inputs = { { { NAOMI_BTN0_KEY, "TRIGGER" }, + { NAOMI_RELOAD_KEY, "" }, NAO_START_DESC NAO_BASE_BTN_DESC }, @@ -241,10 +243,7 @@ static InputDescriptors mvsc2_inputs = { { NAOMI_BTN3_KEY, "LIGHT KICK" }, { NAOMI_BTN4_KEY, "STRONG KICK" }, { NAOMI_BTN5_KEY, "ASSIST B" }, - { NAOMI_UP_KEY, "" }, - { NAOMI_DOWN_KEY, "" }, - { NAOMI_LEFT_KEY, "" }, - { NAOMI_RIGHT_KEY, "" }, + NAO_DPAD_DESC \ NAO_START_DESC NAO_BASE_BTN_DESC }, @@ -255,6 +254,7 @@ static InputDescriptors ninjaslt_inputs = { { NAOMI_BTN2_KEY, "ENTER", NAOMI_BTN0_KEY }, { NAOMI_START_KEY, "", NAOMI_BTN2_KEY, 0, NAOMI_BTN3_KEY }, { NAOMI_BTN0_KEY, "TRIGGER", NAOMI_BTN4_KEY, 0, NAOMI_BTN5_KEY }, + { NAOMI_RELOAD_KEY, "" }, { NAOMI_UP_KEY, "SELECT UP" }, { NAOMI_DOWN_KEY, "SELECT DOWN" }, NAO_BASE_BTN_DESC @@ -301,7 +301,7 @@ static InputDescriptors pstone2_inputs = INPUT_3_BUTTONS("Punch", "Jump", "Attac static InputDescriptors shot1234_inputs = INPUT_4_BUTTONS("SHOT1", "SHOT2", "SHOT3", "SHOT4"); -static InputDescriptors radirgyn_inputs = INPUT_3_BUTTONS("SHOOT", "SWORD", "SHIELD/SPECIAL"); +static InputDescriptors radirgy_inputs = INPUT_3_BUTTONS("SHOOT", "SWORD", "SHIELD/SPECIAL"); static InputDescriptors mamonoro_inputs = INPUT_2_BUTTONS("SHOOT", "SPECIAL"); @@ -406,10 +406,7 @@ static InputDescriptors zombie_inputs = { { NAOMI_BTN0_KEY, "L" }, { NAOMI_BTN1_KEY, "R" }, { NAOMI_BTN2_KEY, "G" }, - { NAOMI_UP_KEY, "" }, - { NAOMI_DOWN_KEY, "" }, - { NAOMI_LEFT_KEY, "" }, - { NAOMI_RIGHT_KEY, "" }, + NAO_DPAD_DESC \ NAO_START_DESC NAO_BASE_BTN_DESC }, @@ -470,6 +467,7 @@ static InputDescriptors guilty_gear_inputs = INPUT_5_BUTTONS("KICK", "SLASH", "H static InputDescriptors ggx_inputs = INPUT_4_BUTTONS("PUNCH", "KICK", "SLASH", "HSLASH"); +static InputDescriptors senko_inputs = INPUT_3_BUTTONS("ACTION", "MAIN", "SUB"); static InputDescriptors senkosp_inputs = INPUT_5_BUTTONS("MAIN", "SUB", "MAIN+SUB", "ACTION", "OVER DRIVE"); static InputDescriptors meltyb_inputs = INPUT_5_BUTTONS("LAttack", "MAttack", "HAttack", "Guard", "Quick Action"); @@ -496,6 +494,34 @@ static InputDescriptors shootout_inputs = { static InputDescriptors vf4_inputs = INPUT_3_BUTTONS("PUNCH", "KICK", "GUARD"); +static InputDescriptors crackindj_inputs = { + { + NAO_START_DESC + NAO_BASE_BTN_DESC + }, + { + { "FADER", Full, 0, true }, + }, +}; + +static InputDescriptors shaktam_inputs = { + { + NAO_START_DESC + NAO_BASE_BTN_DESC + { NAOMI_BTN0_KEY, "SHAKE L" }, + { NAOMI_BTN1_KEY, "SHAKE R" }, + { NAOMI_BTN2_KEY, "KNOCK", NAOMI_DOWN_KEY }, + { NAOMI_DOWN_KEY, "DOWN", NAOMI_LEFT_KEY }, + { NAOMI_UP_KEY, "UP", NAOMI_RIGHT_KEY }, + }, + { + { "TAMBOURINE X", Full, 0 }, + { "TAMBOURINE Y", Full, 1 }, + { "", Full, 2 }, // unused but P2 starts at axis 4 + { "", Full, 3 }, // unused but P2 starts at axis 4 + }, +}; + static InputDescriptors mushik_inputs = { { { NAOMI_BTN0_KEY, "HIT" }, @@ -505,6 +531,98 @@ static InputDescriptors mushik_inputs = { }, }; +static InputDescriptors csmash_inputs = INPUT_2_BUTTONS("SMASH", "JUMP"); +static InputDescriptors otrigger_inputs = INPUT_3_BUTTONS("TRIGGER", "CHANGE", "JUMP"); +static InputDescriptors puyoda_inputs = INPUT_1_BUTTON("STAR"); +static InputDescriptors sgtetris_inputs = INPUT_2_BUTTONS("SW1", "SW2"); +static InputDescriptors virnba_inputs = INPUT_2_BUTTONS("PASS", "SHOOT"); +static InputDescriptors vs2_2k_inputs = INPUT_3_BUTTONS("LONG PASS", "SHOOT", "SHORT PASS"); +static InputDescriptors wwfroyal_inputs = INPUT_3_BUTTONS("ATTACK", "GRAPPLE", "SUPPORT"); +static InputDescriptors asndynmt_inputs = INPUT_3_BUTTONS("PUNCH", "KICK", "JUMP"); +static InputDescriptors illvelo_inputs = INPUT_3_BUTTONS("SHOT", "DOLL", "SPECIAL"); +static InputDescriptors rhytngk_inputs = INPUT_2_BUTTONS("SHOT A", "SHOT B"); +static InputDescriptors sl2007_inputs = INPUT_3_BUTTONS("PUSH 1", "PUSH 2", "PUSH 3"); +static InputDescriptors azumanga_inputs = INPUT_1_BUTTON("BUTTON A"); +static InputDescriptors bdrdown_inputs = INPUT_3_BUTTONS("SHOT", "LASER", "SPEED"); +static InputDescriptors cfield_inputs = INPUT_3_BUTTONS("TRG1", "TRG2", "TRG3"); +static InputDescriptors button12_inputs = INPUT_2_BUTTONS("BUTTON 1", "BUTTON 2"); +static InputDescriptors ikaruga_inputs = INPUT_2_BUTTONS("SHOT", "CHANGE"); +static InputDescriptors jingystm_inputs = INPUT_3_BUTTONS("GUARD", "PUNCH", "KICK"); +static InputDescriptors psyvariar_inputs = INPUT_2_BUTTONS("SHOT", "BOMB"); +static InputDescriptors puyofev_inputs = INPUT_2_BUTTONS("ROTATE1", "ROTATE2"); +static InputDescriptors spkrbtl_inputs = INPUT_4_BUTTONS("BEAT", "CHARGE", "JUMP", "SHIFT"); +static InputDescriptors trgheart_inputs = INPUT_3_BUTTONS("SHOT", "ANCHOR", "BOMB"); +static InputDescriptors vathlete_inputs = INPUT_3_BUTTONS("RUN1", "ACTION", "RUN2"); + +static InputDescriptors samba_inputs = { + { + { NAOMI_BTN0_KEY, "MARACAS R" }, + { NAOMI_BTN1_KEY, "MARACAS L" }, + NAO_START_DESC + NAO_BASE_BTN_DESC + }, + { + { "MARACAS R X", Full, 0 }, + { "MARACAS R Y", Full, 1 }, + { "MARACAS L X", Full, 2 }, + { "MARACAS L Y", Full, 3 }, + } +}; + +static InputDescriptors wldkicks_inputs = { + { + { NAOMI_BTN0_KEY, "BUTTON" }, + { NAOMI_BTN3_KEY, "ENTER" }, // service mode + { NAOMI_UP_KEY, "" }, // service mode + { NAOMI_DOWN_KEY, "" }, // service mode + NAO_BASE_BTN_DESC + }, + { + { "STICK L/R", Full, 0 }, // P1 + { "STICK U/D", Full, 1 }, + { "", Full, 2 }, // P2 + { "", Full, 3 }, + { "", Full, 4 }, // P3 + { "", Full, 5 }, + { "", Full, 6 }, // P4 + { "", Full, 7 }, + { "KICK", Full, 8 }, // P1 FIXME need to set Full here to have read_analog_axis() called but not seen as trigger + { "", Full, 9 }, // P2 + { "", Full, 10 }, // P3 + { "", Full, 11 }, // P4 + } +}; +static InputDescriptors wldkickspcb_inputs = { + { + { NAOMI_BTN0_KEY, "CHANGE" }, // original label: C BUTTON + NAO_START_DESC + NAO_BASE_BTN_DESC + }, + { + { "STICK L/R", Full, 0 }, + { "STICK U/D", Full, 1 }, + { "", Full, 2 }, +// { "", Full, 3 }, + { "BALL", Half, 4 }, // this is wrong, just to indicate RT is used + } +}; + +static InputDescriptors dygolf_inputs = { + { + NAO_DPAD_DESC + NAO_START_DESC + NAO_BASE_BTN_DESC + } +}; + +static InputDescriptors kick4csh_inputs = { + { + { NAOMI_BTN1_KEY, "VIEW" }, + { NAOMI_BTN2_KEY, "CHANCE" }, + { NAOMI_START_KEY, "START/DECIDE" }, + NAO_BASE_BTN_DESC + } +}; // // AtomisWave games // @@ -513,6 +631,11 @@ static InputDescriptors mushik_inputs = { { AWAVE_TEST_KEY, "" }, \ { AWAVE_SERVICE_KEY, "" }, #define AW_START_DESC { AWAVE_START_KEY, "" }, +#define AW_DPAD_DESC { AWAVE_UP_KEY, "" }, \ + { AWAVE_DOWN_KEY, "" }, \ + { AWAVE_LEFT_KEY, "" }, \ + { AWAVE_RIGHT_KEY, "" }, + #define AW_5_BUTTONS(btn0, btn1, btn2, btn3, btn4) { \ { \ @@ -521,10 +644,7 @@ static InputDescriptors mushik_inputs = { { AWAVE_BTN2_KEY, btn2 }, \ { AWAVE_BTN3_KEY, btn3 }, \ { AWAVE_BTN4_KEY, btn4 }, \ - { AWAVE_UP_KEY, "" }, \ - { AWAVE_DOWN_KEY, "" }, \ - { AWAVE_LEFT_KEY, "" }, \ - { AWAVE_RIGHT_KEY, "" }, \ + AW_DPAD_DESC \ AW_START_DESC \ AW_BASE_BTN_DESC \ } \ @@ -536,10 +656,7 @@ static InputDescriptors mushik_inputs = { { AWAVE_BTN1_KEY, btn1 }, \ { AWAVE_BTN2_KEY, btn2 }, \ { AWAVE_BTN3_KEY, btn3 }, \ - { AWAVE_UP_KEY, "" }, \ - { AWAVE_DOWN_KEY, "" }, \ - { AWAVE_LEFT_KEY, "" }, \ - { AWAVE_RIGHT_KEY, "" }, \ + AW_DPAD_DESC \ AW_START_DESC \ AW_BASE_BTN_DESC \ } \ @@ -550,10 +667,7 @@ static InputDescriptors mushik_inputs = { { AWAVE_BTN0_KEY, btn0 }, \ { AWAVE_BTN1_KEY, btn1 }, \ { AWAVE_BTN2_KEY, btn2 }, \ - { AWAVE_UP_KEY, "" }, \ - { AWAVE_DOWN_KEY, "" }, \ - { AWAVE_LEFT_KEY, "" }, \ - { AWAVE_RIGHT_KEY, "" }, \ + AW_DPAD_DESC \ AW_START_DESC \ AW_BASE_BTN_DESC \ } \ @@ -625,6 +739,32 @@ static InputDescriptors mslug6_inputs = AW_5_BUTTONS("SHOOT", "JUMP", "GRENADE", static InputDescriptors rumblef_inputs = AW_5_BUTTONS("LP", "SP", "Dodge", "LK", "SK"); +static InputDescriptors basschal_inputs = { + { + { AWAVE_BTN0_KEY, "ROTATE LEFT" }, + { AWAVE_BTN1_KEY, "ROTATE RIGHT" }, + { AWAVE_LEFT_KEY, "LEFT POINT" }, + { AWAVE_RIGHT_KEY, "RIGHT POINT" }, + AW_START_DESC + AW_BASE_BTN_DESC + }, +}; + +static InputDescriptors aw_lightgun_inputs = { + { + { AWAVE_BTN0_KEY, "TRIGGER" }, + { AWAVE_BTN1_KEY, "PUMP" }, + AW_START_DESC + AW_BASE_BTN_DESC + }, +}; + +static InputDescriptors aw_shot123_inputs = AW_3_BUTTONS("SHOT1", "SHOT2", "SHOT3"); + +// +// Naomi 2 +// + static InputDescriptors kingrt66_inputs = { { { NAOMI_BTN0_KEY, "HORN" }, @@ -745,33 +885,7 @@ static InputDescriptors drvsim_inputs = { }, }; -static InputDescriptors crackindj_inputs = { - { - NAO_START_DESC - NAO_BASE_BTN_DESC - }, - { - { "FADER", Full, 0, true }, - }, -}; - -static InputDescriptors shaktam_inputs = { - { - NAO_START_DESC - NAO_BASE_BTN_DESC - { NAOMI_BTN0_KEY, "SHAKE L" }, - { NAOMI_BTN1_KEY, "SHAKE R" }, - { NAOMI_BTN2_KEY, "KNOCK", NAOMI_DOWN_KEY }, - { NAOMI_DOWN_KEY, "DOWN", NAOMI_LEFT_KEY }, - { NAOMI_UP_KEY, "UP", NAOMI_RIGHT_KEY }, - }, - { - { "TAMBOURINE X", Full, 0 }, - { "TAMBOURINE Y", Full, 1 }, - { "", Full, 2 }, // unused but P2 starts at axis 4 - { "", Full, 3 }, // unused but P2 starts at axis 4 - }, -}; +static InputDescriptors beachspi_inputs = INPUT_2_BUTTONS("A", "B"); // // System SP games @@ -793,3 +907,16 @@ static InputDescriptors lovebery_inputs = { NAO_BASE_BTN_DESC }, }; + +static InputDescriptors tetgiant_inputs = { + { + { DC_BTN_A, "BUTTON L" }, + { DC_BTN_B, "BUTTON R" }, + { DC_DPAD_UP, "" }, + { DC_DPAD_DOWN, "" }, + { DC_DPAD_LEFT, "" }, + { DC_DPAD_RIGHT, "" }, + { DC_BTN_START, "" }, + NAO_BASE_BTN_DESC + }, +}; From bb1b1059d4cb9f6ecf60018fc2ddfb93f532319e Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 11 Dec 2024 10:37:48 +0100 Subject: [PATCH 48/81] sh4: SCIF DR bit can't be reset if rx fifo isn't empty Fixes manic panic periodic touchscreen errors. --- core/hw/sh4/modules/serial.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/hw/sh4/modules/serial.cpp b/core/hw/sh4/modules/serial.cpp index 3c1da16f2..1fd88b496 100644 --- a/core/hw/sh4/modules/serial.cpp +++ b/core/hw/sh4/modules/serial.cpp @@ -120,7 +120,6 @@ void SCIFSerialPort::rxSched() } } } - // TODO fifo might have been emptied since last rx else if (!rxFifo.empty()) { setStatusBit(DR); @@ -192,6 +191,8 @@ void SCIFSerialPort::writeStatus(u16 data) data |= RDF; if (isTDFE()) data |= TDFE; + if (!rxFifo.empty()) + data |= DR; SCIF_LOG("SCIF_SCFSR2.reset %s%s%s%s%s%s%s%s", (data & ER) ? "" : "ER ", (data & TEND) ? "" : "TEND ", From 67eee05c141284612f8cea318a6d2e92d3adfb89 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 11 Dec 2024 10:41:53 +0100 Subject: [PATCH 49/81] lightgun: wiggle the reported lightgun position in a 2x2 square Fixes claychal not registering shots in menus. --- core/hw/pvr/spg.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/hw/pvr/spg.cpp b/core/hw/pvr/spg.cpp index 5fd93bc86..0e2115c77 100755 --- a/core/hw/pvr/spg.cpp +++ b/core/hw/pvr/spg.cpp @@ -235,7 +235,7 @@ static int spg_line_sched(int tag, int cycles, int jitter, void *arg) void read_lightgun_position(int x, int y) { - static u8 flip; + static u32 flip; maple_int_pending = true; if (y < 0 || y >= 480 || x < 0 || x >= 640) { @@ -245,9 +245,11 @@ void read_lightgun_position(int x, int y) else { lightgun_line = y / (SPG_CONTROL.interlace ? 2 : 1) + SPG_VBLANK_INT.vblank_out_interrupt_line_number; - // For some reason returning the same position twice makes it register off screen - lightgun_hpos = (x + 286) ^ flip; - flip ^= 1; + // For some reason returning the same position twice makes it register off screen. + // Atomiswave Clay Challenge wants more wiggle than others for shots to register. + lightgun_line ^= (flip >> 1) & 1; + lightgun_hpos = (x + 286) ^ (flip & 1); + flip++; } } From f5389bcd0bbc6ac4bc738769fedea38fd63a96db Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 11 Dec 2024 11:04:44 +0100 Subject: [PATCH 50/81] battle cable: notify user when data first received from peer Document SCIF register bits. --- core/hw/sh4/sh4_mmr.h | 74 ++++++++++++++++++------------------- core/network/net_platform.h | 2 +- core/network/null_modem.h | 15 +++++++- 3 files changed, 52 insertions(+), 39 deletions(-) diff --git a/core/hw/sh4/sh4_mmr.h b/core/hw/sh4/sh4_mmr.h index e9967722d..6b6e554c7 100644 --- a/core/hw/sh4/sh4_mmr.h +++ b/core/hw/sh4/sh4_mmr.h @@ -1140,12 +1140,12 @@ union SCIF_SCSMR2_type { struct { - u32 CKS : 2; + u32 CKS : 2; // Clock Select u32 : 1; - u32 STOP : 1; - u32 OE_paritymode : 1; - u32 PE : 1; - u32 CHR : 1; + u32 STOP : 1; // Stop Bit Length + u32 OE_paritymode : 1; // Parity Mode + u32 PE : 1; // Parity Enable + u32 CHR : 1; // Character Length u32 : 9; //16 }; @@ -1163,13 +1163,13 @@ union SCIF_SCSCR2_type struct { u32 : 1; - u32 CKE1 : 1; + u32 CKE1 : 1; // Clock Enable 1 u32 : 1; - u32 REIE : 1; - u32 RE : 1; - u32 TE : 1; - u32 RIE : 1; - u32 TIE : 1; + u32 REIE : 1; // Receive Error Interrupt Enable + u32 RE : 1; // Receive Enable + u32 TE : 1; // Transmit Enable + u32 RIE : 1; // Receive Interrupt Enable + u32 TIE : 1; // Transmit Interrupt Enable //8 u32 : 8; //16 @@ -1186,17 +1186,17 @@ union SCIF_SCFSR2_type { struct { - u32 DR : 1; - u32 RDF : 1; - u32 PER : 1; - u32 FER : 1; - u32 BRK : 1; - u32 TDFE : 1; - u32 TEND : 1; - u32 ER : 1; + u32 DR : 1; // Receive Data Ready + u32 RDF : 1; // Receive FIFO Data Full + u32 PER : 1; // Parity Error + u32 FER : 1; // Framing Error + u32 BRK : 1; // Break Detect + u32 TDFE : 1; // Transmit FIFO Data Empty + u32 TEND : 1; // Transmit End + u32 ER : 1; // Receive Error //8 - u32 FERn : 4; - u32 PERn : 4; + u32 FERn : 4; // Number of Framing Errors + u32 PERn : 4; // Number of Parity Errors //16 }; u16 full; @@ -1211,12 +1211,12 @@ union SCIF_SCFCR2_type { struct { - u32 LOOP : 1; - u32 RFRST : 1; - u32 TFRST : 1; - u32 MCE : 1; - u32 TTRG : 2; - u32 RTRG : 2; + u32 LOOP : 1; // Loopback Test + u32 RFRST : 1; // Receive FIFO Data Register Reset + u32 TFRST : 1; // Transmit FIFO Data Register Reset + u32 MCE : 1; // Modem Control Enable + u32 TTRG : 2; // Transmit FIFO Data Number Trigger + u32 RTRG : 2; // Receive FIFO Data Number Trigger //8 u32 : 8; //16 @@ -1225,16 +1225,16 @@ union SCIF_SCFCR2_type }; #define SCIF_SCFCR2 SH4IO_REG_T(SCIF, SCFCR2) -//Read OLNY +//Read ONLY //SCIF SCFDR2 0xFFE8001C 0x1FE8001C 16 0x0000 0x0000 Held Held Pclk union SCIF_SCFDR2_type { struct { - u32 R : 5; + u32 R : 5; // Number of received data bytes u32 : 3; //8 - u32 T : 5; + u32 T : 5; // Number of untransmitted data bytes u32 : 3; //16 }; @@ -1246,13 +1246,13 @@ union SCIF_SCSPTR2_type { struct { - u32 SPB2DT : 1; - u32 SPB2IO : 1; + u32 SPB2DT : 1; // Serial Port Break Data + u32 SPB2IO : 1; // Serial Port Break I/O u32 : 2; - u32 CTSDT : 1; - u32 CTSIO : 1; - u32 RTSDT : 1; - u32 RTSIO : 1; + u32 CTSDT : 1; // Serial Port CTS Port Data + u32 CTSIO : 1; // Serial Port CTS Port I/O + u32 RTSDT : 1; // Serial Port RTS Port Data + u32 RTSIO : 1; // Serial Port RTS Port I/O //8 u32 : 8; //16 @@ -1266,7 +1266,7 @@ union SCIF_SCLSR2_type { struct { - u32 ORER : 1; + u32 ORER : 1; // Overrun Error u32 :15; //16 }; diff --git a/core/network/net_platform.h b/core/network/net_platform.h index ccf6e2222..f4eab1ed1 100644 --- a/core/network/net_platform.h +++ b/core/network/net_platform.h @@ -29,7 +29,7 @@ #define INADDR_NONE 0xffffffff #endif #ifndef INET_ADDRSTRLEN -#define INET_ADDRSTRLEN sizeof(struct sockaddr_in) +#define INET_ADDRSTRLEN 16 #endif #define SOL_TCP 6 // Shrug #else diff --git a/core/network/null_modem.h b/core/network/null_modem.h index 440ee589a..199f162b7 100644 --- a/core/network/null_modem.h +++ b/core/network/null_modem.h @@ -166,10 +166,22 @@ class NullModemPipe : public SerialPort::Pipe } if (rc == 2) { - if (data[0] != 'D') + if (data[0] != 'D') { ERROR_LOG(NETWORK, "Unexpected packet '%c'", data[0]); + } else + { rxBuffer.push_back(data[1]); + if (!dataReceived) + { + dataReceived = true; + char name[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &addr.sin_addr, name, sizeof(name)); + std::string s(name); + s += ":" + std::to_string(htons(addr.sin_port)); + os_notify("Network connected", 5000, s.c_str()); + } + } } else if (rc == 1) { @@ -249,6 +261,7 @@ class NullModemPipe : public SerialPort::Pipe std::deque rxBuffer; sockaddr_in peerAddress{}; u64 lastPoll = 0; + bool dataReceived = false; }; class BattleCableHandshake : public NetworkHandshake From 34961daaec20bf35ceba648b2adba10db4ead81d Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 11 Dec 2024 11:25:50 +0100 Subject: [PATCH 51/81] gdrom: add support for hardware CD-ROM devices Use libcdio to read CD/DVD/BD drives (linux, windows, bsd) Get rid of old ioctl win32 driver. Add detected CDROM devices to game list. Issue #1654 --- .github/workflows/bsd.yml | 8 +- .github/workflows/c-cpp.yml | 12 +- .gitlab-ci.yml | 6 +- CMakeLists.txt | 20 +- core/imgread/cdio.cpp | 234 +++++++++++++++++++++++ core/imgread/common.cpp | 8 +- core/imgread/ioctl.cpp | 360 ----------------------------------- core/oslib/oslib.cpp | 7 + core/oslib/oslib.h | 1 + core/oslib/storage.cpp | 43 +++-- core/ui/game_scanner.cpp | 13 ++ core/ui/game_scanner.h | 4 +- core/ui/gui.cpp | 4 +- shell/linux/make-appimage.sh | 1 + 14 files changed, 328 insertions(+), 393 deletions(-) create mode 100755 core/imgread/cdio.cpp delete mode 100644 core/imgread/ioctl.cpp diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 580bbe90e..7beecc583 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -14,13 +14,13 @@ jobs: include: - operating_system: freebsd version: '14.2' - pkginstall: sudo pkg install -y alsa-lib ccache cmake evdev-proto git libao libevdev libudev-devd libzip lua54 miniupnpc ninja pkgconf pulseaudio sdl2 + pkginstall: sudo pkg install -y alsa-lib ccache cmake evdev-proto git libao libevdev libudev-devd libzip lua54 miniupnpc ninja pkgconf pulseaudio sdl2 libcdio - operating_system: netbsd version: '10.0' - pkginstall: sudo pkgin update && sudo pkgin -y install alsa-lib ccache cmake gcc12 git libao libzip lua54 miniupnpc ninja-build pkgconf pulseaudio SDL2 && export PATH=/usr/pkg/gcc12/bin:$PATH + pkginstall: sudo pkgin update && sudo pkgin -y install alsa-lib ccache cmake gcc12 git libao libzip lua54 miniupnpc ninja-build pkgconf pulseaudio SDL2 libcdio && export PATH=/usr/pkg/gcc12/bin:$PATH - operating_system: openbsd version: '7.6' - pkginstall: sudo pkg_add ccache cmake git libao libzip miniupnpc ninja pkgconf pulseaudio sdl2 + pkginstall: sudo pkg_add ccache cmake git libao libzip miniupnpc ninja pkgconf pulseaudio sdl2 libcdio exclude: - architecture: arm64 @@ -44,5 +44,5 @@ jobs: environment_variables: CCACHE_DIR run: | ${{ matrix.pkginstall }} - cmake -B build -DCMAKE_BUILD_TYPE=Release -G Ninja + cmake -B build -DUSE_LIBCDIO=ON -DCMAKE_BUILD_TYPE=Release -G Ninja cmake --build build --config Release diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 3954dee58..79ec624ff 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -20,11 +20,11 @@ jobs: - {name: i686-pc-windows-msvc, os: windows-latest, shell: cmd, arch: x86, cmakeArgs: -G Ninja, buildType: Release} - {name: apple-darwin, os: macos-latest, shell: sh, cmakeArgs: -G Xcode -DUSE_DISCORD=ON, destDir: osx, buildType: RelWithDebInfo} - {name: apple-ios, os: macos-latest, shell: sh, cmakeArgs: -DCMAKE_SYSTEM_NAME=iOS -G Xcode, destDir: ios, buildType: Release} - - {name: x86_64-pc-linux-gnu, os: ubuntu-20.04, shell: sh, cmakeArgs: -G Ninja -DUSE_DISCORD=ON, destDir: linux, buildType: RelWithDebInfo} + - {name: x86_64-pc-linux-gnu, os: ubuntu-20.04, shell: sh, cmakeArgs: -G Ninja -DUSE_DISCORD=ON -DUSE_LIBCDIO=ON, destDir: linux, buildType: RelWithDebInfo} - {name: x86_64-pc-windows-msvc, os: windows-latest, shell: cmd, arch: x64, cmakeArgs: -G Ninja -DUSE_DISCORD=ON, buildType: Release} - - {name: x86_64-w64-mingw32, os: windows-latest, shell: 'msys2 {0}', cmakeArgs: -G Ninja -DUSE_DISCORD=ON, destDir: win, buildType: RelWithDebInfo} - - {name: libretro-x86_64-pc-linux-gnu, os: ubuntu-latest, shell: sh, cmakeArgs: -DLIBRETRO=ON -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE -G Ninja, buildType: Release} - - {name: libretro-x86_64-w64-mingw32, os: windows-latest, shell: 'msys2 {0}', cmakeArgs: -DLIBRETRO=ON -G Ninja, buildType: Release} + - {name: x86_64-w64-mingw32, os: windows-latest, shell: 'msys2 {0}', cmakeArgs: -G Ninja -DUSE_DISCORD=ON -DUSE_LIBCDIO=ON, destDir: win, buildType: RelWithDebInfo} + - {name: libretro-x86_64-pc-linux-gnu, os: ubuntu-latest, shell: sh, cmakeArgs: -DLIBRETRO=ON -DUSE_LIBCDIO=ON -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE -G Ninja, buildType: Release} + - {name: libretro-x86_64-w64-mingw32, os: windows-latest, shell: 'msys2 {0}', cmakeArgs: -DLIBRETRO=ON -DUSE_LIBCDIO=ON -G Ninja, buildType: Release} steps: - name: Set up build environment (macOS) @@ -45,7 +45,7 @@ jobs: run: | sudo add-apt-repository ppa:christianrauch/libdecoration sudo apt-get update - sudo apt-get -y install ccache libao-dev libasound2-dev libevdev-dev libgl1-mesa-dev liblua5.3-dev libminiupnpc-dev libpulse-dev libsdl2-dev libudev-dev libzip-dev ninja-build libcurl4-openssl-dev + sudo apt-get -y install ccache libao-dev libasound2-dev libevdev-dev libgl1-mesa-dev liblua5.3-dev libminiupnpc-dev libpulse-dev libsdl2-dev libudev-dev libzip-dev ninja-build libcurl4-openssl-dev libcdio-dev sudo apt-get -y install libwayland-dev libdecor-0-dev libaudio-dev libjack-dev libsndio-dev libsamplerate0-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxkbcommon-dev libdrm-dev libgbm-dev libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev fcitx-libs-dev if: runner.os == 'Linux' @@ -53,7 +53,7 @@ jobs: uses: msys2/setup-msys2@v2 with: msystem: MINGW64 - install: git make mingw-w64-x86_64-ccache mingw-w64-x86_64-cmake mingw-w64-x86_64-lua mingw-w64-x86_64-ninja mingw-w64-x86_64-SDL2 mingw-w64-x86_64-toolchain + install: git make mingw-w64-x86_64-ccache mingw-w64-x86_64-cmake mingw-w64-x86_64-lua mingw-w64-x86_64-ninja mingw-w64-x86_64-SDL2 mingw-w64-x86_64-toolchain mingw-w64-x86_64-libcdio if: matrix.config.shell == 'msys2 {0}' - name: Set up build environment (Windows, Visual Studio) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d2f50b97e..b3cc580a8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,12 +9,12 @@ variables: GIT_SUBMODULE_STRATEGY: recursive CORENAME: flycast - CORE_ARGS: -DLIBRETRO=ON -DCMAKE_BUILD_TYPE=Release + CORE_ARGS: -DLIBRETRO=ON -DUSE_LIBCDIO=ON -DCMAKE_BUILD_TYPE=Release .core-defs-linux: extends: .core-defs variables: - CORE_ARGS: -DLIBRETRO=ON -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE -DCMAKE_BUILD_TYPE=Release + CORE_ARGS: -DLIBRETRO=ON -DUSE_LIBCDIO=ON -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE -DCMAKE_BUILD_TYPE=Release .core-defs-osx-x64: extends: .core-defs @@ -36,7 +36,7 @@ .core-defs-android: extends: .core-defs script: - - cmake $CORE_ARGS -DANDROID_PLATFORM=android-$API_LEVEL -DCMAKE_TOOLCHAIN_FILE=$NDK_ROOT/build/cmake/android.toolchain.cmake -DANDROID_STL=c++_static -DANDROID_ABI=$ANDROID_ABI -DANDROID_ARM_MODE=arm "$CMAKE_SOURCE_ROOT" -B$BUILD_DIR + - cmake -DLIBRETRO=ON -DCMAKE_BUILD_TYPE=Release -DANDROID_PLATFORM=android-$API_LEVEL -DCMAKE_TOOLCHAIN_FILE=$NDK_ROOT/build/cmake/android.toolchain.cmake -DANDROID_STL=c++_static -DANDROID_ABI=$ANDROID_ABI -DANDROID_ARM_MODE=arm "$CMAKE_SOURCE_ROOT" -B$BUILD_DIR - cmake --build $BUILD_DIR --target ${CORENAME}_libretro --config Release -- -j $NUMPROC - mv $BUILD_DIR/${CORENAME}_libretro.so $LIBNAME - if [ $STRIP_CORE_LIB -eq 1 ]; then $NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip $LIBNAME; fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 7114589fb..89fc30afb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ option(ENABLE_GDB_SERVER "Build with GDB debugging support" OFF) option(ENABLE_DC_PROFILER "Build with support for target machine (SH4) profiler" OFF) option(ENABLE_FC_PROFILER "Build with support for host app (Flycast) profiler" OFF) option(USE_DISCORD "Use Discord Presence API" OFF) +option(USE_LIBCDIO "Use libcdio for CDROM access" OFF) if(IOS AND NOT LIBRETRO) set(USE_VULKAN OFF CACHE BOOL "Force vulkan off" FORCE) @@ -672,6 +673,23 @@ if(NOT LIBZIP_FOUND OR NINTENDO_SWITCH) target_link_libraries(${PROJECT_NAME} PRIVATE libzip::zip) endif() +if(USE_LIBCDIO) + if(PKG_CONFIG_FOUND) + pkg_check_modules(CDIO IMPORTED_TARGET libcdio) + if(CDIO_FOUND) + target_compile_definitions(${PROJECT_NAME} PRIVATE USE_LIBCDIO) + target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::CDIO) + endif() + endif() + if(NOT CDIO_FOUND) + find_package(libcdio) + if(TARGET libcdio::libcdio) + target_compile_definitions(${PROJECT_NAME} PRIVATE USE_LIBCDIO) + target_link_libraries(${PROJECT_NAME} PRIVATE libcdio::libcdio) + endif() + endif() +endif() + if(WIN32) target_include_directories(${PROJECT_NAME} PRIVATE core/deps/dirent) endif() @@ -1062,13 +1080,13 @@ endif() target_sources(${PROJECT_NAME} PRIVATE core/imgread/cdi.cpp + core/imgread/cdio.cpp core/imgread/chd.cpp core/imgread/common.cpp core/imgread/common.h core/imgread/cue.cpp core/imgread/gdi.cpp core/imgread/ImgReader.cpp - core/imgread/ioctl.cpp core/imgread/iso9660.h core/imgread/isofs.cpp core/imgread/isofs.h) diff --git a/core/imgread/cdio.cpp b/core/imgread/cdio.cpp new file mode 100755 index 000000000..4d4f71b4b --- /dev/null +++ b/core/imgread/cdio.cpp @@ -0,0 +1,234 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#include "build.h" +#ifdef USE_LIBCDIO +#include "types.h" +#include "imgread/common.h" +#include +#include +#include +#include + +namespace hostfs +{ + +const std::vector& getCdromDrives() +{ + static std::vector cdromDevices; + static bool devicesFetched; + + if (devicesFetched) + return cdromDevices; + devicesFetched = true; + // Set a custom log handler + cdio_log_set_handler([](cdio_log_level_t level, const char message[]) { + switch (level) + { + case CDIO_LOG_DEBUG: + DEBUG_LOG(GDROM, "%s", message); + break; + case CDIO_LOG_INFO: + INFO_LOG(GDROM, "%s", message); + break; + case CDIO_LOG_WARN: + WARN_LOG(GDROM, "%s", message); + break; + case CDIO_LOG_ERROR: + case CDIO_LOG_ASSERT: + ERROR_LOG(GDROM, "%s", message); + break; + } + }); + // Get the list of all hardware devices + char **list = cdio_get_devices(DRIVER_DEVICE); + if (list != nullptr) + { + for (char **dev = &list[0]; *dev != nullptr; dev++) + cdromDevices.push_back(*dev); + cdio_free_device_list(list); + } + + return cdromDevices; +} + +} + +struct CdioDrive; + +struct CdioTrack : public TrackFile +{ + CdioTrack(CdioDrive& disk, bool audio) + : disk(disk), audio(audio) {} + bool Read(u32 FAD, u8 *dst, SectorFormat *sector_type, u8 *subcode, SubcodeFormat *subcode_type); + + CdioDrive& disk; + bool audio; +}; + +struct CdioDrive : public Disc +{ + bool open(const char *path) + { + const std::vector& devices = hostfs::getCdromDrives(); + if (!devices.empty()) + { + // If the list isn't empty, check that an entry exists for the current path + bool found = false; + for (const std::string& dev : devices) + if (dev == path) { + found = true; + break; + } + if (!found) + return false; + } + pCdio = cdio_open(path, DRIVER_DEVICE); + if (pCdio == nullptr) { + WARN_LOG(GDROM, "Can't open CD device %s", path); + return false; + } + track_t firstTrk = cdio_get_first_track_num(pCdio); + track_t lastTrk = cdio_get_last_track_num(pCdio); + if (firstTrk == CDIO_INVALID_TRACK || lastTrk == CDIO_INVALID_TRACK) + { + WARN_LOG(GDROM, "Can't find first and/or last track"); + close(); + return false; + } + + Session session; + session.StartFAD = 150; + session.FirstTrack = firstTrk; + sessions.push_back(session); + type = CdDA; // TODO more CD types + + for (int i = firstTrk; i <= lastTrk; i++) + { + lba_t lba = cdio_get_track_lba(pCdio, i); + if (lba == CDIO_INVALID_LBA) + { + WARN_LOG(GDROM, "Can't find track %d", i); + close(); + return false; + } + track_format_t format = cdio_get_track_format(pCdio, i); + bool copy = true; + if (format == TRACK_FORMAT_AUDIO) { + track_flag_t copyFlag = cdio_get_track_copy_permit(pCdio, i); + copy = copyFlag == CDIO_TRACK_FLAG_TRUE; + } + else if (!tracks.empty() && !tracks.back().isDataTrack()) + { + // session 1 lead-out & session 2 lead-in and pre-gap + tracks.back().EndFAD -= 11400; + + type = CdRom_XA; + Session session; + session.StartFAD = lba; + session.FirstTrack = i; + sessions.push_back(session); + } + Track t; + t.ADR = 1; // FIXME correct? + t.CTRL = format == TRACK_FORMAT_AUDIO ? 0 : CDIO_CDROM_DATA_TRACK; + t.StartFAD = lba; + lsn_t last = cdio_get_track_last_lsn(pCdio, i); + if (last == CDIO_INVALID_LSN) + WARN_LOG(GDROM, "Can't get last lsn of track %d", i); + else + t.EndFAD = cdio_lsn_to_lba(last); + if (i == firstTrk) + sessions.front().StartFAD = t.StartFAD; + INFO_LOG(GDROM, "Track #%d: start %d end %d format %d copy %d", i, t.StartFAD, t.EndFAD, format, copy); + t.file = new CdioTrack(*this, format == TRACK_FORMAT_AUDIO); + tracks.push_back(t); + } + lba_t leadout = cdio_get_track_lba(pCdio, CDIO_CDROM_LEADOUT_TRACK); + if (leadout == CDIO_INVALID_LBA) + { + WARN_LOG(GDROM, "Can't find leadout track"); + close(); + return false; + } + LeadOut.StartFAD = leadout; + LeadOut.ADR = 1; + LeadOut.CTRL = CDIO_CDROM_DATA_TRACK; + + return true; + } + + void close() + { + if (pCdio != nullptr) { + cdio_destroy(pCdio); + pCdio = nullptr; + } + } + ~CdioDrive() { + close(); + } + + CdIo_t *pCdio = nullptr; +}; + +bool CdioTrack::Read(u32 FAD, u8 *dst, SectorFormat *sector_type, u8 *subcode, SubcodeFormat *subcode_type) +{ + lsn_t lsn = cdio_lba_to_lsn(FAD); + if (audio) + { + *sector_type = SECFMT_2352; + if (cdio_read_audio_sector(disk.pCdio, dst, lsn) != DRIVER_OP_SUCCESS) { + WARN_LOG(GDROM, "Read audio fad %d failed", FAD); + return false; + } + } + else + { + *sector_type = SECFMT_2048_MODE2_FORM1; + if (cdio_read_mode2_sector(disk.pCdio, dst, lsn, false) != DRIVER_OP_SUCCESS) + { + if (cdio_read_mode1_sector(disk.pCdio, dst, lsn, false) != DRIVER_OP_SUCCESS) { + WARN_LOG(GDROM, "Read data fad %d failed", FAD); + return false; + } + *sector_type = SECFMT_2048_MODE1; + } + } + + return true; +} + +Disc *cdio_parse(const char *file, std::vector *digest) +{ + INFO_LOG(GDROM, "Opening CDIO device %s", file); + CdioDrive *disk = new CdioDrive(); + + if (disk->open(file)) + { + if (digest != nullptr) + digest->clear(); + return disk; + } + else { + delete disk; + return nullptr; + } +} + +#endif // USE_LIBCDIO diff --git a/core/imgread/common.cpp b/core/imgread/common.cpp index 6e7199241..e40c24d5f 100644 --- a/core/imgread/common.cpp +++ b/core/imgread/common.cpp @@ -9,7 +9,7 @@ Disc* chd_parse(const char* file, std::vector *digest); Disc* gdi_parse(const char* file, std::vector *digest); Disc* cdi_parse(const char* file, std::vector *digest); Disc* cue_parse(const char* file, std::vector *digest); -Disc* ioctl_parse(const char* file, std::vector *digest); +Disc *cdio_parse(const char *file, std::vector *digest); static u32 NullDriveDiscType; Disc* disc; @@ -21,8 +21,8 @@ constexpr Disc* (*drivers[])(const char* path, std::vector *digest) gdi_parse, cdi_parse, cue_parse, -#if defined(_WIN32) && !defined(TARGET_UWP) - ioctl_parse, +#ifdef USE_LIBCDIO + cdio_parse, #endif }; @@ -278,7 +278,7 @@ bool Disc::readSector(u32 FAD, u8 *dst, SectorFormat *sector_type, u8 *subcode, void Disc::ReadSectors(u32 FAD, u32 count, u8* dst, u32 fmt, LoadProgress *progress) { - u8 temp[2448]; + u8 temp[2352]; SectorFormat secfmt; SubcodeFormat subfmt; diff --git a/core/imgread/ioctl.cpp b/core/imgread/ioctl.cpp deleted file mode 100644 index 6b879e0ef..000000000 --- a/core/imgread/ioctl.cpp +++ /dev/null @@ -1,360 +0,0 @@ -#include "build.h" -#if defined(_WIN32) && !defined(TARGET_UWP) -#include "types.h" -#include "common.h" - -#include -#include - -#include -#include - -#ifdef _MSC_VER -#define _NTSCSI_USER_MODE_ -#include -#undef _NTSCSI_USER_MODE_ -#else -#define CD_RAW_READ_SUBCODE_SIZE ( 96) - -#pragma pack(push, cdb, 1) -typedef union _CDB { - struct _READ_CD { - UCHAR OperationCode; // 0xBE - SCSIOP_READ_CD - UCHAR RelativeAddress : 1; - UCHAR Reserved0 : 1; - UCHAR ExpectedSectorType : 3; - UCHAR Lun : 3; - UCHAR StartingLBA[4]; - UCHAR TransferBlocks[3]; - UCHAR Reserved2 : 1; - UCHAR ErrorFlags : 2; - UCHAR IncludeEDC : 1; - UCHAR IncludeUserData : 1; - UCHAR HeaderCode : 2; - UCHAR IncludeSyncData : 1; - UCHAR SubChannelSelection : 3; - UCHAR Reserved3 : 5; - UCHAR Control; - } READ_CD; -} CDB, *PCDB; -#pragma pack(pop, cdb) - -#define READ_TOC_FORMAT_FULL_TOC 0x02 - -#define SCSIOP_READ 0x28 -#define SCSIOP_READ_CD 0xBE -#endif - -#define RAW_SECTOR_SIZE 2352 -#define CD_SECTOR_SIZE 2048 -#define SECTORS_AT_READ 20 -#define CD_BLOCKS_PER_SECOND 75 - -struct spti_s -{ - SCSI_PASS_THROUGH_DIRECT sptd; - DWORD alignmentDummy; - BYTE senseBuf[0x12]; -}; - -ULONG msf2fad(const UCHAR Addr[4]) -{ - ULONG Sectors = ( Addr[0] * (CD_BLOCKS_PER_SECOND*60) ) + ( Addr[1]*CD_BLOCKS_PER_SECOND) + Addr[2]; - return Sectors; -} - - -// Msf: Hours, Minutes, Seconds, Frames -ULONG AddressToSectors( UCHAR Addr[4] ); - - -bool spti_SendCommand(HANDLE hand,spti_s& s,SCSI_ADDRESS& ioctl_addr) -{ - s.sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT); - s.sptd.PathId = ioctl_addr.PathId; - s.sptd.TargetId = ioctl_addr.TargetId; - s.sptd.Lun = ioctl_addr.Lun; - s.sptd.TimeOutValue = 30; - //s.sptd.CdbLength = 0x0A; - s.sptd.SenseInfoLength = 0x12; - s.sptd.SenseInfoOffset = offsetof(spti_s, senseBuf); -// s.sptd.DataIn = SCSI_IOCTL_DATA_IN; -// s.sptd.DataTransferLength = 0x800; -// s.sptd.DataBuffer = pdata; - - DWORD bytesReturnedIO = 0; - if(!DeviceIoControl(hand, IOCTL_SCSI_PASS_THROUGH_DIRECT, &s, sizeof(s), &s, sizeof(s), &bytesReturnedIO, NULL)) - return false; - - if(s.sptd.ScsiStatus) - return false; - return true; -} - -bool spti_Read10(HANDLE hand,void * pdata,u32 sector,SCSI_ADDRESS& ioctl_addr) -{ - spti_s s; - memset(&s,0,sizeof(spti_s)); - - s.sptd.Cdb[0] = SCSIOP_READ; - s.sptd.Cdb[1] = (ioctl_addr.Lun&7) << 5;// | DPO ; DPO = 8 - - s.sptd.Cdb[2] = (BYTE)(sector >> 0x18 & 0xFF); // MSB - s.sptd.Cdb[3] = (BYTE)(sector >> 0x10 & 0xFF); - s.sptd.Cdb[4] = (BYTE)(sector >> 0x08 & 0xFF); - s.sptd.Cdb[5] = (BYTE)(sector >> 0x00 & 0xFF); // LSB - - s.sptd.Cdb[7] = 0; - s.sptd.Cdb[8] = 1; - - s.sptd.CdbLength = 0x0A; - s.sptd.DataIn = SCSI_IOCTL_DATA_IN; - s.sptd.DataTransferLength = 0x800; - s.sptd.DataBuffer = pdata; - - return spti_SendCommand(hand,s,ioctl_addr); -} -bool spti_ReadCD(HANDLE hand,void * pdata,u32 sector,SCSI_ADDRESS& ioctl_addr) -{ - spti_s s; - memset(&s,0,sizeof(spti_s)); - CDB& r = *(PCDB)s.sptd.Cdb; - - r.READ_CD.OperationCode = SCSIOP_READ_CD; - - r.READ_CD.StartingLBA[0] = (BYTE)(sector >> 0x18 & 0xFF); - r.READ_CD.StartingLBA[1] = (BYTE)(sector >> 0x10 & 0xFF); - r.READ_CD.StartingLBA[2] = (BYTE)(sector >> 0x08 & 0xFF); - r.READ_CD.StartingLBA[3] = (BYTE)(sector >> 0x00 & 0xFF); - - // 1 sector - r.READ_CD.TransferBlocks[0] = 0; - r.READ_CD.TransferBlocks[1] = 0; - r.READ_CD.TransferBlocks[2] = 1; - - // 0xF8 - r.READ_CD.IncludeSyncData = 1; - r.READ_CD.HeaderCode = 3; - r.READ_CD.IncludeUserData = 1; - r.READ_CD.IncludeEDC = 1; - - r.READ_CD.SubChannelSelection = 1; - - s.sptd.CdbLength = 12; - s.sptd.DataIn = SCSI_IOCTL_DATA_IN; - s.sptd.DataTransferLength = 2448; - s.sptd.DataBuffer = pdata; - return spti_SendCommand(hand,s,ioctl_addr); -} - -struct PhysicalDrive; -struct PhysicalTrack:TrackFile -{ - PhysicalDrive* disc; - PhysicalTrack(PhysicalDrive* disc) { this->disc=disc; } - - bool Read(u32 FAD,u8* dst,SectorFormat* sector_type,u8* subcode,SubcodeFormat* subcode_type) override; -}; - -struct PhysicalDrive:Disc -{ - HANDLE drive; - SCSI_ADDRESS scsi_addr; - bool use_scsi; - - PhysicalDrive() - { - drive=INVALID_HANDLE_VALUE; - memset(&scsi_addr,0,sizeof(scsi_addr)); - use_scsi=false; - } - - bool Build(char* path) - { - drive = CreateFile( path, GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL, OPEN_EXISTING, 0, NULL); - - if ( INVALID_HANDLE_VALUE == drive ) - return false; //failed to open - - printf(" Opened device %s, reading TOC ...",path); - // Get track-table and parse it - CDROM_READ_TOC_EX tocrq={0}; - - tocrq.Format = READ_TOC_FORMAT_FULL_TOC; - tocrq.Msf=1; - tocrq.SessionTrack=1; - u8 buff[2048]; - CDROM_TOC_FULL_TOC_DATA *ftd=(CDROM_TOC_FULL_TOC_DATA*)buff; - - ULONG BytesRead; - memset(buff,0,sizeof(buff)); - int code = DeviceIoControl(drive,IOCTL_CDROM_READ_TOC_EX,&tocrq,sizeof(tocrq),ftd, 2048, &BytesRead, NULL); - -// CDROM_TOC toc; - int currs=-1; - if (0==code) - { - printf(" failed\n"); - //failed to read toc - CloseHandle(drive); - return false; - } - else - { - printf(" done !\n"); - - type=CdRom_XA; - - BytesRead-=sizeof(CDROM_TOC_FULL_TOC_DATA); - BytesRead/=sizeof(ftd->Descriptors[0]); - - for (u32 i=0;iDescriptors[i].Point==0xA2) - { - this->EndFAD=msf2fad(ftd->Descriptors[i].Msf); - continue; - } - if (ftd->Descriptors[i].Point>=1 && ftd->Descriptors[i].Point<=0x63 && - ftd->Descriptors[i].Adr==1) - { - u32 trackn=ftd->Descriptors[i].Point-1; - verify(trackn==tracks.size()); - Track t; - - t.ADR=ftd->Descriptors[i].Adr; - t.CTRL=ftd->Descriptors[i].Control; - t.StartFAD=msf2fad(ftd->Descriptors[i].Msf); - t.file = new PhysicalTrack(this); - - tracks.push_back(t); - - if (currs!=ftd->Descriptors[i].SessionNumber) - { - currs=ftd->Descriptors[i].SessionNumber; - verify(sessions.size()==(currs-1)); - Session s; - s.FirstTrack=trackn+1; - s.StartFAD=t.StartFAD; - - sessions.push_back(s); - } - } - } - LeadOut.StartFAD = EndFAD; - LeadOut.ADR = 1; - LeadOut.CTRL = 4; - } - - DWORD bytesReturnedIO = 0; - BOOL resultIO = DeviceIoControl(drive, IOCTL_SCSI_GET_ADDRESS, NULL, 0, &scsi_addr, sizeof(scsi_addr), &bytesReturnedIO, NULL); - //done ! - if (resultIO) - use_scsi=true; - else - use_scsi=false; - - return true; - } -}; - -bool PhysicalTrack::Read(u32 FAD,u8* dst,SectorFormat* sector_type,u8* subcode,SubcodeFormat* subcode_type) -{ - static u8 temp[2500]; - - u32 LBA=FAD-150; - - if (disc->use_scsi) - { - if (!spti_ReadCD(disc->drive, temp,LBA,disc->scsi_addr)) - { - if (spti_Read10(disc->drive, dst,LBA,disc->scsi_addr)) - { - //sector read success, just user data - *sector_type=SECFMT_2048_MODE2_FORM1; //m2f1 seems more common ? is there some way to detect it properly here? - return true; - } - } - else - { - //sector read success, with subcode - memcpy(dst, temp, 2352); - memcpy(subcode, temp + 2352, CD_RAW_READ_SUBCODE_SIZE); - - *sector_type=SECFMT_2352; - *subcode_type=SUBFMT_96; - return true; - } - } - - //hmm, spti failed/cannot be used - - - //try IOCTL_CDROM_RAW_READ - - - static __RAW_READ_INFO Info; - - Info.SectorCount=1; - Info.DiskOffset.QuadPart = LBA * CD_SECTOR_SIZE; //CD_SECTOR_SIZE, even though we read RAW sectors. Its how winapi works. - ULONG Dummy; - - //try all 3 track modes, starting from the one that succeeded last time (Info is static) to save time ! - for (int tr=0;tr<3;tr++) - { - if ( 0 == DeviceIoControl( disc->drive, IOCTL_CDROM_RAW_READ, &Info, sizeof(Info), dst, RAW_SECTOR_SIZE, &Dummy, NULL ) ) - { - Info.TrackMode=(TRACK_MODE_TYPE)((Info.TrackMode+1)%3); //try next mode - } - else - { - //sector read success - *sector_type=SECFMT_2352; - return true; - } - } - - //finally, try ReadFile - if (SetFilePointer(disc->drive,LBA*2048,0,FILE_BEGIN)!=INVALID_SET_FILE_POINTER) - { - DWORD BytesRead; - if (FALSE!=ReadFile(disc->drive,dst,2048,&BytesRead,0) && BytesRead==2048) - { - //sector read success, just user data - *sector_type=SECFMT_2048_MODE2_FORM1; //m2f1 seems more common ? is there some way to detect it properly here? - return true; - } - } - - printf("IOCTL: Totally failed to read sector @LBA %d\n", LBA); - return false; -} - - -Disc* ioctl_parse(const char* file, std::vector *digest) -{ - - if (strlen(file)==3 && GetDriveType(file)==DRIVE_CDROM) - { - printf("Opening device %s ...",file); - char fn[]={ '\\', '\\', '.', '\\', file[0],':', '\0' }; - PhysicalDrive* rv = new PhysicalDrive(); - - if (rv->Build(fn)) - { - if (digest != nullptr) - digest->clear(); - return rv; - } - else - { - delete rv; - return 0; - } - } - else - { - return 0; - } -} -#endif diff --git a/core/oslib/oslib.cpp b/core/oslib/oslib.cpp index cb2c3aaa9..4595ed42d 100644 --- a/core/oslib/oslib.cpp +++ b/core/oslib/oslib.cpp @@ -315,6 +315,13 @@ void saveScreenshot(const std::string& name, const std::vector& data) #endif +#ifndef USE_LIBCDIO +const std::vector& getCdromDrives() { + static std::vector empty; + return empty; +} +#endif + } // namespace hostfs void os_CreateWindow() diff --git a/core/oslib/oslib.h b/core/oslib/oslib.h index 7074c8346..e55fffea3 100644 --- a/core/oslib/oslib.h +++ b/core/oslib/oslib.h @@ -63,6 +63,7 @@ namespace hostfs std::string getShaderCachePath(const std::string& filename); void saveScreenshot(const std::string& name, const std::vector& data); + const std::vector& getCdromDrives(); #ifdef __ANDROID__ void importHomeDirectory(); void exportHomeDirectory(); diff --git a/core/oslib/storage.cpp b/core/oslib/storage.cpp index 97d21f1ce..dbb1546d9 100644 --- a/core/oslib/storage.cpp +++ b/core/oslib/storage.cpp @@ -194,24 +194,43 @@ class StdStorage : public Storage info.size = st.st_size; info.updateTime = st.st_mtime; #else // _WIN32 + std::string lpath(path); + if (path.length() == 2 && isalpha(path[0]) && path[1] == ':') + /* D: -> \\.\D:\ */ + lpath = "\\\\.\\" + path + "\\"; + else if (path.substr(0, 4) == "\\\\.\\" && path.length() == 6) + /* \\.\D: -> \\.\D:\ */ + lpath += "\\"; nowide::wstackstring wname; - if (wname.convert(path.c_str())) + if (wname.convert(lpath.c_str())) { - WIN32_FILE_ATTRIBUTE_DATA fileAttribs; - if (GetFileAttributesExW(wname.get(), GetFileExInfoStandard, &fileAttribs)) + if (lpath.substr(0, 4) == "\\\\.\\") { - info.isDirectory = (fileAttribs.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; - info.size = fileAttribs.nFileSizeLow + ((u64)fileAttribs.nFileSizeHigh << 32); - u64 t = ((u64)fileAttribs.ftLastWriteTime.dwHighDateTime << 32) | fileAttribs.ftLastWriteTime.dwLowDateTime; - info.updateTime = t / 10000000 - 11644473600LL; // 100-nano to secs minus (unix epoch - windows epoch) + // Win32 device namespace + UINT type = GetDriveTypeW(wname.get()); + if (type != DRIVE_CDROM) + throw StorageException("Invalid device " + lpath.substr(4, 2)); + info.isDirectory = false; + info.isWritable = false; } else { - const int error = GetLastError(); - if (error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) - INFO_LOG(COMMON, "Cannot get attributes of '%s' error 0x%x", path.c_str(), error); - _set_errno(error); - throw StorageException("Cannot get attributes of " + path); + WIN32_FILE_ATTRIBUTE_DATA fileAttribs; + if (GetFileAttributesExW(wname.get(), GetFileExInfoStandard, &fileAttribs)) + { + info.isDirectory = (fileAttribs.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + info.size = fileAttribs.nFileSizeLow + ((u64)fileAttribs.nFileSizeHigh << 32); + u64 t = ((u64)fileAttribs.ftLastWriteTime.dwHighDateTime << 32) | fileAttribs.ftLastWriteTime.dwLowDateTime; + info.updateTime = t / 10000000 - 11644473600LL; // 100-nano to secs minus (unix epoch - windows epoch) + } + else + { + const int error = GetLastError(); + if (error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) + INFO_LOG(COMMON, "Cannot get attributes of '%s' error 0x%x", lpath.c_str(), error); + _set_errno(error); + throw StorageException("Cannot get attributes of " + lpath); + } } } else diff --git a/core/ui/game_scanner.cpp b/core/ui/game_scanner.cpp index 0dba94738..d56dd179e 100644 --- a/core/ui/game_scanner.cpp +++ b/core/ui/game_scanner.cpp @@ -152,9 +152,22 @@ void GameScanner::fetch_game_list() } std::string dcbios = hostfs::findFlash("dc_", "%bios.bin;%boot.bin"); { + const std::vector& cdromDrives = hostfs::getCdromDrives(); LockGuard _(mutex); + // CD-ROM devices + for (auto it = cdromDrives.rbegin(); it != cdromDrives.rend(); ++it) + { + std::string name; + if (it->substr(0, 4) == "\\\\.\\") + name = it->substr(4); + else + name = *it; + game_list.insert(game_list.begin(), { name, *it, name, "", true }); + } + // Dreamcast BIOS if (!dcbios.empty()) game_list.insert(game_list.begin(), { "Dreamcast BIOS" }); + // Arcade games game_list.insert(game_list.end(), arcade_game_list.begin(), arcade_game_list.end()); } if (running) diff --git a/core/ui/game_scanner.h b/core/ui/game_scanner.h index f6f5901d8..e92b17ddb 100644 --- a/core/ui/game_scanner.h +++ b/core/ui/game_scanner.h @@ -26,11 +26,13 @@ #include #include -struct GameMedia { +struct GameMedia +{ std::string name; // Display name std::string path; // Full path to rom. May be an encoded uri std::string fileName; // Last component of the path, decoded std::string gameName; // for arcade games only, description from the rom list + bool device = false; // Corresponds to a physical cdrom device }; class GameScanner diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index e826a6b77..286dc66fa 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -3275,7 +3275,7 @@ static void gui_display_content() if (gui_state == GuiState::SelectDisk) { std::string extension = get_file_extension(game.path); - if (extension != "gdi" && extension != "chd" + if (!game.device && extension != "gdi" && extension != "chd" && extension != "cdi" && extension != "cue") // Only dreamcast disks continue; @@ -3285,7 +3285,7 @@ static void gui_display_content() } std::string gameName = game.name; GameBoxart art; - if (config::BoxartDisplayMode) + if (config::BoxartDisplayMode && !game.device) { art = boxart.getBoxartAndLoad(game); gameName = art.name; diff --git a/shell/linux/make-appimage.sh b/shell/linux/make-appimage.sh index 67f594dc8..e8dddbfdd 100755 --- a/shell/linux/make-appimage.sh +++ b/shell/linux/make-appimage.sh @@ -72,6 +72,7 @@ SHLIBS=( libsqlite3.so.0 libcrypt.so.1 libbsd.so.0 + libcdio.so.18 ) if [ ! -f appimagetool-x86_64.AppImage ]; then From 53e0f12c22b1fd4646c1a0109645ab30b2f34c8b Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 12 Dec 2024 09:57:18 +0100 Subject: [PATCH 52/81] win32: force libcdio static link Issue #1654 --- CMakeLists.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 89fc30afb..e41bba96b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -678,7 +678,12 @@ if(USE_LIBCDIO) pkg_check_modules(CDIO IMPORTED_TARGET libcdio) if(CDIO_FOUND) target_compile_definitions(${PROJECT_NAME} PRIVATE USE_LIBCDIO) - target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::CDIO) + if(MINGW) + # Force static link + target_link_libraries(${PROJECT_NAME} PRIVATE "-l:libcdio.a -l:libiconv.a") + else() + target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::CDIO) + endif() endif() endif() if(NOT CDIO_FOUND) From a7f20087631aa42bdec84e2424c756bbd30767e6 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 12 Dec 2024 10:14:30 +0100 Subject: [PATCH 53/81] rend: support planar VQ textures Fixes gens4all emu in teenage mutant ninja turtle collection. Issue #868 --- core/rend/TexCache.cpp | 39 +++++++++++++++++++++++---------------- core/rend/TexCache.h | 30 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/core/rend/TexCache.cpp b/core/rend/TexCache.cpp index dd4a75486..63fb44bf0 100644 --- a/core/rend/TexCache.cpp +++ b/core/rend/TexCache.cpp @@ -304,20 +304,21 @@ struct PvrTexInfo TexConvFP32 PL32; TexConvFP32 TW32; TexConvFP32 VQ32; + TexConvFP32 PLVQ32; // Conversion to 8 bpp (palette) TexConvFP8 TW8; }; #define TEX_CONV_TABLE \ const PvrTexInfo pvrTexInfo[8] = \ -{ /* name bpp Final format Twiddled VQ Planar(32b) Twiddled(32b) VQ (32b) Palette (8b) */ \ - {"1555", 16, TextureType::_5551, tex1555_TW, tex1555_VQ, tex1555_PL32, tex1555_TW32, tex1555_VQ32, nullptr }, \ - {"565", 16, TextureType::_565, tex565_TW, tex565_VQ, tex565_PL32, tex565_TW32, tex565_VQ32, nullptr }, \ - {"4444", 16, TextureType::_4444, tex4444_TW, tex4444_VQ, tex4444_PL32, tex4444_TW32, tex4444_VQ32, nullptr }, \ - {"yuv", 16, TextureType::_8888, nullptr, nullptr, texYUV422_PL, texYUV422_TW, texYUV422_VQ, nullptr }, \ - {"bumpmap", 16, TextureType::_4444, texBMP_TW, texBMP_VQ, tex4444_PL32, tex4444_TW32, tex4444_VQ32, nullptr }, \ - {"pal4", 4, TextureType::_5551, texPAL4_TW, texPAL4_VQ, nullptr, texPAL4_TW32, texPAL4_VQ32, texPAL4PT_TW }, \ - {"pal8", 8, TextureType::_5551, texPAL8_TW, texPAL8_VQ, nullptr, texPAL8_TW32, texPAL8_VQ32, texPAL8PT_TW }, \ +{ /* name bpp Final format Twiddled VQ Planar(32b) Twiddled(32b) VQ (32b) PL VQ (32b) Palette (8b) */ \ + {"1555", 16, TextureType::_5551, tex1555_TW, tex1555_VQ, tex1555_PL32, tex1555_TW32, tex1555_VQ32, tex1555_PLVQ32, nullptr }, \ + {"565", 16, TextureType::_565, tex565_TW, tex565_VQ, tex565_PL32, tex565_TW32, tex565_VQ32, tex565_PLVQ32, nullptr }, \ + {"4444", 16, TextureType::_4444, tex4444_TW, tex4444_VQ, tex4444_PL32, tex4444_TW32, tex4444_VQ32, tex4444_PLVQ32, nullptr }, \ + {"yuv", 16, TextureType::_8888, nullptr, nullptr, texYUV422_PL, texYUV422_TW, texYUV422_VQ, texYUV422_PLVQ, nullptr }, \ + {"bumpmap", 16, TextureType::_4444, texBMP_TW, texBMP_VQ, tex4444_PL32, tex4444_TW32, tex4444_VQ32, tex4444_PLVQ32, nullptr }, \ + {"pal4", 4, TextureType::_5551, texPAL4_TW, texPAL4_VQ, nullptr, texPAL4_TW32, texPAL4_VQ32, nullptr, texPAL4PT_TW }, \ + {"pal8", 8, TextureType::_5551, texPAL8_TW, texPAL8_VQ, nullptr, texPAL8_TW32, texPAL8_VQ32, nullptr, texPAL8PT_TW }, \ {"ns/1555", 0}, \ } @@ -486,11 +487,6 @@ BaseTextureCacheData::BaseTextureCacheData(TSP tsp, TCW tcw) if (tcw.ScanOrder && tex->PL32 != nullptr) { //Texture is stored 'planar' in memory, no deswizzle is needed - if (tcw.VQ_Comp != 0) - { - WARN_LOG(RENDERER, "Warning: planar texture with VQ set (invalid)"); - this->tcw.VQ_Comp = 0; - } if (tcw.MipMapped != 0) { WARN_LOG(RENDERER, "Warning: planar texture with mipmaps (invalid)"); @@ -508,9 +504,20 @@ BaseTextureCacheData::BaseTextureCacheData(TSP tsp, TCW tcw) //Call the format specific conversion code texconv = nullptr; - texconv32 = tex->PL32; - //calculate the size, in bytes, for the locking - size = stride * height * tex->bpp / 8; + if (tcw.VQ_Comp != 0) + { + // VQ + texconv32 = tex->PLVQ32; + mmStartAddress += VQ_CODEBOOK_SIZE; + size = stride * height / 4; + } + else + { + // Normal + texconv32 = tex->PL32; + //calculate the size, in bytes, for the locking + size = stride * height * tex->bpp / 8; + } } else { diff --git a/core/rend/TexCache.h b/core/rend/TexCache.h index 188f44da9..827d71015 100644 --- a/core/rend/TexCache.h +++ b/core/rend/TexCache.h @@ -420,6 +420,26 @@ void texture_PL(PixelBuffer* pb, const u } } +template +void texture_PLVQ(PixelBuffer* pb, const u8* p_in, u32 width, u32 height) +{ + pb->amove(0, 0); + + height /= PixelConvertor::ypp; + width /= PixelConvertor::xpp; + + for (u32 y = 0; y < height; y++) + { + for (u32 x = 0; x < width; x++) + { + u8 p = *p_in++; + PixelConvertor::Convert(pb, &vq_codebook[p * 8]); + pb->rmovex(PixelConvertor::xpp); + } + pb->rmovey(PixelConvertor::ypp); + } +} + template void texture_TW(PixelBuffer* pb, const u8* p_in, u32 Width, u32 Height) { @@ -497,6 +517,11 @@ constexpr TexConvFP32 tex565_PL32 = texture_PL>>; constexpr TexConvFP32 tex4444_PL32 = texture_PL>>; +constexpr TexConvFP32 texYUV422_PLVQ = texture_PLVQ>; +constexpr TexConvFP32 tex565_PLVQ32 = texture_PLVQ>>; +constexpr TexConvFP32 tex1555_PLVQ32 = texture_PLVQ>>; +constexpr TexConvFP32 tex4444_PLVQ32 = texture_PLVQ>>; + //Twiddle constexpr TexConvFP tex1555_TW = texture_TW>; constexpr TexConvFP tex4444_TW = texture_TW>; @@ -527,6 +552,11 @@ constexpr TexConvFP32 tex565_PL32 = texture_PL>>; constexpr TexConvFP32 tex4444_PL32 = texture_PL>>; +constexpr TexConvFP32 texYUV422_PLVQ = texture_PLVQ>; +constexpr TexConvFP32 tex565_PLVQ32 = texture_PLVQ>>; +constexpr TexConvFP32 tex1555_PLVQ32 = texture_PLVQ>>; +constexpr TexConvFP32 tex4444_PLVQ32 = texture_PLVQ>>; + //Twiddle constexpr TexConvFP tex1555_TW = texture_TW>>; constexpr TexConvFP tex4444_TW = texture_TW>>; From a46677e64e102022718fce3c95d2bbdc800c0517 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 12 Dec 2024 10:17:45 +0100 Subject: [PATCH 54/81] ui: add cancel button when inserting a disk Issue #1566 --- core/ui/gui.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 286dc66fa..369d6fae4 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -3248,6 +3248,13 @@ static void gui_display_content() if (iconButton(ICON_FA_GEAR, "Settings")) gui_setState(GuiState::Settings); } + else + { + ImGui::SameLine(ImGui::GetContentRegionMax().x + - ImGui::GetStyle().FramePadding.x * 2.0f - ImGui::CalcTextSize("Cancel").x); + if (ImGui::Button("Cancel")) + gui_setState(GuiState::Commands); + } ImGui::PopStyleVar(); scanner.fetch_game_list(); From 9ea0563a7a11a563f41cdbfdd3bddf0d29f031d4 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 12 Dec 2024 14:27:23 +0100 Subject: [PATCH 55/81] gdrom: stop cdda playback when reaching session lead out Issue #1654 --- core/hw/gdrom/gdromv3.cpp | 45 ++++++++++++--------- core/hw/naomi/gdcartridge.cpp | 2 +- core/imgread/common.cpp | 73 ++++++++++++++++++----------------- core/imgread/common.h | 4 +- 4 files changed, 66 insertions(+), 58 deletions(-) diff --git a/core/hw/gdrom/gdromv3.cpp b/core/hw/gdrom/gdromv3.cpp index d84eff64b..494f8adec 100644 --- a/core/hw/gdrom/gdromv3.cpp +++ b/core/hw/gdrom/gdromv3.cpp @@ -65,32 +65,39 @@ GD_HardwareInfo_t GD_HardwareInfo; void libCore_CDDA_Sector(s16* sector) { - //silence ! :p if (cdda.status == cdda_t::Playing) { - libGDR_ReadSector((u8*)sector,cdda.CurrAddr.FAD,1,2352); - cdda.CurrAddr.FAD++; - if (cdda.CurrAddr.FAD >= cdda.EndAddr.FAD) + if (libGDR_ReadSector((u8*)sector, cdda.CurrAddr.FAD, 1, 2352, true) == 0) { - if (cdda.repeats==0) - { - //stop - cdda.status = cdda_t::Terminated; - SecNumber.Status = GD_PAUSE; - } - else + // Stop + cdda.CurrAddr.FAD--; // should stay on the last sector read (reported by subcode with cdda status=terminated) + cdda.status = cdda_t::Terminated; + SecNumber.Status = GD_PAUSE; + memset(sector, 0, 2352); + } + else + { + cdda.CurrAddr.FAD++; + if (cdda.CurrAddr.FAD >= cdda.EndAddr.FAD) { - //Repeat ;) - if (cdda.repeats!=0xf) - cdda.repeats--; - - cdda.CurrAddr.FAD=cdda.StartAddr.FAD; + if (cdda.repeats == 0) + { + // Stop + cdda.status = cdda_t::Terminated; + SecNumber.Status = GD_PAUSE; + } + else + { + // Repeat + if (cdda.repeats != 15) + cdda.repeats--; + cdda.CurrAddr.FAD = cdda.StartAddr.FAD; + } } } } - else - { - memset(sector,0,2352); + else { + memset(sector, 0, 2352); } } diff --git a/core/hw/naomi/gdcartridge.cpp b/core/hw/naomi/gdcartridge.cpp index 734aab23b..7c1a7a07a 100644 --- a/core/hw/naomi/gdcartridge.cpp +++ b/core/hw/naomi/gdcartridge.cpp @@ -445,7 +445,7 @@ void GDCartridge::find_file(const char *name, const u8 *dir_sector, u32 &file_st void GDCartridge::read_gdrom(Disc *gdrom, u32 sector, u8* dst, u32 count, LoadProgress *progress) { - gdrom->ReadSectors(sector + 150, count, dst, 2048, progress); + gdrom->ReadSectors(sector + 150, count, dst, 2048, false, progress); } void GDCartridge::device_start(LoadProgress *progress, std::vector *digest) diff --git a/core/imgread/common.cpp b/core/imgread/common.cpp index e40c24d5f..3a55a2b63 100644 --- a/core/imgread/common.cpp +++ b/core/imgread/common.cpp @@ -205,10 +205,14 @@ static u32 createTrackInfoFirstLast(const Track& track, u32 tracknum) return createTrackInfo(track, tracknum << 16); } -void libGDR_ReadSector(u8 *buff, u32 startSector, u32 sectorCount, u32 sectorSize) +u32 libGDR_ReadSector(u8 *buff, u32 startSector, u32 sectorCount, u32 sectorSize, bool stopOnMiss) { if (disc != nullptr) - disc->ReadSectors(startSector, sectorCount, buff, sectorSize); + return disc->ReadSectors(startSector, sectorCount, buff, sectorSize, stopOnMiss); + if (stopOnMiss) + return 0; + memset(buff, 0, sectorCount * sectorSize); + return sectorCount; } void libGDR_GetToc(u32* to, DiskArea area) @@ -276,13 +280,13 @@ bool Disc::readSector(u32 FAD, u8 *dst, SectorFormat *sector_type, u8 *subcode, return false; } -void Disc::ReadSectors(u32 FAD, u32 count, u8* dst, u32 fmt, LoadProgress *progress) +u32 Disc::ReadSectors(u32 FAD, u32 count, u8* dst, u32 fmt, bool stopOnMiss, LoadProgress *progress) { u8 temp[2352]; SectorFormat secfmt; SubcodeFormat subfmt; - for (u32 i = 1; i <= count; i++) + for (u32 i = 0; i < count; i++) { if (progress != nullptr) { @@ -291,43 +295,40 @@ void Disc::ReadSectors(u32 FAD, u32 count, u8* dst, u32 fmt, LoadProgress *progr progress->label = "Loading..."; progress->progress = (float)i / count; } - if (readSector(FAD, temp, &secfmt, q_subchannel, &subfmt)) - { - //TODO: Proper sector conversions - if (secfmt==SECFMT_2352) - { - convertSector(temp,dst,2352,fmt,FAD); - } - else if (fmt == 2048 && secfmt==SECFMT_2336_MODE2) - memcpy(dst,temp+8,2048); - else if (fmt==2048 && (secfmt==SECFMT_2048_MODE1 || secfmt==SECFMT_2048_MODE2_FORM1 )) - { - memcpy(dst,temp,2048); - } - else if (fmt==2352 && (secfmt==SECFMT_2048_MODE1 || secfmt==SECFMT_2048_MODE2_FORM1 )) - { - INFO_LOG(GDROM, "GDR:fmt=2352;secfmt=2048"); - memcpy(dst,temp,2048); - } - else if (fmt==2048 && secfmt==SECFMT_2448_MODE2) - { - // Pier Solar and the Great Architects - convertSector(temp, dst, 2448, fmt, FAD); - } - else - { - WARN_LOG(GDROM, "ERROR: UNABLE TO CONVERT SECTOR. THIS IS FATAL. Format: %d Sector format: %d", fmt, secfmt); - //verify(false); - } - } - else + if (!readSector(FAD, temp, &secfmt, q_subchannel, &subfmt)) { WARN_LOG(GDROM, "Sector Read miss FAD: %d", FAD); - memset(dst, 0, fmt); + if (stopOnMiss) + return i; + memset(temp, 0, sizeof(temp)); + secfmt = SECFMT_2352; + } + + //TODO: Proper sector conversions + if (secfmt == SECFMT_2352) { + convertSector(temp, dst, 2352, fmt, FAD); + } + else if (fmt == 2048 && secfmt == SECFMT_2336_MODE2) { + memcpy(dst, temp + 8, 2048); + } + else if (fmt == 2048 && (secfmt == SECFMT_2048_MODE1 || secfmt == SECFMT_2048_MODE2_FORM1)) { + memcpy(dst, temp, 2048); + } + else if (fmt == 2352 && (secfmt == SECFMT_2048_MODE1 || secfmt == SECFMT_2048_MODE2_FORM1 )) { + INFO_LOG(GDROM, "GDR:fmt=2352;secfmt=2048"); + memcpy(dst, temp, 2048); + } + else if (fmt == 2048 && secfmt == SECFMT_2448_MODE2) { + // Pier Solar and the Great Architects + convertSector(temp, dst, 2448, fmt, FAD); + } + else { + WARN_LOG(GDROM, "ERROR: UNABLE TO CONVERT SECTOR. THIS IS FATAL. Format: %d Sector format: %d", fmt, secfmt); } - dst+=fmt; + dst += fmt; FAD++; } + return count; } void libGDR_ReadSubChannel(u8 * buff, u32 len) diff --git a/core/imgread/common.h b/core/imgread/common.h index 5397f7882..fc740a518 100644 --- a/core/imgread/common.h +++ b/core/imgread/common.h @@ -118,7 +118,7 @@ struct Disc DiscType type; std::string catalog; - void ReadSectors(u32 FAD, u32 count, u8 *dst, u32 fmt, LoadProgress *progress = nullptr); + u32 ReadSectors(u32 FAD, u32 count, u8 *dst, u32 fmt, bool stopOnMiss = false, LoadProgress *progress = nullptr); virtual ~Disc() { @@ -261,7 +261,7 @@ struct RawTrackFile : TrackFile DiscType GuessDiscType(bool m1, bool m2, bool da); //IO -void libGDR_ReadSector(u8 * buff,u32 StartSector,u32 SectorCount,u32 secsz); +u32 libGDR_ReadSector(u8 * buff, u32 StartSector, u32 SectorCount, u32 secsz, bool stopOnMiss = false); void libGDR_ReadSubChannel(u8 * buff, u32 len); void libGDR_GetToc(u32 *toc, DiskArea area); u32 libGDR_GetDiscType(); From 27a0497dd5f67d3ee8a35f30f1778758a341cf10 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 12 Dec 2024 15:52:07 +0100 Subject: [PATCH 56/81] win32: fix opening CD drive on command line. libretro build fix --- CMakeLists.txt | 2 +- core/deps/libretro-common/compat/fopen_utf8.c | 2 +- core/imgread/cdio.cpp | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e41bba96b..0edf3531c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -680,7 +680,7 @@ if(USE_LIBCDIO) target_compile_definitions(${PROJECT_NAME} PRIVATE USE_LIBCDIO) if(MINGW) # Force static link - target_link_libraries(${PROJECT_NAME} PRIVATE "-l:libcdio.a -l:libiconv.a") + target_link_libraries(${PROJECT_NAME} PRIVATE "-l:libcdio.a -l:libiconv.a -lwinmm") else() target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::CDIO) endif() diff --git a/core/deps/libretro-common/compat/fopen_utf8.c b/core/deps/libretro-common/compat/fopen_utf8.c index 52b481e7e..d3d467798 100644 --- a/core/deps/libretro-common/compat/fopen_utf8.c +++ b/core/deps/libretro-common/compat/fopen_utf8.c @@ -31,7 +31,7 @@ #endif #endif -#ifdef _WIN32 +#if defined(_WIN32) && !defined(USE_LIBCDIO) // libcdio has its equivalent version #undef fopen void *fopen_utf8(const char * filename, const char * mode) diff --git a/core/imgread/cdio.cpp b/core/imgread/cdio.cpp index 4d4f71b4b..8b328c11e 100755 --- a/core/imgread/cdio.cpp +++ b/core/imgread/cdio.cpp @@ -89,14 +89,21 @@ struct CdioDrive : public Disc if (!devices.empty()) { // If the list isn't empty, check that an entry exists for the current path + std::string lpath(path); +#ifdef _WIN32 + if (lpath.substr(0, 4) != "\\\\.\\") + lpath = "\\\\.\\" + lpath; +#endif bool found = false; for (const std::string& dev : devices) - if (dev == path) { + if (dev == lpath) { found = true; break; } - if (!found) + if (!found) { + WARN_LOG(GDROM, "%s isn't a CD device", path); return false; + } } pCdio = cdio_open(path, DRIVER_DEVICE); if (pCdio == nullptr) { From 4af1d096deebdebbca6d19456b19eb00ae42c01a Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 13 Dec 2024 15:58:32 +0100 Subject: [PATCH 57/81] audio: reset buffer on loadstate/term. Fix null audio driver on windows Eliminate audio clicks when loading a state or starting the next game. Fix overflow in null audio driver on windows. Detect fast forward and reset time counter. --- core/audio/audiobackend_null.cpp | 19 +++++++++++-------- core/audio/audiostream.cpp | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/core/audio/audiobackend_null.cpp b/core/audio/audiobackend_null.cpp index 67c935a7b..95a6b5647 100644 --- a/core/audio/audiobackend_null.cpp +++ b/core/audio/audiobackend_null.cpp @@ -19,17 +19,20 @@ class NullAudioBackend : public AudioBackend u32 push(const void* frame, u32 samples, bool wait) override { - if (wait) + if (wait && last_time.time_since_epoch() != the_clock::duration::zero()) { - if (last_time.time_since_epoch() != the_clock::duration::zero()) - { - auto fduration = std::chrono::nanoseconds(1000000000L * samples / 44100); - auto duration = fduration - (the_clock::now() - last_time); + auto fduration = std::chrono::nanoseconds(1'000'000'000LL * samples / 44100); + auto duration = fduration - (the_clock::now() - last_time); + if (duration > std::chrono::nanoseconds::zero()) std::this_thread::sleep_for(duration); - last_time += fduration; - } - else + if (duration < -std::chrono::milliseconds(67)) + // if ~4 frames ahead, reset time (fast forward detection) last_time = the_clock::now(); + else + last_time += fduration; + } + else { + last_time = the_clock::now(); } return 1; } diff --git a/core/audio/audiostream.cpp b/core/audio/audiostream.cpp index 4a6694edf..15f9a3965 100644 --- a/core/audio/audiostream.cpp +++ b/core/audio/audiostream.cpp @@ -1,5 +1,8 @@ #include "audiostream.h" #include "cfg/option.h" +#include "emulator.h" + +static void registerForEvents(); struct SoundFrame { s16 l; s16 r; }; @@ -60,6 +63,7 @@ void WriteSample(s16 r, s16 l) void InitAudio() { + registerForEvents(); TermAudio(); std::string slug = config::AudioBackend; @@ -138,3 +142,19 @@ void StopAudioRecording() currentBackend->termRecord(); audio_recording_started = false; } + +static void registerForEvents() +{ + static bool done; + if (done) + return; + done = true; + // Empty the audio buffer when loading a state or terminating the game + const auto& callback = [](Event, void *) { + writePtr = 0; + }; + EventManager::listen(Event::Terminate, callback); + EventManager::listen(Event::LoadState, callback); +} + + From 686f08e7dd932d4ba3ca5ff653ac21ca3c2bb29d Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 13 Dec 2024 16:49:57 +0100 Subject: [PATCH 58/81] vgamepad: hide btn0 and map reload to btn1 for lightgun games --- core/ui/gui.cpp | 1 - core/ui/vgamepad.cpp | 109 +++++++++++++++++++++++++++---------------- 2 files changed, 69 insertions(+), 41 deletions(-) diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 369d6fae4..44bd8d994 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -3033,7 +3033,6 @@ static void gui_display_settings() { maple_ReconnectDevices(); reset_vmus(); - vgamepad::startGame(); } } SaveSettings(); diff --git a/core/ui/vgamepad.cpp b/core/ui/vgamepad.cpp index c96c8dbe6..2e239e45b 100644 --- a/core/ui/vgamepad.cpp +++ b/core/ui/vgamepad.cpp @@ -273,34 +273,55 @@ ControlId hitTest(float x, float y) return None; } -u32 controlToDcKey(ControlId control) +static u32 buttonMap[_Count] { + DC_DPAD_LEFT, + DC_DPAD_UP, + DC_DPAD_RIGHT, + DC_DPAD_DOWN, + DC_BTN_X, + DC_BTN_Y, + DC_BTN_B, + DC_BTN_A, + DC_BTN_START, + DC_AXIS_LT, + DC_AXIS_RT, + 0, // not used: analog area + 0, // not used: analog stick + EMU_BTN_FFORWARD, + DC_BTN_Y, // button 4 + DC_BTN_Z, // button 5 + EMU_BTN_SRVMODE, + DC_BTN_INSERT_CARD, + DC_DPAD_LEFT | DC_DPAD_UP, + DC_DPAD_RIGHT | DC_DPAD_UP, + DC_DPAD_LEFT | DC_DPAD_DOWN, + DC_DPAD_RIGHT | DC_DPAD_DOWN, +}; + +void setButtonMap() { const bool arcade = settings.platform.isArcade(); - switch (control) + if (serviceMode) + { + buttonMap[A] = DC_BTN_D; + buttonMap[B] = DC_DPAD2_UP; + buttonMap[X] = DC_DPAD2_DOWN; + } + else { - case Left: return DC_DPAD_LEFT; - case Up: return DC_DPAD_UP; - case Right: return DC_DPAD_RIGHT; - case Down: return DC_DPAD_DOWN; - case X: return serviceMode ? DC_DPAD2_DOWN : arcade ? DC_BTN_C : DC_BTN_X; - case Y: return arcade ? DC_BTN_X : DC_BTN_Y; - case B: return serviceMode ? DC_DPAD2_UP : DC_BTN_B; - case A: return serviceMode ? DC_BTN_D : DC_BTN_A; - case Start: return DC_BTN_START; - case LeftTrigger: return DC_AXIS_LT; - case RightTrigger: return DC_AXIS_RT; - case FastForward: return EMU_BTN_FFORWARD; - case LeftUp: return DC_DPAD_LEFT | DC_DPAD_UP; - case RightUp: return DC_DPAD_RIGHT | DC_DPAD_UP; - case LeftDown: return DC_DPAD_LEFT | DC_DPAD_DOWN; - case RightDown: return DC_DPAD_RIGHT | DC_DPAD_DOWN; - // Arcade - case Btn4: return DC_BTN_Y; - case Btn5: return DC_BTN_Z; - case InsertCard: return DC_BTN_INSERT_CARD; - case ServiceMode: return EMU_BTN_SRVMODE; - default: return 0; + buttonMap[A] = DC_BTN_A; + buttonMap[B] = DC_BTN_B; + buttonMap[X] = arcade ? DC_BTN_C : DC_BTN_X; } + buttonMap[Y] = arcade ? DC_BTN_X : DC_BTN_Y; +} + +u32 controlToDcKey(ControlId control) +{ + if (control >= Left && control < _Count) + return buttonMap[control]; + else + return 0; } void setAnalogStick(float x, float y) { @@ -315,10 +336,12 @@ float getControlWidth(ControlId control) { void toggleServiceMode() { serviceMode = !serviceMode; - if (serviceMode) { + if (serviceMode) + { Controls[A].disabled = false; Controls[B].disabled = false; Controls[X].disabled = false; + setButtonMap(); } else { startGame(); @@ -382,17 +405,17 @@ void draw() } ImDrawList *drawList = ImGui::GetBackgroundDrawList(); - drawButton(drawList, Controls[Left], kcode[0] & DC_DPAD_LEFT); - drawButton(drawList, Controls[Up], kcode[0] & DC_DPAD_UP); - drawButton(drawList, Controls[Right], kcode[0] & DC_DPAD_RIGHT); - drawButton(drawList, Controls[Down], kcode[0] & DC_DPAD_DOWN); + drawButton(drawList, Controls[Left], kcode[0] & buttonMap[Left]); + drawButton(drawList, Controls[Up], kcode[0] & buttonMap[Up]); + drawButton(drawList, Controls[Right], kcode[0] & buttonMap[Right]); + drawButton(drawList, Controls[Down], kcode[0] & buttonMap[Down]); - drawButton(drawList, Controls[X], kcode[0] & (serviceMode ? DC_DPAD2_DOWN : settings.platform.isConsole() ? DC_BTN_X : DC_BTN_C)); - drawButton(drawList, Controls[Y], kcode[0] & (settings.platform.isConsole() ? DC_BTN_Y : DC_BTN_X)); - drawButton(drawList, Controls[B], kcode[0] & (serviceMode ? DC_DPAD2_UP : DC_BTN_B)); - drawButton(drawList, Controls[A], kcode[0] & (serviceMode ? DC_BTN_D : DC_BTN_A)); + drawButton(drawList, Controls[X], kcode[0] & buttonMap[X]); + drawButton(drawList, Controls[Y], kcode[0] & buttonMap[Y]); + drawButton(drawList, Controls[B], kcode[0] & buttonMap[B]); + drawButton(drawList, Controls[A], kcode[0] & buttonMap[A]); - drawButton(drawList, Controls[Start], kcode[0] & DC_BTN_START); + drawButton(drawList, Controls[Start], kcode[0] & buttonMap[Start]); drawButtonDim(drawList, Controls[LeftTrigger], lt[0] >> 8); @@ -403,10 +426,10 @@ void draw() drawButton(drawList, Controls[FastForward], false); - drawButton(drawList, Controls[Btn4], kcode[0] & DC_BTN_Y); - drawButton(drawList, Controls[Btn5], kcode[0] & DC_BTN_Z); + drawButton(drawList, Controls[Btn4], kcode[0] & buttonMap[Btn4]); + drawButton(drawList, Controls[Btn5], kcode[0] & buttonMap[Btn5]); drawButton(drawList, Controls[ServiceMode], !serviceMode); - drawButton(drawList, Controls[InsertCard], kcode[0] & DC_BTN_INSERT_CARD); + drawButton(drawList, Controls[InsertCard], kcode[0] & buttonMap[InsertCard]); AlphaTrans += ((float)Visible - AlphaTrans) / 2; } @@ -686,6 +709,7 @@ void startGame() { enableAllControls(); serviceMode = false; + setButtonMap(); if (settings.platform.isConsole()) { disableControl(Btn4); @@ -780,7 +804,7 @@ void startGame() if (settings.platform.isAtomiswave()) { // button order: A B X Y B4 - if ((usedButtons & AWAVE_BTN0_KEY) == 0) + if ((usedButtons & AWAVE_BTN0_KEY) == 0 || settings.input.lightgunGame) disableControl(A); if ((usedButtons & AWAVE_BTN1_KEY) == 0) disableControl(B); @@ -822,10 +846,15 @@ void startGame() } else { - if ((usedButtons & NAOMI_BTN0_KEY) == 0) + if ((usedButtons & NAOMI_BTN0_KEY) == 0 || settings.input.lightgunGame) disableControl(A); - if ((usedButtons & NAOMI_BTN1_KEY) == 0) + if ((usedButtons & (NAOMI_BTN1_KEY | NAOMI_RELOAD_KEY)) == 0) disableControl(B); + else if (settings.input.lightgunGame + && (usedButtons & NAOMI_RELOAD_KEY) != 0 + && (usedButtons & NAOMI_BTN1_KEY) == 0) + // Remap button 1 to reload for lightgun games that need it + buttonMap[B] = DC_BTN_RELOAD; if ((usedButtons & NAOMI_BTN2_KEY) == 0) // C disableControl(X); From 421245aeba76ad4e7f1b545794f485f6be4a8722 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 13 Dec 2024 17:21:32 +0100 Subject: [PATCH 59/81] lightgun xhair: centralize logic in crosshairNeeded() --- core/hw/maple/maple_cfg.cpp | 6 ---- core/rend/dx11/dx11_overlay.cpp | 53 +++++++++++++++---------------- core/rend/dx11/dx11context.cpp | 10 +++--- core/rend/dx9/d3d_overlay.cpp | 43 ++++++++++++------------- core/rend/dx9/dxcontext.cpp | 5 +-- core/rend/gles/gldraw.cpp | 11 ++----- core/rend/osd.h | 19 ++++++++--- core/rend/vulkan/oit/oit_drawer.h | 1 - core/rend/vulkan/overlay.cpp | 7 ++-- 9 files changed, 68 insertions(+), 87 deletions(-) diff --git a/core/hw/maple/maple_cfg.cpp b/core/hw/maple/maple_cfg.cpp index 9ce4199de..4cc0f51ab 100644 --- a/core/hw/maple/maple_cfg.cpp +++ b/core/hw/maple/maple_cfg.cpp @@ -405,12 +405,6 @@ void mcfg_CreateDevices() die("Unknown system"); break; } - if (settings.platform.isArcade() && !settings.input.fourPlayerGames) - { - // No known 4-player lightgun/touchscreen game so far - config::CrosshairColor[2].override(0); - config::CrosshairColor[3].override(0); - } vmuDigest(); } diff --git a/core/rend/dx11/dx11_overlay.cpp b/core/rend/dx11/dx11_overlay.cpp index ff507c56e..2c0354741 100644 --- a/core/rend/dx11/dx11_overlay.cpp +++ b/core/rend/dx11/dx11_overlay.cpp @@ -142,39 +142,36 @@ void DX11Overlay::draw(u32 width, u32 height, bool vmu, bool crosshair) quad.draw(vmuTextureViews[i], samplers->getSampler(false)); } } - if (crosshair && crosshairsNeeded()) + if (crosshair) { - if (!xhairTexture) + for (u32 i = 0; i < config::CrosshairColor.size(); i++) { - const u32* texData = getCrosshairTextureData(); - D3D11_TEXTURE2D_DESC desc{}; - desc.Width = 16; - desc.Height = 16; - desc.ArraySize = 1; - desc.SampleDesc.Count = 1; - desc.Usage = D3D11_USAGE_DEFAULT; - desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; - desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - desc.MipLevels = 1; - - if (SUCCEEDED(device->CreateTexture2D(&desc, nullptr, &xhairTexture.get()))) + if (!crosshairNeeded(i)) + continue; + if (!xhairTexture) { - D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc{}; - viewDesc.Format = desc.Format; - viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; - viewDesc.Texture2D.MipLevels = desc.MipLevels; - device->CreateShaderResourceView(xhairTexture, &viewDesc, &xhairTextureView.get()); + const u32* texData = getCrosshairTextureData(); + D3D11_TEXTURE2D_DESC desc{}; + desc.Width = 16; + desc.Height = 16; + desc.ArraySize = 1; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.MipLevels = 1; + + if (SUCCEEDED(device->CreateTexture2D(&desc, nullptr, &xhairTexture.get()))) + { + D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc{}; + viewDesc.Format = desc.Format; + viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + viewDesc.Texture2D.MipLevels = desc.MipLevels; + device->CreateShaderResourceView(xhairTexture, &viewDesc, &xhairTextureView.get()); - deviceContext->UpdateSubresource(xhairTexture, 0, nullptr, texData, 16 * 4, 16 * 4 * 16); + deviceContext->UpdateSubresource(xhairTexture, 0, nullptr, texData, 16 * 4, 16 * 4 * 16); + } } - } - for (u32 i = 0; i < config::CrosshairColor.size(); i++) - { - if (config::CrosshairColor[i] == 0) - continue; - if (settings.platform.isConsole() - && config::MapleMainDevices[i] != MDT_LightGun) - continue; auto [x, y] = getCrosshairPosition(i); #ifdef LIBRETRO diff --git a/core/rend/dx11/dx11context.cpp b/core/rend/dx11/dx11context.cpp index 0e0d5cf4d..808efac00 100644 --- a/core/rend/dx11/dx11context.cpp +++ b/core/rend/dx11/dx11context.cpp @@ -253,7 +253,10 @@ void DX11Context::EndImGuiFrame() { if (pDevice && pDeviceContext && renderTargetView) { - if (!overlayOnly) + if (overlayOnly) { + overlay.draw(settings.display.width, settings.display.height, config::FloatVMUs, true); + } + else { pDeviceContext->OMSetRenderTargets(1, &renderTargetView.get(), nullptr); const FLOAT black[4] { 0.f, 0.f, 0.f, 1.f }; @@ -261,11 +264,6 @@ void DX11Context::EndImGuiFrame() if (renderer != nullptr) renderer->RenderLastFrame(); } - if (overlayOnly) - { - if (crosshairsNeeded() || config::FloatVMUs) - overlay.draw(settings.display.width, settings.display.height, config::FloatVMUs, true); - } ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); } frameRendered = true; diff --git a/core/rend/dx9/d3d_overlay.cpp b/core/rend/dx9/d3d_overlay.cpp index b62c2ecb7..4dc527b7a 100644 --- a/core/rend/dx9/d3d_overlay.cpp +++ b/core/rend/dx9/d3d_overlay.cpp @@ -87,35 +87,32 @@ void D3DOverlay::draw(u32 width, u32 height, bool vmu, bool crosshair) drawQuad(rect, D3DCOLOR_ARGB(192, 255, 255, 255)); } } - if (crosshair && crosshairsNeeded()) + if (crosshair) { - if (!xhairTexture) + for (u32 i = 0; i < config::CrosshairColor.size(); i++) { - const u32* texData = getCrosshairTextureData(); - device->CreateTexture(16, 16, 1, 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &xhairTexture.get(), 0); - D3DLOCKED_RECT rect; - if (SUCCEEDED(xhairTexture->LockRect(0, &rect, nullptr, 0))) + if (!crosshairNeeded(i)) + continue; + + if (!xhairTexture) { - if (rect.Pitch == 16 * sizeof(u32)) - memcpy(rect.pBits, texData, 16 * 16 * sizeof(u32)); - else + const u32* texData = getCrosshairTextureData(); + device->CreateTexture(16, 16, 1, 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &xhairTexture.get(), 0); + D3DLOCKED_RECT rect; + if (SUCCEEDED(xhairTexture->LockRect(0, &rect, nullptr, 0))) { - u8 *dst = (u8 *) rect.pBits; - for (int y = 0; y < 16; y++) - memcpy(dst + y * rect.Pitch, texData + y * 16, 16 * sizeof(u32)); + if (rect.Pitch == 16 * sizeof(u32)) + memcpy(rect.pBits, texData, 16 * 16 * sizeof(u32)); + else + { + u8 *dst = (u8 *) rect.pBits; + for (int y = 0; y < 16; y++) + memcpy(dst + y * rect.Pitch, texData + y * 16, 16 * sizeof(u32)); + } + xhairTexture->UnlockRect(0); } - xhairTexture->UnlockRect(0); } - } - device->SetTexture(0, xhairTexture); - for (u32 i = 0; i < config::CrosshairColor.size(); i++) - { - if (config::CrosshairColor[i] == 0) - continue; - if (settings.platform.isConsole() - && config::MapleMainDevices[i] != MDT_LightGun) - continue; - + device->SetTexture(0, xhairTexture); auto [x, y] = getCrosshairPosition(i); float halfWidth = config::CrosshairSize * settings.display.uiScale / 2.f; RECT rect { (long) (x - halfWidth), (long) (y - halfWidth), (long) (x + halfWidth), (long) (y + halfWidth) }; diff --git a/core/rend/dx9/dxcontext.cpp b/core/rend/dx9/dxcontext.cpp index f57306725..2fb35aa07 100644 --- a/core/rend/dx9/dxcontext.cpp +++ b/core/rend/dx9/dxcontext.cpp @@ -196,10 +196,7 @@ void DXContext::EndImGuiFrame() if (SUCCEEDED(pDevice->BeginScene())) { if (overlayOnly) - { - if (crosshairsNeeded() || config::FloatVMUs) - overlay.draw(settings.display.width, settings.display.height, config::FloatVMUs, true); - } + overlay.draw(settings.display.width, settings.display.height, config::FloatVMUs, true); ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData()); pDevice->EndScene(); } diff --git a/core/rend/gles/gldraw.cpp b/core/rend/gles/gldraw.cpp index dcf0447be..01327cc26 100644 --- a/core/rend/gles/gldraw.cpp +++ b/core/rend/gles/gldraw.cpp @@ -1015,12 +1015,6 @@ static void updateLightGunTexture() static void drawGunCrosshair(u8 port, int width, int height) { - if (config::CrosshairColor[port] == 0) - return; - if (settings.platform.isConsole() - && config::MapleMainDevices[port] != MDT_LightGun) - return; - auto [x, y] = getCrosshairPosition(port); #ifdef LIBRETRO float halfWidth = lightgun_crosshair_size / 2.f / config::ScreenStretching * 100.f * config::RenderResolution / 480.f; @@ -1073,10 +1067,9 @@ void drawVmusAndCrosshairs(int width, int height) drawVmuTexture(i, width, height); } - if (crosshairsNeeded()) { - for (int i = 0 ; i < 4 ; i++) + for (int i = 0 ; i < 4 ; i++) + if (crosshairNeeded(i)) drawGunCrosshair(i, width, height); - } glCheck(); } diff --git a/core/rend/osd.h b/core/rend/osd.h index 60a202797..f88a13846 100644 --- a/core/rend/osd.h +++ b/core/rend/osd.h @@ -30,14 +30,23 @@ void push_vmu_screen(int bus_id, int bus_port, u8* buffer); const u32 *getCrosshairTextureData(); std::pair getCrosshairPosition(int playerNum); -static inline bool crosshairsNeeded() +static inline bool crosshairNeeded(int port) { - if (config::CrosshairColor[0] == 0 && config::CrosshairColor[1] == 0 - && config::CrosshairColor[2] == 0 && config::CrosshairColor[3] == 0) + if (port < 0 || port >= 4) return false; - if (settings.platform.isArcade() && !settings.input.lightgunGame) - // not a lightgun game + if (config::CrosshairColor[port] == 0) return false; + if (settings.platform.isArcade()) + { + // Arcade game: only for lightgun games and P1 or P2 (no known 4-player lightgun or touchscreen game for now) + if (!settings.input.lightgunGame || (port >= 2 && !settings.input.fourPlayerGames)) + return false; + } + else { + // Console game + if (config::MapleMainDevices[port] != MDT_LightGun) + return false; + } return true; } diff --git a/core/rend/vulkan/oit/oit_drawer.h b/core/rend/vulkan/oit/oit_drawer.h index 7075351bf..7ea1a5613 100644 --- a/core/rend/vulkan/oit/oit_drawer.h +++ b/core/rend/vulkan/oit/oit_drawer.h @@ -126,7 +126,6 @@ class OITDrawer : public BaseDrawer SamplerManager *samplerManager = nullptr; OITBuffers *oitBuffers = nullptr; bool needAttachmentTransition = false; - bool needDepthTransition = false; OITDescriptorSets descriptorSets; vk::Buffer curMainBuffer; bool dithering = false; diff --git a/core/rend/vulkan/overlay.cpp b/core/rend/vulkan/overlay.cpp index 78bb7fb5e..483f1e8d9 100644 --- a/core/rend/vulkan/overlay.cpp +++ b/core/rend/vulkan/overlay.cpp @@ -212,19 +212,16 @@ void VulkanOverlay::Draw(vk::CommandBuffer commandBuffer, vk::Extent2D viewport, drawers[i]->Draw(commandBuffer, vmuTextures[i]->GetImageView(), vtx, true, color); } } - if (crosshair && crosshairsNeeded()) + if (crosshair) { pipeline->BindPipeline(commandBuffer); bool imageViewBound = false; for (size_t i = 0; i < config::CrosshairColor.size(); i++) { - if (config::CrosshairColor[i] == 0) - continue; - if (settings.platform.isConsole() && config::MapleMainDevices[i] != MDT_LightGun) + if (!crosshairNeeded(i)) continue; auto [x, y] = getCrosshairPosition(i); - #ifdef LIBRETRO float w = lightgun_crosshair_size * scaling / config::ScreenStretching * 100.f; float h = lightgun_crosshair_size * scaling; From 20517cf0da48514bfd45fcd259208337e99ab9ca Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 14 Dec 2024 16:18:35 +0100 Subject: [PATCH 60/81] vgamepad: enable touch mouse when lightgun used Use unique touch mouse class for iOS and Android. Enable touch mouse for arcade lightgun games or when maple device A is a lg. Disable it otherwise. android: only display controllers that actually send events, including system mouse and keyboard. Use separate Touch Mouse and System Mouse. iOS: fix missing button input for touch mouse. Don't bubble up touch events that hit vgampad buttons or controls. --- core/input/gamepad_device.h | 12 +++- core/input/mouse.h | 12 ++++ core/ui/vgamepad.cpp | 20 +++++- .../com/flycast/emulator/BaseGLActivity.java | 30 ++++---- .../emulator/emu/VirtualJoystickDelegate.java | 10 +-- .../emulator/periph/InputDeviceManager.java | 72 ++++++++++++------- .../flycast/src/main/jni/src/Android.cpp | 3 +- .../src/main/jni/src/android_gamepad.h | 2 +- .../src/main/jni/src/android_input.cpp | 47 +++++++++--- .../emulator-ios/emulator/EmulatorView.mm | 10 ++- .../emulator/PadViewController.mm | 24 +++++-- .../apple/emulator-ios/emulator/ios_gamepad.h | 11 --- 12 files changed, 172 insertions(+), 81 deletions(-) diff --git a/core/input/gamepad_device.h b/core/input/gamepad_device.h index 2799b7abf..4cf560f68 100644 --- a/core/input/gamepad_device.h +++ b/core/input/gamepad_device.h @@ -93,14 +93,22 @@ class GamepadDevice } static void Register(const std::shared_ptr& gamepad); - static void Unregister(const std::shared_ptr& gamepad); - static int GetGamepadCount(); static std::shared_ptr GetGamepad(int index); static void SaveMaplePorts(); static void RampAnalog(); + template + static std::shared_ptr GetGamepad() + { + Lock _(_gamepads_mutex); + for (const auto& gamepad : _gamepads) + if (dynamic_cast(gamepad.get()) != nullptr) + return std::dynamic_pointer_cast(gamepad); + return {}; + } + static void load_system_mappings(); bool find_mapping(int system = settings.platform.system); virtual void resetMappingToDefault(bool arcade, bool gamepad) { diff --git a/core/input/mouse.h b/core/input/mouse.h index d17f8dcf1..f5f238815 100644 --- a/core/input/mouse.h +++ b/core/input/mouse.h @@ -104,3 +104,15 @@ class SystemMouse : public Mouse void setButton(Button button, bool pressed); void setWheel(int delta); }; + +class TouchMouse : public SystemMouse +{ +public: + TouchMouse() : SystemMouse("Flycast", -1) + { + _name = "Touch Mouse"; + _unique_id = "touch_mouse"; + loadMapping(); + } +}; + diff --git a/core/ui/vgamepad.cpp b/core/ui/vgamepad.cpp index 2e239e45b..adf0f5ea4 100644 --- a/core/ui/vgamepad.cpp +++ b/core/ui/vgamepad.cpp @@ -30,6 +30,7 @@ #include "oslib/resources.h" #include "cfg/cfg.h" #include "input/gamepad.h" +#include "input/mouse.h" #include "hw/naomi/naomi_cart.h" #include "hw/naomi/card_reader.h" #include "hw/maple/maple_devs.h" @@ -710,6 +711,7 @@ void startGame() enableAllControls(); serviceMode = false; setButtonMap(); + bool enableTouchMouse = false; if (settings.platform.isConsole()) { disableControl(Btn4); @@ -719,7 +721,7 @@ void startGame() switch (config::MapleMainDevices[0]) { case MDT_LightGun: - // TODO enable mouse? + enableTouchMouse = true; disableControl(AnalogArea); disableControl(LeftTrigger); disableControl(RightTrigger); @@ -878,12 +880,14 @@ void startGame() if ((usedButtons & NAOMI_START_KEY) == 0) disableControl(Start); } + if (settings.input.lightgunGame) + enableTouchMouse = true; } else { if (settings.input.lightgunGame) { - // TODO enable mouse? + enableTouchMouse = true; disableControl(A); disableControl(X); disableControl(Y); @@ -906,6 +910,18 @@ void startGame() } } } + std::shared_ptr touchMouse = GamepadDevice::GetGamepad(); + if (touchMouse != nullptr) + { + if (enableTouchMouse) { + if (touchMouse->maple_port() == -1) + touchMouse->set_maple_port(0); + } + else { + if (touchMouse->maple_port() == 0) + touchMouse->set_maple_port(-1); + } + } } void resetEditing() { diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java index 6af259689..873a4334b 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java @@ -269,7 +269,7 @@ private boolean showMenu() { private boolean processJoystickInput(MotionEvent event, int axis) { float v = event.getAxisValue(axis); - return InputDeviceManager.getInstance().joystickAxisEvent(event.getDeviceId(), axis, (int)Math.round(v * 32767.f)); + return InputDeviceManager.getInstance().axisEvent(event.getDeviceId(), axis, (int)Math.round(v * 32767.f)); } @Override public boolean onGenericMotionEvent(MotionEvent event) { @@ -283,29 +283,29 @@ public boolean onGenericMotionEvent(MotionEvent event) { if (range.getAxis() == MotionEvent.AXIS_HAT_X) { float v = event.getAxisValue(MotionEvent.AXIS_HAT_X); if (v == -1.0) { - rc |= InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_LEFT, true); - InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_RIGHT, false); + rc |= InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_LEFT, true); + InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_RIGHT, false); } else if (v == 1.0) { - InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_LEFT, false); - rc |= InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_RIGHT, true); + InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_LEFT, false); + rc |= InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_RIGHT, true); } else { - InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_LEFT, false); - InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_RIGHT, false); + InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_LEFT, false); + InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_RIGHT, false); } } else if (range.getAxis() == MotionEvent.AXIS_HAT_Y) { float v = event.getAxisValue(MotionEvent.AXIS_HAT_Y); if (v == -1.0) { - rc |= InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_UP, true); - InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_DOWN, false); + rc |= InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_UP, true); + InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_DOWN, false); } else if (v == 1.0) { - InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_UP, false); - rc |= InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_DOWN, true); + InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_UP, false); + rc |= InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_DOWN, true); } else { - InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_UP, false); - InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_DOWN, false); + InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_UP, false); + InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), KeyEvent.KEYCODE_DPAD_DOWN, false); } } else @@ -329,7 +329,7 @@ else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == InputDevice.S @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), keyCode, false)) + if (InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), keyCode, false)) return true; if (hasKeyboard && InputDeviceManager.getInstance().keyboardEvent(keyCode, false)) return true; @@ -348,7 +348,7 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { } return true; } - if (InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), keyCode, true)) + if (InputDeviceManager.getInstance().buttonEvent(event.getDeviceId(), keyCode, true)) return true; if (hasKeyboard) { diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java index a0276225f..3ecae4bee 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java @@ -74,7 +74,7 @@ private boolean touchMouseEvent(MotionEvent event) case MotionEvent.ACTION_CANCEL: mousePid = -1; mouseButtons = 0; - InputDeviceManager.getInstance().mouseEvent(mousePos[0], mousePos[1], mouseButtons); + InputDeviceManager.getInstance().touchMouseEvent(mousePos[0], mousePos[1], mouseButtons); return true; case MotionEvent.ACTION_POINTER_DOWN: @@ -85,7 +85,7 @@ private boolean touchMouseEvent(MotionEvent event) mousePos[0] = Math.round(event.getX(actionIndex)); mousePos[1] = Math.round(event.getY(actionIndex)); mouseButtons = MotionEvent.BUTTON_PRIMARY; // Mouse left button down - InputDeviceManager.getInstance().mouseEvent(mousePos[0], mousePos[1], mouseButtons); + InputDeviceManager.getInstance().touchMouseEvent(mousePos[0], mousePos[1], mouseButtons); return true; } return false; @@ -96,7 +96,7 @@ private boolean touchMouseEvent(MotionEvent event) if (event.getPointerId(i) == mousePid) { mousePos[0] = Math.round(event.getX(i)); mousePos[1] = Math.round(event.getY(i)); - InputDeviceManager.getInstance().mouseEvent(mousePos[0], mousePos[1], mouseButtons); + InputDeviceManager.getInstance().touchMouseEvent(mousePos[0], mousePos[1], mouseButtons); break; } } @@ -108,7 +108,7 @@ private boolean touchMouseEvent(MotionEvent event) mousePos[0] = Math.round(event.getX(actionIndex)); mousePos[1] = Math.round(event.getY(actionIndex)); mouseButtons = 0; - InputDeviceManager.getInstance().mouseEvent(mousePos[0], mousePos[1], mouseButtons); + InputDeviceManager.getInstance().touchMouseEvent(mousePos[0], mousePos[1], mouseButtons); return true; } break; @@ -168,7 +168,7 @@ public boolean onTouchEvent(MotionEvent event, int width, int height) // Release the mouse too mousePid = -1; mouseButtons = 0; - InputDeviceManager.getInstance().mouseEvent(mousePos[0], mousePos[1], mouseButtons); + InputDeviceManager.getInstance().touchMouseEvent(mousePos[0], mousePos[1], mouseButtons); // Then fall through case MotionEvent.ACTION_POINTER_DOWN: { diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java index 09947d944..25850fc2a 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java @@ -11,8 +11,10 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.commons.lang3.ArrayUtils; @@ -32,6 +34,7 @@ private static class VibrationParams { long stopTime; } private Map vibParams = new HashMap<>(); + private Set knownDevices = new HashSet<>(); public InputDeviceManager() { @@ -45,9 +48,6 @@ public void startListening(Context applicationContext) if (hasTouchscreen) joystickAdded(VIRTUAL_GAMEPAD_ID, null, 0, null, null, null, getVibrator(VIRTUAL_GAMEPAD_ID) != null); - int[] ids = InputDevice.getDeviceIds(); - for (int id : ids) - onInputDeviceAdded(id); inputManager = (InputManager)applicationContext.getSystemService(Context.INPUT_SERVICE); inputManager.registerInputDeviceListener(this, null); } @@ -63,25 +63,6 @@ public void stopListening() @Override public void onInputDeviceAdded(int i) { - InputDevice device = InputDevice.getDevice(i); - if (device != null && (device.getSources() & InputDevice.SOURCE_CLASS_BUTTON) == InputDevice.SOURCE_CLASS_BUTTON) { - int port = 0; - if ((device.getSources() & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) { - port = this.maple_port == 3 ? 3 : this.maple_port++; - } - List axes = device.getMotionRanges(); - List fullAxes = new ArrayList<>(); - List halfAxes = new ArrayList<>(); - for (InputDevice.MotionRange range : axes) { - if (range.getMin() == 0) - halfAxes.add(range.getAxis()); - else - fullAxes.add(range.getAxis()); - } - joystickAdded(i, device.getName(), port, device.getDescriptor(), - ArrayUtils.toPrimitive(fullAxes.toArray(new Integer[0])), ArrayUtils.toPrimitive(halfAxes.toArray(new Integer[0])), - getVibrator(i) != null); - } } @Override @@ -89,6 +70,7 @@ public void onInputDeviceRemoved(int i) { if (maple_port > 0) maple_port--; joystickRemoved(i); + knownDevices.remove(i); } @Override @@ -209,6 +191,47 @@ public boolean hasTouchscreen() { return hasTouchscreen; } + private boolean createDevice(int id) + { + if (id == 0) + return false; + if (knownDevices.contains(id)) + return true; + InputDevice device = InputDevice.getDevice(id); + if (device == null || (device.getSources() & InputDevice.SOURCE_CLASS_BUTTON) != InputDevice.SOURCE_CLASS_BUTTON) + return false; + int port = 0; + if ((device.getSources() & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) { + port = this.maple_port == 3 ? 3 : this.maple_port++; + } + List axes = device.getMotionRanges(); + List fullAxes = new ArrayList<>(); + List halfAxes = new ArrayList<>(); + for (InputDevice.MotionRange range : axes) { + if (range.getMin() == 0) + halfAxes.add(range.getAxis()); + else + fullAxes.add(range.getAxis()); + } + joystickAdded(id, device.getName(), port, device.getDescriptor(), + ArrayUtils.toPrimitive(fullAxes.toArray(new Integer[0])), ArrayUtils.toPrimitive(halfAxes.toArray(new Integer[0])), + getVibrator(id) != null); + knownDevices.add(id); + return true; + } + public boolean buttonEvent(int id, int button, boolean pressed) + { + if (!createDevice(id)) + return false; + return joystickButtonEvent(id, button, pressed); + } + public boolean axisEvent(int id, int button, int value) + { + if (!createDevice(id)) + return false; + return joystickAxisEvent(id, button, value); + } + public static InputDeviceManager getInstance() { return INSTANCE; } @@ -217,10 +240,11 @@ public static InputDeviceManager getInstance() { public native void virtualReleaseAll(); public native void virtualJoystick(float x, float y); public native void virtualButtonInput(int key, boolean pressed); - public native boolean joystickButtonEvent(int id, int button, boolean pressed); - public native boolean joystickAxisEvent(int id, int button, int value); + private native boolean joystickButtonEvent(int id, int button, boolean pressed); + private native boolean joystickAxisEvent(int id, int button, int value); public native void mouseEvent(int xpos, int ypos, int buttons); public native void mouseScrollEvent(int scrollValue); + public native void touchMouseEvent(int xpos, int ypos, int buttons); private native void joystickAdded(int id, String name, int maple_port, String uniqueId, int[] fullAxes, int[] halfAxes, boolean rumbleEnabled); private native void joystickRemoved(int id); public native boolean keyboardEvent(int key, boolean pressed); diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index 955803ded..e4a250907 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -17,6 +17,7 @@ #endif #include "jni_util.h" #include "android_storage.h" +#include "http_client.h" #include #include @@ -35,8 +36,6 @@ namespace jni thread_local JVMAttacher jvm_attacher; } -#include "http_client.h" - extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_screenCharacteristics(JNIEnv *env, jobject obj, jfloat screenDpi, jfloat refreshRate) { settings.display.dpi = screenDpi; diff --git a/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h b/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h index 79d1cf985..9b9fd4801 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h +++ b/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h @@ -411,7 +411,7 @@ class AndroidMouse : public SystemMouse class AndroidVirtualGamepad : public VirtualGamepad { public: - AndroidVirtualGamepad(bool rumbleEnabled) : VirtualGamepad("Android") { + AndroidVirtualGamepad(bool rumbleEnabled) : VirtualGamepad("Flycast") { this->rumbleEnabled = rumbleEnabled; } diff --git a/shell/android-studio/flycast/src/main/jni/src/android_input.cpp b/shell/android-studio/flycast/src/main/jni/src/android_input.cpp index 074663ca2..9814a84f0 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_input.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/android_input.cpp @@ -23,6 +23,7 @@ #include "hw/maple/maple_if.h" std::shared_ptr mouse; +std::shared_ptr touchMouse; std::shared_ptr keyboard; std::shared_ptr virtualGamepad; @@ -96,11 +97,6 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceMa { input_device_manager = env->NewGlobalRef(obj); input_device_manager_rumble = env->GetMethodID(env->GetObjectClass(obj), "rumble", "(IFFI)Z"); - // FIXME Don't connect it by default or any screen touch will register as button A press - mouse = std::make_shared(-1); - GamepadDevice::Register(mouse); - keyboard = std::make_shared(); - GamepadDevice::Register(keyboard); gui_setOnScreenKeyboardCallback([](bool show) { if (g_activity != nullptr) jni::env()->CallVoidMethod(g_activity, showScreenKeyboardMid, show); @@ -110,9 +106,14 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceMa extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickAdded(JNIEnv *env, jobject obj, jint id, jstring name, jint maple_port, jstring junique_id, jintArray fullAxes, jintArray halfAxes, jboolean hasRumble) { - if (id == AndroidVirtualGamepad::GAMEPAD_ID) { + if (id == 0) + return; + if (id == AndroidVirtualGamepad::GAMEPAD_ID) + { virtualGamepad = std::make_shared(hasRumble); GamepadDevice::Register(virtualGamepad); + touchMouse = std::make_shared(); + GamepadDevice::Register(touchMouse); } else { @@ -130,9 +131,12 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceMa extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickRemoved(JNIEnv *env, jobject obj, jint id) { - if (id == AndroidVirtualGamepad::GAMEPAD_ID) { + if (id == AndroidVirtualGamepad::GAMEPAD_ID) + { GamepadDevice::Unregister(virtualGamepad); virtualGamepad.reset(); + GamepadDevice::Unregister(touchMouse); + touchMouse.reset(); } else { std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); @@ -169,7 +173,12 @@ extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDevi } extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_keyboardEvent(JNIEnv *env, jobject obj, - jint key, jboolean pressed) { + jint key, jboolean pressed) +{ + if (keyboard == nullptr) { + keyboard = std::make_shared(); + GamepadDevice::Register(keyboard); + } keyboard->input(key, pressed); return true; } @@ -191,9 +200,18 @@ extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDevi return false; } +static void createMouse() +{ + if (mouse == nullptr) { + mouse = std::make_shared(touchMouse == nullptr ? 0 : 1); + GamepadDevice::Register(mouse); + } +} + extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_mouseEvent(JNIEnv *env, jobject obj, jint xpos, jint ypos, jint buttons) { + createMouse(); mouse->setAbsPos(xpos, ypos, settings.display.width, settings.display.height); mouse->setButton(Mouse::LEFT_BUTTON, (buttons & 1) != 0); mouse->setButton(Mouse::RIGHT_BUTTON, (buttons & 2) != 0); @@ -201,6 +219,17 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceMa } extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_mouseScrollEvent(JNIEnv *env, jobject obj, - jint scrollValue) { + jint scrollValue) +{ + createMouse(); mouse->setWheel(scrollValue); } + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_touchMouseEvent(JNIEnv *env, jobject obj, + jint xpos, jint ypos, jint buttons) +{ + touchMouse->setAbsPos(xpos, ypos, settings.display.width, settings.display.height); + touchMouse->setButton(Mouse::LEFT_BUTTON, (buttons & 1) != 0); + touchMouse->setButton(Mouse::RIGHT_BUTTON, (buttons & 2) != 0); + touchMouse->setButton(Mouse::MIDDLE_BUTTON, (buttons & 4) != 0); +} diff --git a/shell/apple/emulator-ios/emulator/EmulatorView.mm b/shell/apple/emulator-ios/emulator/EmulatorView.mm index d77e2eba0..42a34aff0 100644 --- a/shell/apple/emulator-ios/emulator/EmulatorView.mm +++ b/shell/apple/emulator-ios/emulator/EmulatorView.mm @@ -24,13 +24,13 @@ #include "ios_gamepad.h" @implementation EmulatorView { - std::shared_ptr mouse; + std::shared_ptr mouse; } - (void)didMoveToSuperview { [super didMoveToSuperview]; - mouse = std::make_shared(); + mouse = std::make_shared(); GamepadDevice::Register(mouse); } @@ -45,8 +45,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; { UITouch *touch = [touches anyObject]; [self touchLocation:touch]; - if (gui_is_open()) - mouse->setButton(Mouse::LEFT_BUTTON, true); + mouse->setButton(Mouse::LEFT_BUTTON, true); [super touchesBegan:touches withEvent:event]; } @@ -54,8 +53,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; { UITouch *touch = [touches anyObject]; [self touchLocation:touch]; - if (gui_is_open()) - mouse->setButton(Mouse::LEFT_BUTTON, false); + mouse->setButton(Mouse::LEFT_BUTTON, false); [super touchesEnded:touches withEvent:event]; } diff --git a/shell/apple/emulator-ios/emulator/PadViewController.mm b/shell/apple/emulator-ios/emulator/PadViewController.mm index 97dedb34f..fb649db40 100644 --- a/shell/apple/emulator-ios/emulator/PadViewController.mm +++ b/shell/apple/emulator-ios/emulator/PadViewController.mm @@ -106,6 +106,7 @@ static CGPoint translateCoords(const CGPoint& pos, const CGSize& size) - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; { + bool forwardEvent = true; [self startHideTimer]; for (UITouch *touch in touches) { @@ -117,6 +118,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; [self resetAnalog]; joyTouch = touch; joyBias = point; + forwardEvent = false; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; @@ -126,17 +128,21 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; touchToButton[key] = [NSNumber numberWithInt:control]; // button down virtualGamepad->buttonInput(control, true); + forwardEvent = false; } } - [super touchesBegan:touches withEvent:event]; + if (forwardEvent) + [super touchesBegan:touches withEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; { + bool forwardEvent = true; for (UITouch *touch in touches) { if (touch == joyTouch) { [self resetAnalog]; + forwardEvent = false; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; @@ -145,13 +151,16 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; [touchToButton removeObjectForKey:key]; // button up virtualGamepad->buttonInput(static_cast(control.intValue), false); + forwardEvent = false; } } - [super touchesEnded:touches withEvent:event]; + if (forwardEvent) + [super touchesEnded:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; { + bool forwardEvent = true; [self startHideTimer]; for (UITouch *touch in touches) { @@ -165,6 +174,7 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; point.x = std::max(std::min(1.0, point.x / sz), -1.0); point.y = std::max(std::min(1.0, point.y / sz), -1.0); virtualGamepad->joystickInput(point.x, point.y); + forwardEvent = false; continue; } vgamepad::ControlId control = vgamepad::hitTest(point.x, point.y); @@ -179,15 +189,19 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; // button down virtualGamepad->buttonInput(control, true); touchToButton[key] = [NSNumber numberWithInt:control]; + forwardEvent = false; } - [super touchesMoved:touches withEvent:event]; + if (forwardEvent) + [super touchesMoved:touches withEvent:event]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; { + bool forwardEvent = true; for (UITouch *touch in touches) { if (touch == joyTouch) { [self resetAnalog]; + forwardEvent = false; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; @@ -196,8 +210,10 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; [touchToButton removeObjectForKey:key]; // button up virtualGamepad->buttonInput(static_cast(control.intValue), false); + forwardEvent = false; } } - [super touchesCancelled:touches withEvent:event]; + if (forwardEvent) + [super touchesCancelled:touches withEvent:event]; } @end diff --git a/shell/apple/emulator-ios/emulator/ios_gamepad.h b/shell/apple/emulator-ios/emulator/ios_gamepad.h index e3aa0683a..420132e5b 100644 --- a/shell/apple/emulator-ios/emulator/ios_gamepad.h +++ b/shell/apple/emulator-ios/emulator/ios_gamepad.h @@ -506,14 +506,3 @@ class IOSVirtualGamepad : public VirtualGamepad return false; } }; - -class IOSTouchMouse : public SystemMouse -{ -public: - IOSTouchMouse() : SystemMouse("iOS") - { - _unique_id = "ios_mouse"; - _name = "Touchscreen (Mouse)"; - loadMapping(); - } -}; From 825778445a4a671044cda9def44b714adfd1a629 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 14 Dec 2024 16:54:18 +0100 Subject: [PATCH 61/81] ui: show boxart cover when starting a game --- core/ui/gui.cpp | 27 +++++++++++++++++++++------ core/ui/gui_util.h | 2 +- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 44bd8d994..f29ff70ba 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -3436,14 +3436,28 @@ static void gui_display_onboarding() select_file_popup(title, &systemdir_selected_callback); } +static void drawBoxartBackground() +{ + GameMedia game; + game.path = settings.content.path; + game.fileName = settings.content.fileName; + GameBoxart art = boxart.getBoxart(game); + ImguiFileTexture tex(art.boxartPath); + ImDrawList *dl = ImGui::GetBackgroundDrawList(); + tex.draw(dl, ImVec2(0, 0), ImVec2(settings.display.width, settings.display.height), 1.f); +} + static std::future networkStatus; static void gui_network_start() { + drawBoxartBackground(); centerNextWindow(); - ImGui::SetNextWindowSize(ScaledVec2(330, 180)); + ImGui::SetNextWindowSize(ScaledVec2(330, 0)); + ImGui::SetNextWindowBgAlpha(0.8f); + ImguiStyleVar _1(ImGuiStyleVar_WindowPadding, ScaledVec2(20, 20)); - ImGui::Begin("##network", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize); + ImGui::Begin("##network", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize); ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 10)); ImGui::AlignTextToFramePadding(); @@ -3471,7 +3485,6 @@ static void gui_network_start() float currentwidth = ImGui::GetContentRegionAvail().x; ImGui::SetCursorPosX((currentwidth - uiScaled(100.f)) / 2.f + ImGui::GetStyle().WindowPadding.x); - ImGui::SetCursorPosY(uiScaled(126.f)); if (ImGui::Button("Cancel", ScaledVec2(100.f, 0)) && NetworkHandshake::instance != nullptr) { NetworkHandshake::instance->stop(); @@ -3490,10 +3503,13 @@ static void gui_network_start() static void gui_display_loadscreen() { + drawBoxartBackground(); centerNextWindow(); - ImGui::SetNextWindowSize(ScaledVec2(330, 180)); + ImGui::SetNextWindowSize(ScaledVec2(330, 0)); + ImGui::SetNextWindowBgAlpha(0.8f); + ImguiStyleVar _(ImGuiStyleVar_WindowPadding, ScaledVec2(20, 20)); - if (ImGui::Begin("##loading", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + if (ImGui::Begin("##loading", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize)) { ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 10)); ImGui::AlignTextToFramePadding(); @@ -3531,7 +3547,6 @@ static void gui_display_loadscreen() float currentwidth = ImGui::GetContentRegionAvail().x; ImGui::SetCursorPosX((currentwidth - uiScaled(100.f)) / 2.f + ImGui::GetStyle().WindowPadding.x); - ImGui::SetCursorPosY(uiScaled(126.f)); if (ImGui::Button("Cancel", ScaledVec2(100.f, 0))) gameLoader.cancel(); } diff --git a/core/ui/gui_util.h b/core/ui/gui_util.h index baadb4466..02520f77f 100644 --- a/core/ui/gui_util.h +++ b/core/ui/gui_util.h @@ -201,7 +201,7 @@ class ImguiTexture public: void draw(const ImVec2& size, const ImVec4& tint_col = ImVec4(1, 1, 1, 1), const ImVec4& border_col = ImVec4(0, 0, 0, 0)); - void draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha = 1.f); + void draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha); void draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& color = ImVec4(1, 1, 1, 1)); bool button(const char* str_id, const ImVec2& image_size, const std::string& title = {}, const ImVec4& bg_col = ImVec4(0, 0, 0, 0), From d789b57412aa2c2b88681ee263df3639d6f71c77 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 14 Dec 2024 17:00:51 +0100 Subject: [PATCH 62/81] rend: get rid of globals for fog/palette updates & tex cache flush Add state vars to Renderer Use events to detect loading a state and terminating the game. Don't present last rendered frame after game terminated. --- core/emulator.cpp | 2 +- core/hw/pvr/Renderer_if.h | 39 ++++++++++++++++++++++++++- core/hw/pvr/pvr.cpp | 4 +-- core/hw/pvr/pvr_regs.cpp | 3 +-- core/nullDC.cpp | 1 - core/rend/TexCache.cpp | 4 +-- core/rend/TexCache.h | 4 --- core/rend/dx11/dx11_renderer.cpp | 16 ++++++----- core/rend/dx11/dx11_renderer.h | 1 + core/rend/dx9/d3d_renderer.cpp | 20 +++++++------- core/rend/dx9/d3d_renderer.h | 1 + core/rend/gl4/gles.cpp | 2 +- core/rend/gles/gles.cpp | 16 +++++------ core/rend/gles/gles.h | 3 +++ core/rend/vulkan/oit/oit_renderer.cpp | 1 + core/rend/vulkan/vulkan_driver.h | 3 ++- core/rend/vulkan/vulkan_renderer.cpp | 20 +++++++------- core/rend/vulkan/vulkan_renderer.h | 3 +++ 18 files changed, 93 insertions(+), 50 deletions(-) diff --git a/core/emulator.cpp b/core/emulator.cpp index c6d765528..0e716e770 100644 --- a/core/emulator.cpp +++ b/core/emulator.cpp @@ -880,7 +880,7 @@ void Emulator::loadstate(Deserializer& deser) mmu_set_state(); getSh4Executor()->ResetCache(); - KillTex = true; + EventManager::event(Event::LoadState); } void Emulator::setNetworkState(bool online) diff --git a/core/hw/pvr/Renderer_if.h b/core/hw/pvr/Renderer_if.h index 369498ce3..c9a5e29af 100644 --- a/core/hw/pvr/Renderer_if.h +++ b/core/hw/pvr/Renderer_if.h @@ -1,6 +1,7 @@ #pragma once #include "types.h" #include "ta_ctx.h" +#include "emulator.h" #include extern u32 FrameCount; @@ -22,6 +23,8 @@ void rend_enable_renderer(bool enabled); bool rend_is_enabled(); void rend_serialize(Serializer& ser); void rend_deserialize(Deserializer& deser); +static void rend_updatePalette(); +static void rend_updateFogTable(); /////// extern TA_context* _pvrrc; @@ -54,7 +57,14 @@ struct FramebufferInfo struct Renderer { - virtual ~Renderer() = default; + Renderer() { + EventManager::listen(Event::Terminate, onEvent, this); + EventManager::listen(Event::LoadState, onEvent, this); + } + virtual ~Renderer() { + EventManager::unlisten(Event::Terminate, onEvent, this); + EventManager::unlisten(Event::LoadState, onEvent, this); + } virtual bool Init() = 0; virtual void Term() = 0; @@ -72,6 +82,25 @@ struct Renderer virtual bool Present() { return true; } virtual BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) { return nullptr; } + +protected: + bool resetTextureCache = false; + bool clearLastFrame = false; + bool updatePalette = true; + bool updateFogTable = true; + +private: + static void onEvent(Event event, void *arg) + { + Renderer *renderer = static_cast(arg); + renderer->resetTextureCache = true; + renderer->updatePalette = true; + renderer->updateFogTable = true; + if (event == Event::Terminate) + renderer->clearLastFrame = true; + } + friend void rend_updatePalette(); + friend void rend_updateFogTable(); }; extern Renderer* renderer; @@ -81,3 +110,11 @@ extern u32 fb_watch_addr_end; extern bool fb_dirty; void check_framebuffer_write(); +static inline void rend_updatePalette() { + if (renderer != nullptr) + renderer->updatePalette = true; +} +static inline void rend_updateFogTable() { + if (renderer != nullptr) + renderer->updateFogTable = true; +} diff --git a/core/hw/pvr/pvr.cpp b/core/hw/pvr/pvr.cpp index 22bead33e..92e325ef5 100644 --- a/core/hw/pvr/pvr.cpp +++ b/core/hw/pvr/pvr.cpp @@ -31,7 +31,6 @@ extern u8 ta_fsm[2049]; //[2048] stores the current state extern u32 ta_fsm_cl; extern u32 taRenderPass; // pvr_regs.cpp -extern bool fog_needs_update; extern bool pal_needs_update; namespace pvr @@ -39,7 +38,6 @@ namespace pvr void reset(bool hard) { - KillTex = true; Regs_Reset(hard); spg_Reset(hard); if (hard) @@ -92,7 +90,7 @@ void deserialize(Deserializer& deser) YUV_deserialize(deser); deser >> pvr_regs; - fog_needs_update = true; + rend_updateFogTable(); spg_Deserialize(deser); diff --git a/core/hw/pvr/pvr_regs.cpp b/core/hw/pvr/pvr_regs.cpp index 235c728fb..67eb5e714 100644 --- a/core/hw/pvr/pvr_regs.cpp +++ b/core/hw/pvr/pvr_regs.cpp @@ -6,7 +6,6 @@ #include bool pal_needs_update=true; -bool fog_needs_update=true; u8 pvr_regs[pvr_RegSize]; @@ -221,7 +220,7 @@ void pvr_WriteReg(u32 paddr,u32 data) if (addr >= PALETTE_RAM_START_addr && PvrReg(addr,u32) != data) pal_needs_update = true; else if (addr >= FOG_TABLE_START_addr && addr <= FOG_TABLE_END_addr && PvrReg(addr,u32) != data) - fog_needs_update = true; + rend_updateFogTable(); break; } PvrReg(addr, u32) = data; diff --git a/core/nullDC.cpp b/core/nullDC.cpp index 893df5c3f..8e60388a3 100644 --- a/core/nullDC.cpp +++ b/core/nullDC.cpp @@ -281,7 +281,6 @@ void dc_loadstate(int index) } free(data); - EventManager::event(Event::LoadState); } time_t dc_getStateCreationDate(int index) diff --git a/core/rend/TexCache.cpp b/core/rend/TexCache.cpp index 63fb44bf0..35191ea1f 100644 --- a/core/rend/TexCache.cpp +++ b/core/rend/TexCache.cpp @@ -14,12 +14,10 @@ const u8 *vq_codebook; u32 palette_index; -bool KillTex=false; u32 palette16_ram[1024]; u32 palette32_ram[1024]; u32 pal_hash_256[4]; u32 pal_hash_16[64]; -bool palette_updated; extern bool pal_needs_update; // Rough approximation of LoD bias from D adjust param, only used to increase LoD @@ -87,7 +85,7 @@ void palette_update() if (!pal_needs_update) return; pal_needs_update = false; - palette_updated = true; + rend_updatePalette(); if (!isDirectX(config::RendererType)) { diff --git a/core/rend/TexCache.h b/core/rend/TexCache.h index 827d71015..9fedcc679 100644 --- a/core/rend/TexCache.h +++ b/core/rend/TexCache.h @@ -16,11 +16,8 @@ constexpr int VQ_CODEBOOK_SIZE = 256 * 8; extern u32 palette_index; extern u32 palette16_ram[1024]; extern u32 palette32_ram[1024]; -extern bool fog_needs_update; extern u32 pal_hash_256[4]; extern u32 pal_hash_16[64]; -extern bool KillTex; -extern bool palette_updated; extern u32 detwiddle[2][11][1024]; @@ -818,7 +815,6 @@ class BaseTextureCache texture.Delete(); cache.clear(); - KillTex = false; INFO_LOG(RENDERER, "Texture cache cleared"); } diff --git a/core/rend/dx11/dx11_renderer.cpp b/core/rend/dx11/dx11_renderer.cpp index c4246e388..199c586f1 100644 --- a/core/rend/dx11/dx11_renderer.cpp +++ b/core/rend/dx11/dx11_renderer.cpp @@ -158,7 +158,7 @@ bool DX11Renderer::Init() quad->init(device, deviceContext, shaders); n2Helper.init(device, deviceContext); - fog_needs_update = true; + updateFogTable = true; if (!success) { @@ -318,8 +318,10 @@ BaseTextureCacheData *DX11Renderer::GetTexture(TSP tsp, TCW tcw) void DX11Renderer::Process(TA_context* ctx) { - if (KillTex) + if (resetTextureCache) { texCache.Clear(); + resetTextureCache = false; + } texCache.Cleanup(); ta_parse(ctx, true); @@ -936,7 +938,7 @@ void DX11Renderer::drawStrips() bool DX11Renderer::RenderLastFrame() { - if (!frameRenderedOnce) + if (!frameRenderedOnce || clearLastFrame) return false; displayFramebuffer(); return true; @@ -1222,9 +1224,9 @@ void DX11Renderer::readRttRenderTarget(u32 texAddress) void DX11Renderer::updatePaletteTexture() { - if (palette_updated) + if (updatePalette) { - palette_updated = false; + updatePalette = false; deviceContext->UpdateSubresource(paletteTexture, 0, nullptr, palette32_ram, 32 * sizeof(u32), 32 * sizeof(u32) * 32); } deviceContext->PSSetShaderResources(1, 1, &paletteTextureView.get()); @@ -1235,9 +1237,9 @@ void DX11Renderer::updateFogTexture() { if (!config::Fog) return; - if (fog_needs_update) + if (updateFogTable) { - fog_needs_update = false; + updateFogTable = false; u8 temp_tex_buffer[256]; MakeFogTexture(temp_tex_buffer); diff --git a/core/rend/dx11/dx11_renderer.h b/core/rend/dx11/dx11_renderer.h index c14678a87..c6d0ca63e 100644 --- a/core/rend/dx11/dx11_renderer.h +++ b/core/rend/dx11/dx11_renderer.h @@ -45,6 +45,7 @@ struct DX11Renderer : public Renderer if (!frameRendered) return false; frameRendered = false; + clearLastFrame = false; #ifndef LIBRETRO imguiDriver->setFrameRendered(); #else diff --git a/core/rend/dx9/d3d_renderer.cpp b/core/rend/dx9/d3d_renderer.cpp index e3530ab2e..ec77cb1c5 100644 --- a/core/rend/dx9/d3d_renderer.cpp +++ b/core/rend/dx9/d3d_renderer.cpp @@ -139,7 +139,7 @@ bool D3DRenderer::Init() success &= (bool)shaders.getVertexShader(true); success &= SUCCEEDED(device->CreateTexture(32, 32, 1, D3DUSAGE_DYNAMIC, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &paletteTexture.get(), 0)); success &= SUCCEEDED(device->CreateTexture(128, 2, 1, D3DUSAGE_DYNAMIC, D3DFMT_A8, D3DPOOL_DEFAULT, &fogTexture.get(), 0)); - fog_needs_update = true; + updateFogTable = true; if (!success) { @@ -199,8 +199,8 @@ void D3DRenderer::postReset() verify(rc); rc = SUCCEEDED(device->CreateTexture(128, 2, 1, D3DUSAGE_DYNAMIC, D3DFMT_A8, D3DPOOL_DEFAULT, &fogTexture.get(), 0)); verify(rc); - fog_needs_update = true; - palette_updated = true; + updateFogTable = true; + updatePalette = true; } void D3DRenderer::Term() @@ -315,8 +315,10 @@ void D3DRenderer::Process(TA_context* ctx) if (settings.platform.isNaomi2()) throw FlycastException("DirectX 9 doesn't support Naomi 2 games. Select a different graphics API"); - if (KillTex) + if (resetTextureCache) { texCache.Clear(); + resetTextureCache = false; + } texCache.Cleanup(); ta_parse(ctx, false); @@ -1279,7 +1281,7 @@ void D3DRenderer::displayFramebuffer() bool D3DRenderer::RenderLastFrame() { - if (!frameRenderedOnce || !theDXContext.isReady()) + if (clearLastFrame || !frameRenderedOnce || !theDXContext.isReady()) return false; backbuffer.reset(); bool rc = SUCCEEDED(device->GetRenderTarget(0, &backbuffer.get())); @@ -1292,9 +1294,9 @@ bool D3DRenderer::RenderLastFrame() void D3DRenderer::updatePaletteTexture() { - if (!palette_updated) + if (!updatePalette) return; - palette_updated = false; + updatePalette = false; D3DLOCKED_RECT rect; bool rc = SUCCEEDED(paletteTexture->LockRect(0, &rect, nullptr, 0)); @@ -1316,9 +1318,9 @@ void D3DRenderer::updatePaletteTexture() void D3DRenderer::updateFogTexture() { - if (!fog_needs_update || !config::Fog) + if (!updateFogTable || !config::Fog) return; - fog_needs_update = false; + updateFogTable = false; u8 temp_tex_buffer[256]; MakeFogTexture(temp_tex_buffer); diff --git a/core/rend/dx9/d3d_renderer.h b/core/rend/dx9/d3d_renderer.h index a7e5d71b2..e6392131b 100644 --- a/core/rend/dx9/d3d_renderer.h +++ b/core/rend/dx9/d3d_renderer.h @@ -110,6 +110,7 @@ struct D3DRenderer : public Renderer return false; imguiDriver->setFrameRendered(); frameRendered = false; + clearLastFrame = false; return true; } BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override; diff --git a/core/rend/gl4/gles.cpp b/core/rend/gl4/gles.cpp index e28e3e6a0..5cd12b6aa 100644 --- a/core/rend/gl4/gles.cpp +++ b/core/rend/gl4/gles.cpp @@ -763,7 +763,7 @@ bool OpenGL4Renderer::Init() u32 dst[16]; UpscalexBRZ(2, src, dst, 2, 2, false); } - fog_needs_update = true; + updateFogTable = true; TextureCacheData::SetDirectXColorOrder(false); TextureCacheData::setUploadToGPUFlavor(); diff --git a/core/rend/gles/gles.cpp b/core/rend/gles/gles.cpp index eabcdcca1..ad14b9009 100644 --- a/core/rend/gles/gles.cpp +++ b/core/rend/gles/gles.cpp @@ -982,7 +982,7 @@ bool OpenGLRenderer::Init() u32 dst[16]; UpscalexBRZ(2, src, dst, 2, 2, false); } - fog_needs_update = true; + updateFogTable = true; TextureCacheData::SetDirectXColorOrder(false); TextureCacheData::setUploadToGPUFlavor(); @@ -1049,19 +1049,19 @@ void OpenGLRenderer::Process(TA_context* ctx) if (gl.gl_major < 3 && settings.platform.isNaomi2()) throw FlycastException("OpenGL ES 3.0+ required for Naomi 2"); - if (KillTex) + if (resetTextureCache) { TexCache.Clear(); + resetTextureCache = false; + } TexCache.Cleanup(); - if (fog_needs_update && config::Fog) - { - fog_needs_update = false; + if (updateFogTable && config::Fog) { + updateFogTable = false; updateFogTexture((u8 *)FOG_TABLE, getFogTextureSlot(), gl.single_channel_format); } - if (palette_updated) - { + if (updatePalette) { updatePaletteTexture(getPaletteTextureSlot()); - palette_updated = false; + updatePalette = false; } ta_parse(ctx, gl.prim_restart_fixed_supported || gl.prim_restart_supported); } diff --git a/core/rend/gles/gles.h b/core/rend/gles/gles.h index 3a887feea..d5bb74252 100755 --- a/core/rend/gles/gles.h +++ b/core/rend/gles/gles.h @@ -502,6 +502,8 @@ struct OpenGLRenderer : Renderer bool RenderLastFrame() override { + if (clearLastFrame) + return false; saveCurrentFramebuffer(); bool ret = renderLastFrame(); restoreCurrentFramebuffer(); @@ -519,6 +521,7 @@ struct OpenGLRenderer : Renderer #ifndef LIBRETRO imguiDriver->setFrameRendered(); #endif + clearLastFrame = false; frameRendered = false; return true; } diff --git a/core/rend/vulkan/oit/oit_renderer.cpp b/core/rend/vulkan/oit/oit_renderer.cpp index e5e0bc7e0..8422f47d4 100644 --- a/core/rend/vulkan/oit/oit_renderer.cpp +++ b/core/rend/vulkan/oit/oit_renderer.cpp @@ -105,6 +105,7 @@ class OITVulkanRenderer final : public BaseVulkanRenderer bool Present() override { + clearLastFrame = false; if (config::EmulateFramebuffer || framebufferRendered) return presentFramebuffer(); else diff --git a/core/rend/vulkan/vulkan_driver.h b/core/rend/vulkan/vulkan_driver.h index e48359289..3c00d0aa9 100644 --- a/core/rend/vulkan/vulkan_driver.h +++ b/core/rend/vulkan/vulkan_driver.h @@ -56,7 +56,8 @@ class VulkanDriver final : public ImGuiDriver if (!rendering || newFrameStarted) { context->BeginRenderPass(); - context->PresentLastFrame(); + if (renderer->RenderLastFrame()) + context->PresentLastFrame(); } if (!justStarted) { diff --git a/core/rend/vulkan/vulkan_renderer.cpp b/core/rend/vulkan/vulkan_renderer.cpp index 5935f9d44..108073901 100644 --- a/core/rend/vulkan/vulkan_renderer.cpp +++ b/core/rend/vulkan/vulkan_renderer.cpp @@ -87,8 +87,10 @@ BaseTextureCacheData *BaseVulkanRenderer::GetTexture(TSP tsp, TCW tcw) void BaseVulkanRenderer::Process(TA_context* ctx) { framebufferRendered = false; - if (KillTex) + if (resetTextureCache) { textureCache.Clear(); + resetTextureCache = false; + } texCommandPool.BeginFrame(); textureCache.SetCurrentIndex(texCommandPool.GetIndex()); @@ -184,11 +186,11 @@ void BaseVulkanRenderer::CheckFogTexture() { fogTexture = std::make_unique(); fogTexture->tex_type = TextureType::_8; - fog_needs_update = true; + updateFogTable = true; } - if (!fog_needs_update || !config::Fog) + if (!updateFogTable || !config::Fog) return; - fog_needs_update = false; + updateFogTable = false; u8 texData[256]; MakeFogTexture(texData); @@ -199,15 +201,14 @@ void BaseVulkanRenderer::CheckFogTexture() void BaseVulkanRenderer::CheckPaletteTexture() { - if (!paletteTexture) - { + if (!paletteTexture) { paletteTexture = std::make_unique(); paletteTexture->tex_type = TextureType::_8888; - palette_updated = true; } - if (!palette_updated) + else if (!updatePalette) { return; - palette_updated = false; + } + updatePalette = false; paletteTexture->SetCommandBuffer(texCommandBuffer); paletteTexture->UploadToGPU(1024, 1, (u8 *)palette32_ram, false); @@ -299,6 +300,7 @@ class VulkanRenderer final : public BaseVulkanRenderer bool Present() override { + clearLastFrame = false; if (config::EmulateFramebuffer || framebufferRendered) return presentFramebuffer(); else diff --git a/core/rend/vulkan/vulkan_renderer.h b/core/rend/vulkan/vulkan_renderer.h index 0780d5799..f24ff3c00 100644 --- a/core/rend/vulkan/vulkan_renderer.h +++ b/core/rend/vulkan/vulkan_renderer.h @@ -41,6 +41,9 @@ class BaseVulkanRenderer : public Renderer void RenderFramebuffer(const FramebufferInfo& info) override; void RenderVideoRouting(); + bool RenderLastFrame() override { + return !clearLastFrame; + } bool GetLastFrame(std::vector& data, int& width, int& height) override { return GetContext()->GetLastFrame(data, width, height); } From e5b1d56a7cdc34dfbcdc904d2c490a28447f20a5 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 14 Dec 2024 19:07:18 +0100 Subject: [PATCH 63/81] rend: split texture conversion routines into their own file --- CMakeLists.txt | 2 + core/hw/pvr/Renderer_if.cpp | 2 +- core/hw/pvr/pvr.cpp | 1 - core/rend/CustomTexture.cpp | 2 +- core/rend/CustomTexture.h | 4 +- core/rend/TexCache.cpp | 200 +-------- core/rend/TexCache.h | 588 +------------------------- core/rend/gles/gles.cpp | 1 - core/rend/gles/gltex.cpp | 1 - core/rend/texconv.cpp | 539 +++++++++++++++++++++++ core/rend/texconv.h | 257 +++++++++++ core/rend/transform_matrix.h | 2 +- core/rend/vulkan/oit/oit_renderpass.h | 1 + core/rend/vulkan/vk_context_lr.h | 2 +- core/rend/vulkan/vulkan_context.h | 2 +- core/ui/boxart/pvrparser.h | 3 +- 16 files changed, 843 insertions(+), 764 deletions(-) create mode 100644 core/rend/texconv.cpp create mode 100644 core/rend/texconv.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0edf3531c..b9f267fa0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1348,6 +1348,8 @@ target_sources(${PROJECT_NAME} PRIVATE core/rend/tileclip.h core/rend/TexCache.cpp core/rend/TexCache.h + core/rend/texconv.cpp + core/rend/texconv.h core/rend/norend/norend.cpp) if(NOT LIBRETRO) target_sources(${PROJECT_NAME} PRIVATE diff --git a/core/hw/pvr/Renderer_if.cpp b/core/hw/pvr/Renderer_if.cpp index 4e987f8e5..7f669f3fd 100644 --- a/core/hw/pvr/Renderer_if.cpp +++ b/core/hw/pvr/Renderer_if.cpp @@ -1,6 +1,6 @@ #include "Renderer_if.h" #include "spg.h" -#include "rend/TexCache.h" +#include "rend/texconv.h" #include "rend/transform_matrix.h" #include "cfg/option.h" #include "emulator.h" diff --git a/core/hw/pvr/pvr.cpp b/core/hw/pvr/pvr.cpp index 92e325ef5..285496d45 100644 --- a/core/hw/pvr/pvr.cpp +++ b/core/hw/pvr/pvr.cpp @@ -21,7 +21,6 @@ #include "pvr_regs.h" #include "Renderer_if.h" #include "ta_ctx.h" -#include "rend/TexCache.h" #include "serialize.h" #include "pvr_mem.h" #include "elan.h" diff --git a/core/rend/CustomTexture.cpp b/core/rend/CustomTexture.cpp index 93078201f..1ff8444b2 100644 --- a/core/rend/CustomTexture.cpp +++ b/core/rend/CustomTexture.cpp @@ -16,8 +16,8 @@ You should have received a copy of the GNU General Public License along with reicast. If not, see . */ -#include "TexCache.h" #include "CustomTexture.h" +#include "TexCache.h" #include "oslib/directory.h" #include "oslib/storage.h" #include "cfg/option.h" diff --git a/core/rend/CustomTexture.h b/core/rend/CustomTexture.h index 29223ee8e..3994b9925 100644 --- a/core/rend/CustomTexture.h +++ b/core/rend/CustomTexture.h @@ -18,7 +18,7 @@ */ #pragma once -#include "TexCache.h" +#include "texconv.h" #include "stdclass.h" #include @@ -26,6 +26,8 @@ #include #include +class BaseTextureCacheData; + class CustomTexture { public: CustomTexture() : loader_thread(loader_thread_func, this, "CustomTexLoader") {} diff --git a/core/rend/TexCache.cpp b/core/rend/TexCache.cpp index 35191ea1f..fe73022ed 100644 --- a/core/rend/TexCache.cpp +++ b/core/rend/TexCache.cpp @@ -1,10 +1,24 @@ +/* + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ #include "TexCache.h" -#include "CustomTexture.h" #include "deps/xbrz/xbrz.h" #include "hw/pvr/pvr_mem.h" #include "hw/mem/addrspace.h" -#include #include #include @@ -12,12 +26,6 @@ #include #endif -const u8 *vq_codebook; -u32 palette_index; -u32 palette16_ram[1024]; -u32 palette32_ram[1024]; -u32 pal_hash_256[4]; -u32 pal_hash_16[64]; extern bool pal_needs_update; // Rough approximation of LoD bias from D adjust param, only used to increase LoD @@ -25,143 +33,6 @@ const std::array D_Adjust_LoD_Bias = { 0.f, -4.f, -2.f, -1.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f }; -u32 detwiddle[2][11][1024]; -//input : address in the yyyyyxxxxx format -//output : address in the xyxyxyxy format -//U : x resolution , V : y resolution -//twiddle works on 64b words - - -static u32 twiddle_slow(u32 x,u32 y,u32 x_sz,u32 y_sz) -{ - u32 rv=0;//low 2 bits are directly passed -> needs some misc stuff to work.However - //Pvr internally maps the 64b banks "as if" they were twiddled :p - - u32 sh=0; - x_sz>>=1; - y_sz>>=1; - while(x_sz!=0 || y_sz!=0) - { - if (y_sz) - { - u32 temp=y&1; - rv|=temp<>=1; - y>>=1; - sh++; - } - if (x_sz) - { - u32 temp=x&1; - rv|=temp<>=1; - x>>=1; - sh++; - } - } - return rv; -} - -static void BuildTwiddleTables() -{ - for (u32 s = 0; s < 11; s++) - { - u32 x_sz = 1024; - u32 y_sz = 1 << s; - for (u32 i = 0; i < x_sz; i++) - { - detwiddle[0][s][i] = twiddle_slow(i, 0, x_sz, y_sz); - detwiddle[1][s][i] = twiddle_slow(0, i, y_sz, x_sz); - } - } -} - -static OnLoad btt(&BuildTwiddleTables); - -void palette_update() -{ - if (!pal_needs_update) - return; - pal_needs_update = false; - rend_updatePalette(); - - if (!isDirectX(config::RendererType)) - { - switch(PAL_RAM_CTRL&3) - { - case 0: - for (int i=0;i<1024;i++) - { - palette16_ram[i] = Unpacker1555::unpack(PALETTE_RAM[i]); - palette32_ram[i] = Unpacker1555_32::unpack(PALETTE_RAM[i]); - } - break; - - case 1: - for (int i=0;i<1024;i++) - { - palette16_ram[i] = UnpackerNop::unpack(PALETTE_RAM[i]); - palette32_ram[i] = Unpacker565_32::unpack(PALETTE_RAM[i]); - } - break; - - case 2: - for (int i=0;i<1024;i++) - { - palette16_ram[i] = Unpacker4444::unpack(PALETTE_RAM[i]); - palette32_ram[i] = Unpacker4444_32::unpack(PALETTE_RAM[i]); - } - break; - - case 3: - for (int i=0;i<1024;i++) - palette32_ram[i] = Unpacker8888::unpack(PALETTE_RAM[i]); - break; - } - } - else - { - switch(PAL_RAM_CTRL&3) - { - - case 0: - for (int i=0;i<1024;i++) - { - palette16_ram[i] = UnpackerNop::unpack(PALETTE_RAM[i]); - palette32_ram[i] = Unpacker1555_32::unpack(PALETTE_RAM[i]); - } - break; - - case 1: - for (int i=0;i<1024;i++) - { - palette16_ram[i] = UnpackerNop::unpack(PALETTE_RAM[i]); - palette32_ram[i] = Unpacker565_32::unpack(PALETTE_RAM[i]); - } - break; - - case 2: - for (int i=0;i<1024;i++) - { - palette16_ram[i] = UnpackerNop::unpack(PALETTE_RAM[i]); - palette32_ram[i] = Unpacker4444_32::unpack(PALETTE_RAM[i]); - } - break; - - case 3: - for (int i=0;i<1024;i++) - palette32_ram[i] = UnpackerNop::unpack(PALETTE_RAM[i]); - break; - } - } - for (std::size_t i = 0; i < std::size(pal_hash_16); i++) - pal_hash_16[i] = XXH32(&PALETTE_RAM[i << 4], 16 * 4, 7); - for (std::size_t i = 0; i < std::size(pal_hash_256); i++) - pal_hash_256[i] = XXH32(&PALETTE_RAM[i << 8], 256 * 4, 7); -} - static std::vector VramLocks[VRAM_SIZE_MAX / PAGE_SIZE]; //List functions @@ -290,45 +161,6 @@ void UpscalexBRZ(int factor, u32* source, u32* dest, int width, int height, bool #endif } -struct PvrTexInfo -{ - const char* name; - int bpp; //4/8 for pal. 16 for yuv, rgb, argb - TextureType type; - // Conversion to 16 bpp - TexConvFP TW; - TexConvFP VQ; - // Conversion to 32 bpp - TexConvFP32 PL32; - TexConvFP32 TW32; - TexConvFP32 VQ32; - TexConvFP32 PLVQ32; - // Conversion to 8 bpp (palette) - TexConvFP8 TW8; -}; - -#define TEX_CONV_TABLE \ -const PvrTexInfo pvrTexInfo[8] = \ -{ /* name bpp Final format Twiddled VQ Planar(32b) Twiddled(32b) VQ (32b) PL VQ (32b) Palette (8b) */ \ - {"1555", 16, TextureType::_5551, tex1555_TW, tex1555_VQ, tex1555_PL32, tex1555_TW32, tex1555_VQ32, tex1555_PLVQ32, nullptr }, \ - {"565", 16, TextureType::_565, tex565_TW, tex565_VQ, tex565_PL32, tex565_TW32, tex565_VQ32, tex565_PLVQ32, nullptr }, \ - {"4444", 16, TextureType::_4444, tex4444_TW, tex4444_VQ, tex4444_PL32, tex4444_TW32, tex4444_VQ32, tex4444_PLVQ32, nullptr }, \ - {"yuv", 16, TextureType::_8888, nullptr, nullptr, texYUV422_PL, texYUV422_TW, texYUV422_VQ, texYUV422_PLVQ, nullptr }, \ - {"bumpmap", 16, TextureType::_4444, texBMP_TW, texBMP_VQ, tex4444_PL32, tex4444_TW32, tex4444_VQ32, tex4444_PLVQ32, nullptr }, \ - {"pal4", 4, TextureType::_5551, texPAL4_TW, texPAL4_VQ, nullptr, texPAL4_TW32, texPAL4_VQ32, nullptr, texPAL4PT_TW }, \ - {"pal8", 8, TextureType::_5551, texPAL8_TW, texPAL8_VQ, nullptr, texPAL8_TW32, texPAL8_VQ32, nullptr, texPAL8PT_TW }, \ - {"ns/1555", 0}, \ -} - -namespace opengl { - TEX_CONV_TABLE; -} -namespace directx { - TEX_CONV_TABLE; -} -#undef TEX_CONV_TABLE -static const PvrTexInfo *pvrTexInfo = opengl::pvrTexInfo; - extern const u32 VQMipPoint[11] = { VQ_CODEBOOK_SIZE + 0x00000, // 1 diff --git a/core/rend/TexCache.h b/core/rend/TexCache.h index 9fedcc679..1ce4ba250 100644 --- a/core/rend/TexCache.h +++ b/core/rend/TexCache.h @@ -1,7 +1,25 @@ +/* + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ #pragma once #include "oslib/oslib.h" #include "hw/pvr/Renderer_if.h" #include "cfg/option.h" +#include "texconv.h" +#include "CustomTexture.h" #include #include @@ -11,570 +29,6 @@ #include #include -extern const u8 *vq_codebook; -constexpr int VQ_CODEBOOK_SIZE = 256 * 8; -extern u32 palette_index; -extern u32 palette16_ram[1024]; -extern u32 palette32_ram[1024]; -extern u32 pal_hash_256[4]; -extern u32 pal_hash_16[64]; - -extern u32 detwiddle[2][11][1024]; - -void palette_update(); - -template -class PixelBuffer -{ - pixel_type* p_buffer_start = nullptr; - pixel_type* p_current_mipmap = nullptr; - pixel_type* p_current_line = nullptr; - pixel_type* p_current_pixel = nullptr; - - u32 pixels_per_line = 0; - -public: - ~PixelBuffer() - { - deinit(); - } - - void init(u32 width, u32 height, bool mipmapped) - { - deinit(); - size_t size = width * height * sizeof(pixel_type); - if (mipmapped) - { - do - { - width /= 2; - height /= 2; - size += width * height * sizeof(pixel_type); - } - while (width != 0 && height != 0); - } - p_buffer_start = p_current_line = p_current_pixel = p_current_mipmap = (pixel_type *)malloc(size); - this->pixels_per_line = 1; - } - - void init(u32 width, u32 height) - { - deinit(); - p_buffer_start = p_current_line = p_current_pixel = p_current_mipmap = (pixel_type *)malloc(width * height * sizeof(pixel_type)); - this->pixels_per_line = width; - } - - void deinit() - { - if (p_buffer_start != NULL) - { - free(p_buffer_start); - p_buffer_start = p_current_mipmap = p_current_line = p_current_pixel = NULL; - } - } - - void steal_data(PixelBuffer &buffer) - { - deinit(); - p_buffer_start = p_current_mipmap = p_current_line = p_current_pixel = buffer.p_buffer_start; - pixels_per_line = buffer.pixels_per_line; - buffer.p_buffer_start = buffer.p_current_mipmap = buffer.p_current_line = buffer.p_current_pixel = NULL; - } - - void set_mipmap(int level) - { - u32 offset = 0; - for (int i = 0; i < level; i++) - offset += (1 << (2 * i)); - p_current_mipmap = p_current_line = p_current_pixel = p_buffer_start + offset; - pixels_per_line = 1 << level; - } - - pixel_type *data(u32 x = 0, u32 y = 0) - { - return p_current_mipmap + pixels_per_line * y + x; - } - - void prel(u32 x, pixel_type value) - { - p_current_pixel[x] = value; - } - - void prel(u32 x, u32 y, pixel_type value) - { - p_current_pixel[y * pixels_per_line + x] = value; - } - - void rmovex(u32 value) - { - p_current_pixel += value; - } - - void rmovey(u32 value) - { - p_current_line += pixels_per_line * value; - p_current_pixel = p_current_line; - } - - void amove(u32 x_m, u32 y_m) - { - //p_current_pixel=p_buffer_start; - p_current_line = p_current_mipmap + pixels_per_line * y_m; - p_current_pixel = p_current_line + x_m; - } -}; - -// OpenGL -struct RGBAPacker { - static u32 pack(u8 r, u8 g, u8 b, u8 a) { - return r | (g << 8) | (b << 16) | (a << 24); - } -}; -// DirectX -struct BGRAPacker { - static u32 pack(u8 r, u8 g, u8 b, u8 a) { - return b | (g << 8) | (r << 16) | (a << 24); - } -}; - -template -inline static u32 YUV422(s32 Y, s32 Yu, s32 Yv) -{ - Yu -= 128; - Yv -= 128; - - s32 R = Y + Yv * 11 / 8; // Y + (Yv-128) * (11/8) ? - s32 G = Y - (Yu * 11 + Yv * 22) / 32; // Y - (Yu-128) * (11/8) * 0.25 - (Yv-128) * (11/8) * 0.5 ? - s32 B = Y + Yu * 110 / 64; // Y + (Yu-128) * (11/8) * 1.25 ? - - return Packer::pack(std::clamp(R, 0, 255), std::clamp(G, 0, 255), std::clamp(B, 0, 255), 0xFF); -} - -#define twop(x,y,bcx,bcy) (detwiddle[0][bcy][x]+detwiddle[1][bcx][y]) - -template -struct UnpackerNop { - using unpacked_type = Pixel; - static Pixel unpack(Pixel word) { - return word; - } -}; - -// ARGB1555 to RGBA5551 -struct Unpacker1555 { - using unpacked_type = u16; - static u16 unpack(u16 word) { - return ((word >> 15) & 1) | (((word >> 10) & 0x1F) << 11) | (((word >> 5) & 0x1F) << 6) | (((word >> 0) & 0x1F) << 1); - } -}; - -// ARGB4444 to RGBA4444 -struct Unpacker4444 { - using unpacked_type = u16; - static u16 unpack(u16 word) { - return (((word >> 0) & 0xF) << 4) | (((word >> 4) & 0xF) << 8) | (((word >> 8) & 0xF) << 12) | (((word >> 12) & 0xF) << 0); - } -}; - -template -struct Unpacker1555_32 { - using unpacked_type = u32; - static u32 unpack(u16 word) { - return Packer::pack( - (((word >> 10) & 0x1F) << 3) | ((word >> 12) & 7), - (((word >> 5) & 0x1F) << 3) | ((word >> 7) & 7), - (((word >> 0) & 0x1F) << 3) | ((word >> 2) & 7), - (word & 0x8000) ? 0xFF : 0); - } -}; - -template -struct Unpacker565_32 { - using unpacked_type = u32; - static u32 unpack(u16 word) { - return Packer::pack( - (((word >> 11) & 0x1F) << 3) | ((word >> 13) & 7), - (((word >> 5) & 0x3F) << 2) | ((word >> 9) & 3), - (((word >> 0) & 0x1F) << 3) | ((word >> 2) & 7), - 0xFF); - } -}; - -template -struct Unpacker4444_32 { - using unpacked_type = u32; - static u32 unpack(u16 word) { - return Packer::pack( - (((word >> 8) & 0xF) << 4) | ((word >> 8) & 0xF), - (((word >> 4) & 0xF) << 4) | ((word >> 4) & 0xF), - (((word >> 0) & 0xF) << 4) | ((word >> 0) & 0xF), - (((word >> 12) & 0xF) << 4) | ((word >> 12) & 0xF)); - } -}; - -// ARGB8888 to whatever -template -struct Unpacker8888 { - using unpacked_type = u32; - static u32 unpack(u32 word) { - return Packer::pack( - (word >> 16) & 0xFF, - (word >> 8) & 0xFF, - (word >> 0) & 0xFF, - (word >> 24) & 0xFF); - } -}; - -template -struct ConvertPlanar -{ - using unpacked_type = typename Unpacker::unpacked_type; - static constexpr u32 xpp = 4; - static constexpr u32 ypp = 1; - static void Convert(PixelBuffer *pb, const u8 *data) - { - const u16 *p_in = (const u16 *)data; - pb->prel(0, Unpacker::unpack(p_in[0])); - pb->prel(1, Unpacker::unpack(p_in[1])); - pb->prel(2, Unpacker::unpack(p_in[2])); - pb->prel(3, Unpacker::unpack(p_in[3])); - } -}; - -template -struct ConvertPlanarYUV -{ - using unpacked_type = u32; - static constexpr u32 xpp = 4; - static constexpr u32 ypp = 1; - static void Convert(PixelBuffer *pb, const u8 *data) - { - //convert 4x1 4444 to 4x1 8888 - const u32 *p_in = (const u32 *)data; - - - s32 Y0 = (p_in[0] >> 8) & 255; // - s32 Yu = (p_in[0] >> 0) & 255; //p_in[0] - s32 Y1 = (p_in[0] >> 24) & 255; //p_in[3] - s32 Yv = (p_in[0] >> 16) & 255; //p_in[2] - - //0,0 - pb->prel(0, YUV422(Y0, Yu, Yv)); - //1,0 - pb->prel(1, YUV422(Y1, Yu, Yv)); - - //next 4 bytes - p_in += 1; - - Y0 = (p_in[0] >> 8) & 255; // - Yu = (p_in[0] >> 0) & 255; //p_in[0] - Y1 = (p_in[0] >> 24) & 255; //p_in[3] - Yv = (p_in[0] >> 16) & 255; //p_in[2] - - //0,0 - pb->prel(2, YUV422(Y0, Yu, Yv)); - //1,0 - pb->prel(3, YUV422(Y1, Yu, Yv)); - } -}; - -template -struct ConvertTwiddle -{ - using unpacked_type = typename Unpacker::unpacked_type; - static constexpr u32 xpp = 2; - static constexpr u32 ypp = 2; - static void Convert(PixelBuffer *pb, const u8 *data) - { - const u16 *p_in = (const u16 *)data; - pb->prel(0, 0, Unpacker::unpack(p_in[0])); - pb->prel(0, 1, Unpacker::unpack(p_in[1])); - pb->prel(1, 0, Unpacker::unpack(p_in[2])); - pb->prel(1, 1, Unpacker::unpack(p_in[3])); - } -}; - -template -struct ConvertTwiddleYUV -{ - using unpacked_type = u32; - static constexpr u32 xpp = 2; - static constexpr u32 ypp = 2; - static void Convert(PixelBuffer *pb, const u8 *data) - { - //convert 4x1 4444 to 4x1 8888 - const u16* p_in = (const u16 *)data; - - s32 Y0 = (p_in[0] >> 8) & 255; // - s32 Yu = (p_in[0] >> 0) & 255; //p_in[0] - s32 Y1 = (p_in[2] >> 8) & 255; //p_in[3] - s32 Yv = (p_in[2] >> 0) & 255; //p_in[2] - - //0,0 - pb->prel(0, 0, YUV422(Y0, Yu, Yv)); - //1,0 - pb->prel(1, 0, YUV422(Y1, Yu, Yv)); - - //next 4 bytes - //p_in+=2; - - Y0 = (p_in[1] >> 8) & 255; // - Yu = (p_in[1] >> 0) & 255; //p_in[0] - Y1 = (p_in[3] >> 8) & 255; //p_in[3] - Yv = (p_in[3] >> 0) & 255; //p_in[2] - - //0,1 - pb->prel(0, 1, YUV422(Y0, Yu, Yv)); - //1,1 - pb->prel(1, 1, YUV422(Y1, Yu, Yv)); - } -}; - -template -struct UnpackerPalToRgb { - using unpacked_type = Pixel; - static Pixel unpack(u8 col) - { - u32 *pal = sizeof(Pixel) == 2 ? &palette16_ram[palette_index] : &palette32_ram[palette_index]; - return pal[col]; - } -}; - -template -struct ConvertTwiddlePal4 -{ - using unpacked_type = typename Unpacker::unpacked_type; - static constexpr u32 xpp = 4; - static constexpr u32 ypp = 4; - static void Convert(PixelBuffer *pb, const u8 *data) - { - const u8 *p_in = data; - - pb->prel(0, 0, Unpacker::unpack(p_in[0] & 0xF)); - pb->prel(0, 1, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; - pb->prel(1, 0, Unpacker::unpack(p_in[0] & 0xF)); - pb->prel(1, 1, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; - - pb->prel(0, 2, Unpacker::unpack(p_in[0] & 0xF)); - pb->prel(0, 3, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; - pb->prel(1, 2, Unpacker::unpack(p_in[0] & 0xF)); - pb->prel(1, 3, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; - - pb->prel(2, 0, Unpacker::unpack(p_in[0] & 0xF)); - pb->prel(2, 1, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; - pb->prel(3, 0, Unpacker::unpack(p_in[0] & 0xF)); - pb->prel(3, 1, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; - - pb->prel(2, 2, Unpacker::unpack(p_in[0] & 0xF)); - pb->prel(2, 3, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; - pb->prel(3, 2, Unpacker::unpack(p_in[0] & 0xF)); - pb->prel(3, 3, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; - } -}; - -template -struct ConvertTwiddlePal8 -{ - using unpacked_type = typename Unpacker::unpacked_type; - static constexpr u32 xpp = 2; - static constexpr u32 ypp = 4; - static void Convert(PixelBuffer *pb, const u8 *data) - { - const u8* p_in = (const u8 *)data; - - pb->prel(0, 0, Unpacker::unpack(p_in[0])); p_in++; - pb->prel(0, 1, Unpacker::unpack(p_in[0])); p_in++; - pb->prel(1, 0, Unpacker::unpack(p_in[0])); p_in++; - pb->prel(1, 1, Unpacker::unpack(p_in[0])); p_in++; - - pb->prel(0, 2, Unpacker::unpack(p_in[0])); p_in++; - pb->prel(0, 3, Unpacker::unpack(p_in[0])); p_in++; - pb->prel(1, 2, Unpacker::unpack(p_in[0])); p_in++; - pb->prel(1, 3, Unpacker::unpack(p_in[0])); p_in++; - } -}; - -//handler functions -template -void texture_PL(PixelBuffer* pb, const u8* p_in, u32 Width, u32 Height) -{ - pb->amove(0,0); - - Height/=PixelConvertor::ypp; - Width/=PixelConvertor::xpp; - - for (u32 y=0;yrmovex(PixelConvertor::xpp); - } - pb->rmovey(PixelConvertor::ypp); - } -} - -template -void texture_PLVQ(PixelBuffer* pb, const u8* p_in, u32 width, u32 height) -{ - pb->amove(0, 0); - - height /= PixelConvertor::ypp; - width /= PixelConvertor::xpp; - - for (u32 y = 0; y < height; y++) - { - for (u32 x = 0; x < width; x++) - { - u8 p = *p_in++; - PixelConvertor::Convert(pb, &vq_codebook[p * 8]); - pb->rmovex(PixelConvertor::xpp); - } - pb->rmovey(PixelConvertor::ypp); - } -} - -template -void texture_TW(PixelBuffer* pb, const u8* p_in, u32 Width, u32 Height) -{ - pb->amove(0, 0); - - const u32 divider = PixelConvertor::xpp * PixelConvertor::ypp; - - const u32 bcx = bitscanrev(Width); - const u32 bcy = bitscanrev(Height); - - for (u32 y = 0; y < Height; y += PixelConvertor::ypp) - { - for (u32 x = 0; x < Width; x += PixelConvertor::xpp) - { - const u8* p = &p_in[(twop(x, y, bcx, bcy) / divider) << 3]; - PixelConvertor::Convert(pb, p); - - pb->rmovex(PixelConvertor::xpp); - } - pb->rmovey(PixelConvertor::ypp); - } -} - -template -void texture_VQ(PixelBuffer* pb, const u8* p_in, u32 Width, u32 Height) -{ - pb->amove(0, 0); - - const u32 divider = PixelConvertor::xpp * PixelConvertor::ypp; - const u32 bcx = bitscanrev(Width); - const u32 bcy = bitscanrev(Height); - - for (u32 y = 0; y < Height; y += PixelConvertor::ypp) - { - for (u32 x = 0; x < Width; x += PixelConvertor::xpp) - { - u8 p = p_in[twop(x, y, bcx, bcy) / divider]; - PixelConvertor::Convert(pb, &vq_codebook[p * 8]); - - pb->rmovex(PixelConvertor::xpp); - } - pb->rmovey(PixelConvertor::ypp); - } -} - -typedef void (*TexConvFP)(PixelBuffer *pb, const u8 *p_in, u32 width, u32 height); -typedef void (*TexConvFP8)(PixelBuffer *pb, const u8 *p_in, u32 width, u32 height); -typedef void (*TexConvFP32)(PixelBuffer *pb, const u8 *p_in, u32 width, u32 height); - -//Twiddle -constexpr TexConvFP tex565_TW = texture_TW>>; -// Palette -constexpr TexConvFP texPAL4_TW = texture_TW>>; -constexpr TexConvFP texPAL8_TW = texture_TW>>; -constexpr TexConvFP32 texPAL4_TW32 = texture_TW>>; -constexpr TexConvFP32 texPAL8_TW32 = texture_TW>>; -constexpr TexConvFP8 texPAL4PT_TW = texture_TW>>; -constexpr TexConvFP8 texPAL8PT_TW = texture_TW>>; -//VQ -constexpr TexConvFP tex565_VQ = texture_VQ>>; -// According to the documentation, a texture cannot be compressed and use -// a palette at the same time. However the hardware displays them -// just fine. -constexpr TexConvFP texPAL4_VQ = texture_VQ>>; -constexpr TexConvFP texPAL8_VQ = texture_VQ>>; -constexpr TexConvFP32 texPAL4_VQ32 = texture_VQ>>; -constexpr TexConvFP32 texPAL8_VQ32 = texture_VQ>>; - -namespace opengl { -// OpenGL - -//Planar -constexpr TexConvFP32 texYUV422_PL = texture_PL>; -constexpr TexConvFP32 tex565_PL32 = texture_PL>>; -constexpr TexConvFP32 tex1555_PL32 = texture_PL>>; -constexpr TexConvFP32 tex4444_PL32 = texture_PL>>; - -constexpr TexConvFP32 texYUV422_PLVQ = texture_PLVQ>; -constexpr TexConvFP32 tex565_PLVQ32 = texture_PLVQ>>; -constexpr TexConvFP32 tex1555_PLVQ32 = texture_PLVQ>>; -constexpr TexConvFP32 tex4444_PLVQ32 = texture_PLVQ>>; - -//Twiddle -constexpr TexConvFP tex1555_TW = texture_TW>; -constexpr TexConvFP tex4444_TW = texture_TW>; -constexpr TexConvFP texBMP_TW = tex4444_TW; -constexpr TexConvFP32 texYUV422_TW = texture_TW>; - -constexpr TexConvFP32 tex565_TW32 = texture_TW>>; -constexpr TexConvFP32 tex1555_TW32 = texture_TW>>; -constexpr TexConvFP32 tex4444_TW32 = texture_TW>>; - -//VQ -constexpr TexConvFP tex1555_VQ = texture_VQ>; -constexpr TexConvFP tex4444_VQ = texture_VQ>; -constexpr TexConvFP texBMP_VQ = tex4444_VQ; -constexpr TexConvFP32 texYUV422_VQ = texture_VQ>; - -constexpr TexConvFP32 tex565_VQ32 = texture_VQ>>; -constexpr TexConvFP32 tex1555_VQ32 = texture_VQ>>; -constexpr TexConvFP32 tex4444_VQ32 = texture_VQ>>; -} - -namespace directx { -// DirectX - -//Planar -constexpr TexConvFP32 texYUV422_PL = texture_PL>; -constexpr TexConvFP32 tex565_PL32 = texture_PL>>; -constexpr TexConvFP32 tex1555_PL32 = texture_PL>>; -constexpr TexConvFP32 tex4444_PL32 = texture_PL>>; - -constexpr TexConvFP32 texYUV422_PLVQ = texture_PLVQ>; -constexpr TexConvFP32 tex565_PLVQ32 = texture_PLVQ>>; -constexpr TexConvFP32 tex1555_PLVQ32 = texture_PLVQ>>; -constexpr TexConvFP32 tex4444_PLVQ32 = texture_PLVQ>>; - -//Twiddle -constexpr TexConvFP tex1555_TW = texture_TW>>; -constexpr TexConvFP tex4444_TW = texture_TW>>; -constexpr TexConvFP texBMP_TW = tex4444_TW; -constexpr TexConvFP32 texYUV422_TW = texture_TW>; - -constexpr TexConvFP32 tex565_TW32 = texture_TW>>; -constexpr TexConvFP32 tex1555_TW32 = texture_TW>>; -constexpr TexConvFP32 tex4444_TW32 = texture_TW>>; - -//VQ -constexpr TexConvFP tex1555_VQ = texture_VQ>>; -constexpr TexConvFP tex4444_VQ = texture_VQ>>; -constexpr TexConvFP texBMP_VQ = tex4444_VQ; -constexpr TexConvFP32 texYUV422_VQ = texture_VQ>; - -constexpr TexConvFP32 tex565_VQ32 = texture_VQ>>; -constexpr TexConvFP32 tex1555_VQ32 = texture_VQ>>; -constexpr TexConvFP32 tex4444_VQ32 = texture_VQ>>; -} - class BaseTextureCacheData; struct vram_block @@ -590,9 +44,6 @@ bool VramLockedWrite(u8* address); void UpscalexBRZ(int factor, u32* source, u32* dest, int width, int height, bool has_alpha); -struct PvrTexInfo; -enum class TextureType { _565, _5551, _4444, _8888, _8 }; - class BaseTextureCacheData { protected: @@ -720,9 +171,6 @@ class BaseTextureCacheData static void SetDirectXColorOrder(bool enabled); }; -// TODO Split the texture cache in a separate header -#include "CustomTexture.h" - template class BaseTextureCache { diff --git a/core/rend/gles/gles.cpp b/core/rend/gles/gles.cpp index ad14b9009..f9354131b 100644 --- a/core/rend/gles/gles.cpp +++ b/core/rend/gles/gles.cpp @@ -8,7 +8,6 @@ #include "rend/gles/postprocess.h" #include "vmu_xhair.h" #endif -#include "rend/TexCache.h" #include "rend/transform_matrix.h" #include "wsi/gl_context.h" #include "emulator.h" diff --git a/core/rend/gles/gltex.cpp b/core/rend/gles/gltex.cpp index b7c10e182..67efc8d07 100644 --- a/core/rend/gles/gltex.cpp +++ b/core/rend/gles/gltex.cpp @@ -1,7 +1,6 @@ #include "glcache.h" #include "gles.h" #include "hw/pvr/pvr_mem.h" -#include "rend/TexCache.h" #include diff --git a/core/rend/texconv.cpp b/core/rend/texconv.cpp new file mode 100644 index 000000000..6b6cae678 --- /dev/null +++ b/core/rend/texconv.cpp @@ -0,0 +1,539 @@ +/* + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#include "texconv.h" +#include "cfg/option.h" +#include "hw/pvr/Renderer_if.h" +#include +#include + +const u8 *vq_codebook; +u32 palette_index; +u32 palette16_ram[1024]; +u32 palette32_ram[1024]; +u32 pal_hash_256[4]; +u32 pal_hash_16[64]; +extern bool pal_needs_update; + +u32 detwiddle[2][11][1024]; +//input : address in the yyyyyxxxxx format +//output : address in the xyxyxyxy format +//U : x resolution , V : y resolution +//twiddle works on 64b words + +static u32 twiddle_slow(u32 x, u32 y, u32 x_sz, u32 y_sz) +{ + u32 rv = 0; // low 2 bits are directly passed -> needs some misc stuff to work.However + // Pvr internally maps the 64b banks "as if" they were twiddled + + u32 sh = 0; + x_sz >>= 1; + y_sz >>= 1; + while (x_sz != 0 || y_sz != 0) + { + if (y_sz != 0) + { + u32 temp = y & 1; + rv |= temp << sh; + + y_sz >>= 1; + y >>= 1; + sh++; + } + if (x_sz != 0) + { + u32 temp = x & 1; + rv |= temp << sh; + + x_sz >>= 1; + x >>= 1; + sh++; + } + } + return rv; +} + +static OnLoad _([]() { + constexpr u32 x_sz = 1024; + for (u32 s = 0; s < 11; s++) + { + u32 y_sz = 1 << s; + for (u32 i = 0; i < x_sz; i++) { + detwiddle[0][s][i] = twiddle_slow(i, 0, x_sz, y_sz); + detwiddle[1][s][i] = twiddle_slow(0, i, y_sz, x_sz); + } + } +}); + +void palette_update() +{ + if (!pal_needs_update) + return; + pal_needs_update = false; + rend_updatePalette(); + + if (!isDirectX(config::RendererType)) + { + switch (PAL_RAM_CTRL & 3) + { + case 0: + for (int i = 0; i < 1024; i++) { + palette16_ram[i] = Unpacker1555::unpack(PALETTE_RAM[i]); + palette32_ram[i] = Unpacker1555_32::unpack(PALETTE_RAM[i]); + } + break; + + case 1: + for (int i = 0; i < 1024; i++) { + palette16_ram[i] = UnpackerNop::unpack(PALETTE_RAM[i]); + palette32_ram[i] = Unpacker565_32::unpack(PALETTE_RAM[i]); + } + break; + + case 2: + for (int i = 0; i < 1024; i++) { + palette16_ram[i] = Unpacker4444::unpack(PALETTE_RAM[i]); + palette32_ram[i] = Unpacker4444_32::unpack(PALETTE_RAM[i]); + } + break; + + case 3: + for (int i = 0; i < 1024; i++) + palette32_ram[i] = Unpacker8888::unpack(PALETTE_RAM[i]); + break; + } + } + else + { + switch (PAL_RAM_CTRL & 3) + { + case 0: + for (int i = 0; i < 1024; i++) { + palette16_ram[i] = UnpackerNop::unpack(PALETTE_RAM[i]); + palette32_ram[i] = Unpacker1555_32::unpack(PALETTE_RAM[i]); + } + break; + + case 1: + for (int i = 0; i < 1024; i++) { + palette16_ram[i] = UnpackerNop::unpack(PALETTE_RAM[i]); + palette32_ram[i] = Unpacker565_32::unpack(PALETTE_RAM[i]); + } + break; + + case 2: + for (int i = 0; i < 1024; i++) { + palette16_ram[i] = UnpackerNop::unpack(PALETTE_RAM[i]); + palette32_ram[i] = Unpacker4444_32::unpack(PALETTE_RAM[i]); + } + break; + + case 3: + for (int i = 0; i < 1024; i++) + palette32_ram[i] = UnpackerNop::unpack(PALETTE_RAM[i]); + break; + } + } + for (std::size_t i = 0; i < std::size(pal_hash_16); i++) + pal_hash_16[i] = XXH32(&PALETTE_RAM[i << 4], 16 * 4, 7); + for (std::size_t i = 0; i < std::size(pal_hash_256); i++) + pal_hash_256[i] = XXH32(&PALETTE_RAM[i << 8], 256 * 4, 7); +} + +template +inline static u32 YUV422(s32 Y, s32 Yu, s32 Yv) +{ + Yu -= 128; + Yv -= 128; + + s32 R = Y + Yv * 11 / 8; // Y + (Yv-128) * (11/8) ? + s32 G = Y - (Yu * 11 + Yv * 22) / 32; // Y - (Yu-128) * (11/8) * 0.25 - (Yv-128) * (11/8) * 0.5 ? + s32 B = Y + Yu * 110 / 64; // Y + (Yu-128) * (11/8) * 1.25 ? + + return Packer::pack(std::clamp(R, 0, 255), std::clamp(G, 0, 255), std::clamp(B, 0, 255), 0xFF); +} + +static u32 twop(u32 x, u32 y, u32 bcx, u32 bcy) { + return detwiddle[0][bcy][x] + detwiddle[1][bcx][y]; +} + +template +struct ConvertPlanar +{ + using unpacked_type = typename Unpacker::unpacked_type; + static constexpr u32 xpp = 4; + static constexpr u32 ypp = 1; + static void Convert(PixelBuffer *pb, const u8 *data) + { + const u16 *p_in = (const u16 *)data; + pb->prel(0, Unpacker::unpack(p_in[0])); + pb->prel(1, Unpacker::unpack(p_in[1])); + pb->prel(2, Unpacker::unpack(p_in[2])); + pb->prel(3, Unpacker::unpack(p_in[3])); + } +}; + +template +struct ConvertPlanarYUV +{ + using unpacked_type = u32; + static constexpr u32 xpp = 4; + static constexpr u32 ypp = 1; + static void Convert(PixelBuffer *pb, const u8 *data) + { + //convert 4x1 4444 to 4x1 8888 + const u32 *p_in = (const u32 *)data; + + + s32 Y0 = (p_in[0] >> 8) & 255; // + s32 Yu = (p_in[0] >> 0) & 255; //p_in[0] + s32 Y1 = (p_in[0] >> 24) & 255; //p_in[3] + s32 Yv = (p_in[0] >> 16) & 255; //p_in[2] + + //0,0 + pb->prel(0, YUV422(Y0, Yu, Yv)); + //1,0 + pb->prel(1, YUV422(Y1, Yu, Yv)); + + //next 4 bytes + p_in += 1; + + Y0 = (p_in[0] >> 8) & 255; // + Yu = (p_in[0] >> 0) & 255; //p_in[0] + Y1 = (p_in[0] >> 24) & 255; //p_in[3] + Yv = (p_in[0] >> 16) & 255; //p_in[2] + + //0,0 + pb->prel(2, YUV422(Y0, Yu, Yv)); + //1,0 + pb->prel(3, YUV422(Y1, Yu, Yv)); + } +}; + +template +struct ConvertTwiddle +{ + using unpacked_type = typename Unpacker::unpacked_type; + static constexpr u32 xpp = 2; + static constexpr u32 ypp = 2; + static void Convert(PixelBuffer *pb, const u8 *data) + { + const u16 *p_in = (const u16 *)data; + pb->prel(0, 0, Unpacker::unpack(p_in[0])); + pb->prel(0, 1, Unpacker::unpack(p_in[1])); + pb->prel(1, 0, Unpacker::unpack(p_in[2])); + pb->prel(1, 1, Unpacker::unpack(p_in[3])); + } +}; + +template +struct ConvertTwiddleYUV +{ + using unpacked_type = u32; + static constexpr u32 xpp = 2; + static constexpr u32 ypp = 2; + static void Convert(PixelBuffer *pb, const u8 *data) + { + //convert 4x1 4444 to 4x1 8888 + const u16* p_in = (const u16 *)data; + + s32 Y0 = (p_in[0] >> 8) & 255; // + s32 Yu = (p_in[0] >> 0) & 255; //p_in[0] + s32 Y1 = (p_in[2] >> 8) & 255; //p_in[3] + s32 Yv = (p_in[2] >> 0) & 255; //p_in[2] + + //0,0 + pb->prel(0, 0, YUV422(Y0, Yu, Yv)); + //1,0 + pb->prel(1, 0, YUV422(Y1, Yu, Yv)); + + //next 4 bytes + //p_in+=2; + + Y0 = (p_in[1] >> 8) & 255; // + Yu = (p_in[1] >> 0) & 255; //p_in[0] + Y1 = (p_in[3] >> 8) & 255; //p_in[3] + Yv = (p_in[3] >> 0) & 255; //p_in[2] + + //0,1 + pb->prel(0, 1, YUV422(Y0, Yu, Yv)); + //1,1 + pb->prel(1, 1, YUV422(Y1, Yu, Yv)); + } +}; + +template +struct UnpackerPalToRgb { + using unpacked_type = Pixel; + static Pixel unpack(u8 col) + { + u32 *pal = sizeof(Pixel) == 2 ? &palette16_ram[palette_index] : &palette32_ram[palette_index]; + return pal[col]; + } +}; + +template +struct ConvertTwiddlePal4 +{ + using unpacked_type = typename Unpacker::unpacked_type; + static constexpr u32 xpp = 4; + static constexpr u32 ypp = 4; + static void Convert(PixelBuffer *pb, const u8 *data) + { + const u8 *p_in = data; + + pb->prel(0, 0, Unpacker::unpack(p_in[0] & 0xF)); + pb->prel(0, 1, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; + pb->prel(1, 0, Unpacker::unpack(p_in[0] & 0xF)); + pb->prel(1, 1, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; + + pb->prel(0, 2, Unpacker::unpack(p_in[0] & 0xF)); + pb->prel(0, 3, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; + pb->prel(1, 2, Unpacker::unpack(p_in[0] & 0xF)); + pb->prel(1, 3, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; + + pb->prel(2, 0, Unpacker::unpack(p_in[0] & 0xF)); + pb->prel(2, 1, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; + pb->prel(3, 0, Unpacker::unpack(p_in[0] & 0xF)); + pb->prel(3, 1, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; + + pb->prel(2, 2, Unpacker::unpack(p_in[0] & 0xF)); + pb->prel(2, 3, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; + pb->prel(3, 2, Unpacker::unpack(p_in[0] & 0xF)); + pb->prel(3, 3, Unpacker::unpack((p_in[0] >> 4) & 0xF)); p_in++; + } +}; + +template +struct ConvertTwiddlePal8 +{ + using unpacked_type = typename Unpacker::unpacked_type; + static constexpr u32 xpp = 2; + static constexpr u32 ypp = 4; + static void Convert(PixelBuffer *pb, const u8 *data) + { + const u8* p_in = (const u8 *)data; + + pb->prel(0, 0, Unpacker::unpack(p_in[0])); p_in++; + pb->prel(0, 1, Unpacker::unpack(p_in[0])); p_in++; + pb->prel(1, 0, Unpacker::unpack(p_in[0])); p_in++; + pb->prel(1, 1, Unpacker::unpack(p_in[0])); p_in++; + + pb->prel(0, 2, Unpacker::unpack(p_in[0])); p_in++; + pb->prel(0, 3, Unpacker::unpack(p_in[0])); p_in++; + pb->prel(1, 2, Unpacker::unpack(p_in[0])); p_in++; + pb->prel(1, 3, Unpacker::unpack(p_in[0])); p_in++; + } +}; + +//handler functions +template +void texture_PL(PixelBuffer* pb, const u8* p_in, u32 width, u32 height) +{ + pb->amove(0,0); + + height /= PixelConvertor::ypp; + width /= PixelConvertor::xpp; + + for (u32 y = 0; y < height; y++) + { + for (u32 x = 0; x < width; x++) + { + const u8* p = p_in; + PixelConvertor::Convert(pb, p); + p_in += 8; + + pb->rmovex(PixelConvertor::xpp); + } + pb->rmovey(PixelConvertor::ypp); + } +} + +template +void texture_PLVQ(PixelBuffer* pb, const u8* p_in, u32 width, u32 height) +{ + pb->amove(0, 0); + + height /= PixelConvertor::ypp; + width /= PixelConvertor::xpp; + + for (u32 y = 0; y < height; y++) + { + for (u32 x = 0; x < width; x++) + { + u8 p = *p_in++; + PixelConvertor::Convert(pb, &vq_codebook[p * 8]); + pb->rmovex(PixelConvertor::xpp); + } + pb->rmovey(PixelConvertor::ypp); + } +} + +template +void texture_TW(PixelBuffer* pb, const u8* p_in, u32 width, u32 height) +{ + pb->amove(0, 0); + + const u32 divider = PixelConvertor::xpp * PixelConvertor::ypp; + + const u32 bcx = bitscanrev(width); + const u32 bcy = bitscanrev(height); + + for (u32 y = 0; y < height; y += PixelConvertor::ypp) + { + for (u32 x = 0; x < width; x += PixelConvertor::xpp) + { + const u8* p = &p_in[(twop(x, y, bcx, bcy) / divider) << 3]; + PixelConvertor::Convert(pb, p); + + pb->rmovex(PixelConvertor::xpp); + } + pb->rmovey(PixelConvertor::ypp); + } +} + +template +void texture_VQ(PixelBuffer* pb, const u8* p_in, u32 width, u32 height) +{ + pb->amove(0, 0); + + const u32 divider = PixelConvertor::xpp * PixelConvertor::ypp; + const u32 bcx = bitscanrev(width); + const u32 bcy = bitscanrev(height); + + for (u32 y = 0; y < height; y += PixelConvertor::ypp) + { + for (u32 x = 0; x < width; x += PixelConvertor::xpp) + { + u8 p = p_in[twop(x, y, bcx, bcy) / divider]; + PixelConvertor::Convert(pb, &vq_codebook[p * 8]); + + pb->rmovex(PixelConvertor::xpp); + } + pb->rmovey(PixelConvertor::ypp); + } +} + +//Twiddle +const TexConvFP tex565_TW = texture_TW>>; +// Palette +const TexConvFP texPAL4_TW = texture_TW>>; +const TexConvFP texPAL8_TW = texture_TW>>; +const TexConvFP32 texPAL4_TW32 = texture_TW>>; +const TexConvFP32 texPAL8_TW32 = texture_TW>>; +const TexConvFP8 texPAL4PT_TW = texture_TW>>; +const TexConvFP8 texPAL8PT_TW = texture_TW>>; +//VQ +const TexConvFP tex565_VQ = texture_VQ>>; +// According to the documentation, a texture cannot be compressed and use +// a palette at the same time. However the hardware displays them +// just fine. +const TexConvFP texPAL4_VQ = texture_VQ>>; +const TexConvFP texPAL8_VQ = texture_VQ>>; +const TexConvFP32 texPAL4_VQ32 = texture_VQ>>; +const TexConvFP32 texPAL8_VQ32 = texture_VQ>>; + +namespace opengl { +// OpenGL + +//Planar +const TexConvFP32 texYUV422_PL = texture_PL>; +const TexConvFP32 tex565_PL32 = texture_PL>>; +const TexConvFP32 tex1555_PL32 = texture_PL>>; +const TexConvFP32 tex4444_PL32 = texture_PL>>; + +const TexConvFP32 texYUV422_PLVQ = texture_PLVQ>; +const TexConvFP32 tex565_PLVQ32 = texture_PLVQ>>; +const TexConvFP32 tex1555_PLVQ32 = texture_PLVQ>>; +const TexConvFP32 tex4444_PLVQ32 = texture_PLVQ>>; + +//Twiddle +const TexConvFP tex1555_TW = texture_TW>; +const TexConvFP tex4444_TW = texture_TW>; +const TexConvFP texBMP_TW = tex4444_TW; +const TexConvFP32 texYUV422_TW = texture_TW>; + +const TexConvFP32 tex565_TW32 = texture_TW>>; +const TexConvFP32 tex1555_TW32 = texture_TW>>; +const TexConvFP32 tex4444_TW32 = texture_TW>>; + +//VQ +const TexConvFP tex1555_VQ = texture_VQ>; +const TexConvFP tex4444_VQ = texture_VQ>; +const TexConvFP texBMP_VQ = tex4444_VQ; +const TexConvFP32 texYUV422_VQ = texture_VQ>; + +const TexConvFP32 tex565_VQ32 = texture_VQ>>; +const TexConvFP32 tex1555_VQ32 = texture_VQ>>; +const TexConvFP32 tex4444_VQ32 = texture_VQ>>; +} + +namespace directx { +// DirectX + +//Planar +const TexConvFP32 texYUV422_PL = texture_PL>; +const TexConvFP32 tex565_PL32 = texture_PL>>; +const TexConvFP32 tex1555_PL32 = texture_PL>>; +const TexConvFP32 tex4444_PL32 = texture_PL>>; + +const TexConvFP32 texYUV422_PLVQ = texture_PLVQ>; +const TexConvFP32 tex565_PLVQ32 = texture_PLVQ>>; +const TexConvFP32 tex1555_PLVQ32 = texture_PLVQ>>; +const TexConvFP32 tex4444_PLVQ32 = texture_PLVQ>>; + +//Twiddle +const TexConvFP tex1555_TW = texture_TW>>; +const TexConvFP tex4444_TW = texture_TW>>; +const TexConvFP texBMP_TW = tex4444_TW; +const TexConvFP32 texYUV422_TW = texture_TW>; + +const TexConvFP32 tex565_TW32 = texture_TW>>; +const TexConvFP32 tex1555_TW32 = texture_TW>>; +const TexConvFP32 tex4444_TW32 = texture_TW>>; + +//VQ +const TexConvFP tex1555_VQ = texture_VQ>>; +const TexConvFP tex4444_VQ = texture_VQ>>; +const TexConvFP texBMP_VQ = tex4444_VQ; +const TexConvFP32 texYUV422_VQ = texture_VQ>; + +const TexConvFP32 tex565_VQ32 = texture_VQ>>; +const TexConvFP32 tex1555_VQ32 = texture_VQ>>; +const TexConvFP32 tex4444_VQ32 = texture_VQ>>; +} + +#define TEX_CONV_TABLE \ +const PvrTexInfo pvrTexInfo[8] = \ +{ /* name bpp Final format Twiddled VQ Planar(32b) Twiddled(32b) VQ (32b) PL VQ (32b) Palette (8b) */ \ + {"1555", 16, TextureType::_5551, tex1555_TW, tex1555_VQ, tex1555_PL32, tex1555_TW32, tex1555_VQ32, tex1555_PLVQ32, nullptr }, \ + {"565", 16, TextureType::_565, tex565_TW, tex565_VQ, tex565_PL32, tex565_TW32, tex565_VQ32, tex565_PLVQ32, nullptr }, \ + {"4444", 16, TextureType::_4444, tex4444_TW, tex4444_VQ, tex4444_PL32, tex4444_TW32, tex4444_VQ32, tex4444_PLVQ32, nullptr }, \ + {"yuv", 16, TextureType::_8888, nullptr, nullptr, texYUV422_PL, texYUV422_TW, texYUV422_VQ, texYUV422_PLVQ, nullptr }, \ + {"bumpmap", 16, TextureType::_4444, texBMP_TW, texBMP_VQ, tex4444_PL32, tex4444_TW32, tex4444_VQ32, tex4444_PLVQ32, nullptr }, \ + {"pal4", 4, TextureType::_5551, texPAL4_TW, texPAL4_VQ, nullptr, texPAL4_TW32, texPAL4_VQ32, nullptr, texPAL4PT_TW }, \ + {"pal8", 8, TextureType::_5551, texPAL8_TW, texPAL8_VQ, nullptr, texPAL8_TW32, texPAL8_VQ32, nullptr, texPAL8PT_TW }, \ + {"ns/1555", 0}, \ +} + +namespace opengl { + TEX_CONV_TABLE; +} +namespace directx { + TEX_CONV_TABLE; +} +#undef TEX_CONV_TABLE +const PvrTexInfo *pvrTexInfo = opengl::pvrTexInfo; diff --git a/core/rend/texconv.h b/core/rend/texconv.h new file mode 100644 index 000000000..cbaa2cf26 --- /dev/null +++ b/core/rend/texconv.h @@ -0,0 +1,257 @@ +/* + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#pragma once +#include "types.h" + +constexpr int VQ_CODEBOOK_SIZE = 256 * 8; +extern const u8 *vq_codebook; +extern u32 palette_index; +extern u32 palette16_ram[1024]; +extern u32 palette32_ram[1024]; +extern u32 pal_hash_256[4]; +extern u32 pal_hash_16[64]; + +void palette_update(); + +template +class PixelBuffer +{ + Pixel* p_buffer_start = nullptr; + Pixel* p_current_mipmap = nullptr; + Pixel* p_current_line = nullptr; + Pixel* p_current_pixel = nullptr; + + u32 pixels_per_line = 0; + +public: + ~PixelBuffer() { + deinit(); + } + + void init(u32 width, u32 height, bool mipmapped) + { + deinit(); + size_t size = width * height * sizeof(Pixel); + if (mipmapped) + { + do + { + width /= 2; + height /= 2; + size += width * height * sizeof(Pixel); + } + while (width != 0 && height != 0); + } + p_buffer_start = p_current_line = p_current_pixel = p_current_mipmap = (Pixel *)malloc(size); + this->pixels_per_line = 1; + } + + void init(u32 width, u32 height) + { + deinit(); + p_buffer_start = p_current_line = p_current_pixel = p_current_mipmap = (Pixel *)malloc(width * height * sizeof(Pixel)); + this->pixels_per_line = width; + } + + void deinit() + { + if (p_buffer_start != nullptr) + { + free(p_buffer_start); + p_buffer_start = p_current_mipmap = p_current_line = p_current_pixel = nullptr; + } + } + + void steal_data(PixelBuffer &buffer) + { + deinit(); + p_buffer_start = p_current_mipmap = p_current_line = p_current_pixel = buffer.p_buffer_start; + pixels_per_line = buffer.pixels_per_line; + buffer.p_buffer_start = buffer.p_current_mipmap = buffer.p_current_line = buffer.p_current_pixel = nullptr; + } + + void set_mipmap(int level) + { + u32 offset = 0; + for (int i = 0; i < level; i++) + offset += (1 << (2 * i)); + p_current_mipmap = p_current_line = p_current_pixel = p_buffer_start + offset; + pixels_per_line = 1 << level; + } + + Pixel *data(u32 x = 0, u32 y = 0) + { + return p_current_mipmap + pixels_per_line * y + x; + } + + void prel(u32 x, Pixel value) + { + p_current_pixel[x] = value; + } + + void prel(u32 x, u32 y, Pixel value) + { + p_current_pixel[y * pixels_per_line + x] = value; + } + + void rmovex(u32 value) + { + p_current_pixel += value; + } + + void rmovey(u32 value) + { + p_current_line += pixels_per_line * value; + p_current_pixel = p_current_line; + } + + void amove(u32 x_m, u32 y_m) + { + //p_current_pixel=p_buffer_start; + p_current_line = p_current_mipmap + pixels_per_line * y_m; + p_current_pixel = p_current_line + x_m; + } +}; + +// OpenGL +struct RGBAPacker { + static u32 pack(u8 r, u8 g, u8 b, u8 a) { + return r | (g << 8) | (b << 16) | (a << 24); + } +}; +// DirectX +struct BGRAPacker { + static u32 pack(u8 r, u8 g, u8 b, u8 a) { + return b | (g << 8) | (r << 16) | (a << 24); + } +}; + +template +struct UnpackerNop { + using unpacked_type = Pixel; + static Pixel unpack(Pixel word) { + return word; + } +}; + +// ARGB1555 to RGBA5551 +struct Unpacker1555 { + using unpacked_type = u16; + static u16 unpack(u16 word) { + return ((word >> 15) & 1) | (((word >> 10) & 0x1F) << 11) | (((word >> 5) & 0x1F) << 6) | (((word >> 0) & 0x1F) << 1); + } +}; + +// ARGB4444 to RGBA4444 +struct Unpacker4444 { + using unpacked_type = u16; + static u16 unpack(u16 word) { + return (((word >> 0) & 0xF) << 4) | (((word >> 4) & 0xF) << 8) | (((word >> 8) & 0xF) << 12) | (((word >> 12) & 0xF) << 0); + } +}; + +template +struct Unpacker1555_32 { + using unpacked_type = u32; + static u32 unpack(u16 word) { + return Packer::pack( + (((word >> 10) & 0x1F) << 3) | ((word >> 12) & 7), + (((word >> 5) & 0x1F) << 3) | ((word >> 7) & 7), + (((word >> 0) & 0x1F) << 3) | ((word >> 2) & 7), + (word & 0x8000) ? 0xFF : 0); + } +}; + +template +struct Unpacker565_32 { + using unpacked_type = u32; + static u32 unpack(u16 word) { + return Packer::pack( + (((word >> 11) & 0x1F) << 3) | ((word >> 13) & 7), + (((word >> 5) & 0x3F) << 2) | ((word >> 9) & 3), + (((word >> 0) & 0x1F) << 3) | ((word >> 2) & 7), + 0xFF); + } +}; + +template +struct Unpacker4444_32 { + using unpacked_type = u32; + static u32 unpack(u16 word) { + return Packer::pack( + (((word >> 8) & 0xF) << 4) | ((word >> 8) & 0xF), + (((word >> 4) & 0xF) << 4) | ((word >> 4) & 0xF), + (((word >> 0) & 0xF) << 4) | ((word >> 0) & 0xF), + (((word >> 12) & 0xF) << 4) | ((word >> 12) & 0xF)); + } +}; + +// ARGB8888 to whatever +template +struct Unpacker8888 { + using unpacked_type = u32; + static u32 unpack(u32 word) { + return Packer::pack( + (word >> 16) & 0xFF, + (word >> 8) & 0xFF, + (word >> 0) & 0xFF, + (word >> 24) & 0xFF); + } +}; + +enum class TextureType { _565, _5551, _4444, _8888, _8 }; + +typedef void (*TexConvFP)(PixelBuffer *pb, const u8 *p_in, u32 width, u32 height); +typedef void (*TexConvFP8)(PixelBuffer *pb, const u8 *p_in, u32 width, u32 height); +typedef void (*TexConvFP32)(PixelBuffer *pb, const u8 *p_in, u32 width, u32 height); + +struct PvrTexInfo +{ + const char* name; + int bpp; //4/8 for pal. 16 for yuv, rgb, argb + TextureType type; + // Conversion to 16 bpp + TexConvFP TW; + TexConvFP VQ; + // Conversion to 32 bpp + TexConvFP32 PL32; + TexConvFP32 TW32; + TexConvFP32 VQ32; + TexConvFP32 PLVQ32; + // Conversion to 8 bpp (palette) + TexConvFP8 TW8; +}; + +namespace opengl +{ + extern const TexConvFP32 tex1555_TW32; + extern const TexConvFP32 tex1555_VQ32; + extern const TexConvFP32 tex1555_PL32; + extern const TexConvFP32 tex565_TW32; + extern const TexConvFP32 tex565_VQ32; + extern const TexConvFP32 tex565_PL32; + extern const TexConvFP32 tex4444_TW32; + extern const TexConvFP32 tex4444_VQ32; + extern const TexConvFP32 tex4444_PL32; + + extern const PvrTexInfo pvrTexInfo[8]; +} +namespace directx +{ + extern const PvrTexInfo pvrTexInfo[8]; +} +extern const PvrTexInfo *pvrTexInfo; diff --git a/core/rend/transform_matrix.h b/core/rend/transform_matrix.h index e6335f01b..d79327272 100644 --- a/core/rend/transform_matrix.h +++ b/core/rend/transform_matrix.h @@ -19,7 +19,7 @@ along with Flycast. If not, see . */ #pragma once -#include "TexCache.h" +#include "hw/pvr/Renderer_if.h" #include "hw/pvr/ta_ctx.h" #include "cfg/option.h" diff --git a/core/rend/vulkan/oit/oit_renderpass.h b/core/rend/vulkan/oit/oit_renderpass.h index 0208be5f1..9c3c9cf87 100644 --- a/core/rend/vulkan/oit/oit_renderpass.h +++ b/core/rend/vulkan/oit/oit_renderpass.h @@ -20,6 +20,7 @@ */ #pragma once #include "../vulkan_context.h" +#include "cfg/option.h" class RenderPasses { diff --git a/core/rend/vulkan/vk_context_lr.h b/core/rend/vulkan/vk_context_lr.h index c97f489bc..2dc1f0afa 100644 --- a/core/rend/vulkan/vk_context_lr.h +++ b/core/rend/vulkan/vk_context_lr.h @@ -22,7 +22,7 @@ #include "vulkan.h" #include "vmallocator.h" #include "quad.h" -#include "rend/TexCache.h" +#include "rend/texconv.h" #include "libretro_vulkan.h" #include "wsi/context.h" #include "commandpool.h" diff --git a/core/rend/vulkan/vulkan_context.h b/core/rend/vulkan/vulkan_context.h index 8001905c2..4c6a45056 100644 --- a/core/rend/vulkan/vulkan_context.h +++ b/core/rend/vulkan/vulkan_context.h @@ -87,7 +87,7 @@ class CommandBufferDebugScope { #include "vmallocator.h" #include "quad.h" -#include "rend/TexCache.h" +#include "rend/texconv.h" #include "overlay.h" #include "wsi/context.h" #include diff --git a/core/ui/boxart/pvrparser.h b/core/ui/boxart/pvrparser.h index 989f52e24..cf713ac62 100644 --- a/core/ui/boxart/pvrparser.h +++ b/core/ui/boxart/pvrparser.h @@ -18,7 +18,8 @@ */ #pragma once #include "types.h" -#include "rend/TexCache.h" +#include "rend/texconv.h" +#include "hw/pvr/ta_structs.h" extern const u32 VQMipPoint[11]; extern const u32 OtherMipPoint[11]; From 667e3906d83ea0f66f514a77fc2158e86c421b06 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 14 Dec 2024 19:13:05 +0100 Subject: [PATCH 64/81] sh4: work around previous SCIF issues when loading a state Force SCIF_SCBRR2 to 8 bits when loading a state. See e03e11b8a99d3dcf0a363a66e6d398777cf9c203 and 6115a918b2edaab7fc0d4930a8f1e6fc387783fc. --- core/hw/sh4/modules/serial.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/hw/sh4/modules/serial.cpp b/core/hw/sh4/modules/serial.cpp index 1fd88b496..7338af8f2 100644 --- a/core/hw/sh4/modules/serial.cpp +++ b/core/hw/sh4/modules/serial.cpp @@ -440,6 +440,7 @@ void SCIFSerialPort::deserialize(Deserializer& deser) statusLastRead = 0; transmitting = false; } + SCIF_SCBRR2 &= 0xff; // work around previous issues with dynarecs updateBaudRate(); } From ecd0305a60a47a5550847addbb84b2e7ba0b1b7c Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 14 Dec 2024 19:43:32 +0100 Subject: [PATCH 65/81] fix build without dynarec --- core/build.h | 5 +++++ core/hw/sh4/dyna/blockmanager.h | 20 ++++++++++++++++++-- core/hw/sh4/dyna/decoder_opcodes.h | 4 ++-- core/hw/sh4/dyna/shil.cpp | 7 ++++--- core/hw/sh4/dyna/ssa.cpp | 3 +++ core/linux/common.cpp | 2 +- core/ui/gui.cpp | 2 ++ core/windows/fault_handler.cpp | 2 ++ 8 files changed, 37 insertions(+), 8 deletions(-) diff --git a/core/build.h b/core/build.h index b01192b21..183669cba 100755 --- a/core/build.h +++ b/core/build.h @@ -42,6 +42,11 @@ #define HOST_CPU CPU_GENERIC #endif +#if defined(TARGET_IPHONE) && !defined(__aarch64__) +// iOS simulator +#define TARGET_NO_REC +#endif + #if defined(TARGET_NO_REC) #define FEAT_SHREC DYNAREC_NONE #define FEAT_AREC DYNAREC_NONE diff --git a/core/hw/sh4/dyna/blockmanager.h b/core/hw/sh4/dyna/blockmanager.h index 255f42803..fd6445f25 100644 --- a/core/hw/sh4/dyna/blockmanager.h +++ b/core/hw/sh4/dyna/blockmanager.h @@ -84,15 +84,31 @@ void bm_Init(); void bm_Term(); void bm_vmem_pagefill(void** ptr,u32 size_bytes); -bool bm_RamWriteAccess(void *p); -void bm_RamWriteAccess(u32 addr); static inline bool bm_IsRamPageProtected(u32 addr) { extern bool unprotected_pages[RAM_SIZE_MAX/PAGE_SIZE]; addr &= RAM_MASK; return !unprotected_pages[addr / PAGE_SIZE]; } + +#if FEAT_SHREC != DYNAREC_NONE + +bool bm_RamWriteAccess(void *p); +void bm_RamWriteAccess(u32 addr); void bm_LockPage(u32 addr, u32 size = PAGE_SIZE); void bm_UnlockPage(u32 addr, u32 size = PAGE_SIZE); u32 bm_getRamOffset(void *p); +#else + +inline static bool bm_RamWriteAccess(void *p) { + return false; +} +inline static void bm_RamWriteAccess(u32 addr) {} +inline static void bm_LockPage(u32 addr, u32 size = PAGE_SIZE) {} +inline static void bm_UnlockPage(u32 addr, u32 size = PAGE_SIZE) {} +inline static u32 bm_getRamOffset(void *p) { + return 0; +} + +#endif diff --git a/core/hw/sh4/dyna/decoder_opcodes.h b/core/hw/sh4/dyna/decoder_opcodes.h index 6eee9fed6..b44c34fc6 100644 --- a/core/hw/sh4/dyna/decoder_opcodes.h +++ b/core/hw/sh4/dyna/decoder_opcodes.h @@ -1,8 +1,10 @@ #pragma once #if FEAT_SHREC != DYNAREC_NONE #define sh4dec(str) void dec_##str (u32 op) +void dec_illegalOp(u32 op); #else #define sh4dec(str) static void dec_##str (u32 op) { } +inline static void dec_illegalOp(u32 op) {} #endif sh4dec(i1000_1011_iiii_iiii); @@ -33,5 +35,3 @@ sh4dec(i0100_nnnn_0110_1010); sh4dec(i0100_nnnn_0110_0110); sh4dec(i0100_nnnn_0001_1011); sh4dec(i0100_nnnn_0000_0011); - -void dec_illegalOp(u32 op); diff --git a/core/hw/sh4/dyna/shil.cpp b/core/hw/sh4/dyna/shil.cpp index 7428f8926..d5c161761 100644 --- a/core/hw/sh4/dyna/shil.cpp +++ b/core/hw/sh4/dyna/shil.cpp @@ -1,11 +1,10 @@ -#include - #include "types.h" +#if FEAT_SHREC != DYNAREC_NONE #include "hw/sh4/sh4_mem.h" #include "hw/sh4/sh4_mmr.h" - #include "ngen.h" #include "ssa.h" +#include void AnalyseBlock(RuntimeBlockInfo* blk) { @@ -180,3 +179,5 @@ const char* shil_opcode_name(int op) { return shilop_str[op]; } + +#endif // FEAT_SHREC != DYNAREC_NONE diff --git a/core/hw/sh4/dyna/ssa.cpp b/core/hw/sh4/dyna/ssa.cpp index 11b641bf3..783874765 100644 --- a/core/hw/sh4/dyna/ssa.cpp +++ b/core/hw/sh4/dyna/ssa.cpp @@ -18,6 +18,8 @@ You should have received a copy of the GNU General Public License along with reicast. If not, see . */ +#include "build.h" +#if FEAT_SHREC != DYNAREC_NONE #include "blockmanager.h" #include "ssa.h" @@ -369,3 +371,4 @@ bool SSAOptimizer::ExecuteConstOp(shil_opcode* op) return false; } } +#endif // FEAT_SHREC != DYNAREC_NONE diff --git a/core/linux/common.cpp b/core/linux/common.cpp index 6e7ac28e8..c1f9d8b77 100644 --- a/core/linux/common.cpp +++ b/core/linux/common.cpp @@ -51,11 +51,11 @@ void fault_handler(int sn, siginfo_t * si, void *segfault_ctx) // texture protection in VRAM if (VramLockedWrite((u8*)si->si_addr)) return; +#if FEAT_SHREC == DYNAREC_JIT // FPCB jump table protection if (addrspace::bm_lockedWrite((u8*)si->si_addr)) return; -#if FEAT_SHREC == DYNAREC_JIT // fast mem access rewriting host_context_t ctx; context_from_segfault(&ctx, segfault_ctx); diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index f29ff70ba..9f68259f2 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -2804,6 +2804,7 @@ static void gui_settings_network() static void gui_settings_advanced() { +#if FEAT_SHREC != DYNAREC_NONE header("CPU Mode"); { ImGui::Columns(2, "cpu_modes", false); @@ -2819,6 +2820,7 @@ static void gui_settings_advanced() "%d MHz"); } ImGui::Spacing(); +#endif header("Other"); { OptionCheckbox("HLE BIOS", config::UseReios, "Force high-level BIOS emulation"); diff --git a/core/windows/fault_handler.cpp b/core/windows/fault_handler.cpp index ac7ce0903..babe160f8 100644 --- a/core/windows/fault_handler.cpp +++ b/core/windows/fault_handler.cpp @@ -128,9 +128,11 @@ static LONG WINAPI exceptionHandler(EXCEPTION_POINTERS *ep) // texture protection in VRAM if (VramLockedWrite(address)) return EXCEPTION_CONTINUE_EXECUTION; +#if FEAT_SHREC == DYNAREC_JIT // FPCB jump table protection if (addrspace::bm_lockedWrite(address)) return EXCEPTION_CONTINUE_EXECUTION; +#endif host_context_t context; readContext(ep, context); From 3f8d645c42d71a7e4d053b7117188d5bf9216667 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sun, 15 Dec 2024 15:55:28 +0100 Subject: [PATCH 66/81] rend: last frame was sometimes presented after the game is stopped When the clearLastFrame flag is set, renderers should return false from Present() and RenderLastFrame() until a new frame is rendered. --- core/rend/dx11/dx11_renderer.cpp | 2 ++ core/rend/dx11/dx11_renderer.h | 3 +-- core/rend/dx11/oit/dx11_oitrenderer.cpp | 1 + core/rend/dx9/d3d_renderer.cpp | 2 ++ core/rend/dx9/d3d_renderer.h | 3 +-- core/rend/gl4/gles.cpp | 1 + core/rend/gles/gldraw.cpp | 1 + core/rend/gles/gles.cpp | 1 + core/rend/gles/gles.h | 3 +-- core/rend/vulkan/oit/oit_renderer.cpp | 3 ++- core/rend/vulkan/vulkan_renderer.cpp | 10 ++++++++-- 11 files changed, 21 insertions(+), 9 deletions(-) diff --git a/core/rend/dx11/dx11_renderer.cpp b/core/rend/dx11/dx11_renderer.cpp index 199c586f1..ebb15ede2 100644 --- a/core/rend/dx11/dx11_renderer.cpp +++ b/core/rend/dx11/dx11_renderer.cpp @@ -525,6 +525,7 @@ bool DX11Renderer::Render() #endif frameRendered = true; frameRenderedOnce = true; + clearLastFrame = false; } return !is_rtt; @@ -1040,6 +1041,7 @@ void DX11Renderer::RenderFramebuffer(const FramebufferInfo& info) #endif frameRendered = true; frameRenderedOnce = true; + clearLastFrame = false; } void DX11Renderer::setBaseScissor() diff --git a/core/rend/dx11/dx11_renderer.h b/core/rend/dx11/dx11_renderer.h index c6d0ca63e..fb73fd7e8 100644 --- a/core/rend/dx11/dx11_renderer.h +++ b/core/rend/dx11/dx11_renderer.h @@ -42,10 +42,9 @@ struct DX11Renderer : public Renderer bool Present() override { - if (!frameRendered) + if (!frameRendered || clearLastFrame) return false; frameRendered = false; - clearLastFrame = false; #ifndef LIBRETRO imguiDriver->setFrameRendered(); #else diff --git a/core/rend/dx11/oit/dx11_oitrenderer.cpp b/core/rend/dx11/oit/dx11_oitrenderer.cpp index 5911cadf3..faaa430a9 100644 --- a/core/rend/dx11/oit/dx11_oitrenderer.cpp +++ b/core/rend/dx11/oit/dx11_oitrenderer.cpp @@ -694,6 +694,7 @@ struct DX11OITRenderer : public DX11Renderer #endif frameRendered = true; frameRenderedOnce = true; + clearLastFrame = false; } return !is_rtt; diff --git a/core/rend/dx9/d3d_renderer.cpp b/core/rend/dx9/d3d_renderer.cpp index ec77cb1c5..f3554a56d 100644 --- a/core/rend/dx9/d3d_renderer.cpp +++ b/core/rend/dx9/d3d_renderer.cpp @@ -302,6 +302,7 @@ void D3DRenderer::RenderFramebuffer(const FramebufferInfo& info) drawOSD(); frameRendered = true; frameRenderedOnce = true; + clearLastFrame = false; theDXContext.setFrameRendered(); } @@ -1181,6 +1182,7 @@ bool D3DRenderer::Render() drawOSD(); frameRendered = true; frameRenderedOnce = true; + clearLastFrame = false; theDXContext.setFrameRendered(); } diff --git a/core/rend/dx9/d3d_renderer.h b/core/rend/dx9/d3d_renderer.h index e6392131b..045c7a03e 100644 --- a/core/rend/dx9/d3d_renderer.h +++ b/core/rend/dx9/d3d_renderer.h @@ -106,11 +106,10 @@ struct D3DRenderer : public Renderer bool RenderLastFrame() override; bool Present() override { - if (!frameRendered) + if (!frameRendered || clearLastFrame) return false; imguiDriver->setFrameRendered(); frameRendered = false; - clearLastFrame = false; return true; } BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override; diff --git a/core/rend/gl4/gles.cpp b/core/rend/gl4/gles.cpp index 5cd12b6aa..ff0f64ec8 100644 --- a/core/rend/gl4/gles.cpp +++ b/core/rend/gl4/gles.cpp @@ -713,6 +713,7 @@ struct OpenGL4Renderer : OpenGLRenderer if (!config::EmulateFramebuffer) { frameRendered = true; + clearLastFrame = false; drawOSD(); renderVideoRouting(); } diff --git a/core/rend/gles/gldraw.cpp b/core/rend/gles/gldraw.cpp index 01327cc26..d79d8e824 100644 --- a/core/rend/gles/gldraw.cpp +++ b/core/rend/gles/gldraw.cpp @@ -688,6 +688,7 @@ void OpenGLRenderer::RenderFramebuffer(const FramebufferInfo& info) drawOSD(); frameRendered = true; + clearLastFrame = false; renderVideoRouting(); restoreCurrentFramebuffer(); } diff --git a/core/rend/gles/gles.cpp b/core/rend/gles/gles.cpp index f9354131b..d2d7b7ffd 100644 --- a/core/rend/gles/gles.cpp +++ b/core/rend/gles/gles.cpp @@ -1361,6 +1361,7 @@ bool OpenGLRenderer::Render() if (!config::EmulateFramebuffer) { frameRendered = true; + clearLastFrame = false; drawOSD(); renderVideoRouting(); } diff --git a/core/rend/gles/gles.h b/core/rend/gles/gles.h index d5bb74252..13f3a1d71 100755 --- a/core/rend/gles/gles.h +++ b/core/rend/gles/gles.h @@ -516,12 +516,11 @@ struct OpenGLRenderer : Renderer bool Present() override { - if (!frameRendered) + if (!frameRendered || clearLastFrame) return false; #ifndef LIBRETRO imguiDriver->setFrameRendered(); #endif - clearLastFrame = false; frameRendered = false; return true; } diff --git a/core/rend/vulkan/oit/oit_renderer.cpp b/core/rend/vulkan/oit/oit_renderer.cpp index 8422f47d4..090808b16 100644 --- a/core/rend/vulkan/oit/oit_renderer.cpp +++ b/core/rend/vulkan/oit/oit_renderer.cpp @@ -105,7 +105,8 @@ class OITVulkanRenderer final : public BaseVulkanRenderer bool Present() override { - clearLastFrame = false; + if (clearLastFrame) + return false; if (config::EmulateFramebuffer || framebufferRendered) return presentFramebuffer(); else diff --git a/core/rend/vulkan/vulkan_renderer.cpp b/core/rend/vulkan/vulkan_renderer.cpp index 108073901..c40fc989b 100644 --- a/core/rend/vulkan/vulkan_renderer.cpp +++ b/core/rend/vulkan/vulkan_renderer.cpp @@ -86,7 +86,11 @@ BaseTextureCacheData *BaseVulkanRenderer::GetTexture(TSP tsp, TCW tcw) void BaseVulkanRenderer::Process(TA_context* ctx) { - framebufferRendered = false; + if (!ctx->rend.isRTT) { + framebufferRendered = false; + if (!config::EmulateFramebuffer) + clearLastFrame = false; + } if (resetTextureCache) { textureCache.Clear(); resetTextureCache = false; @@ -156,6 +160,7 @@ void BaseVulkanRenderer::RenderFramebuffer(const FramebufferInfo& info) commandBuffer.end(); fbCommandPool.EndFrame(); framebufferRendered = true; + clearLastFrame = false; } void BaseVulkanRenderer::RenderVideoRouting() @@ -300,7 +305,8 @@ class VulkanRenderer final : public BaseVulkanRenderer bool Present() override { - clearLastFrame = false; + if (clearLastFrame) + return false; if (config::EmulateFramebuffer || framebufferRendered) return presentFramebuffer(); else From 34a5e7f47da15b4b1de267c82292e8c221bd4510 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 16 Dec 2024 16:53:38 +0100 Subject: [PATCH 67/81] input: dreamconn+ support prototype Issue #1305 --- CMakeLists.txt | 9 ++- core/hw/maple/maple_devs.cpp | 67 ++++++++++++++++++ core/sdl/dreamconn.cpp | 127 +++++++++++++++++++++++++++++++++++ core/sdl/dreamconn.h | 56 +++++++++++++++ core/sdl/sdl.cpp | 10 +++ 5 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 core/sdl/dreamconn.cpp create mode 100644 core/sdl/dreamconn.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b9f267fa0..76f9e082e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -461,7 +461,14 @@ if(NOT LIBRETRO) endif() target_compile_definitions(${PROJECT_NAME} PRIVATE USE_SDL USE_SDL_AUDIO) - target_sources(${PROJECT_NAME} PRIVATE core/sdl/sdl.cpp core/sdl/sdl.h core/sdl/sdl_gamepad.h core/sdl/sdl_keyboard.h) + target_sources(${PROJECT_NAME} PRIVATE + core/sdl/sdl.cpp + core/sdl/sdl.h + core/sdl/sdl_gamepad.h + core/sdl/sdl_keyboard.h + core/sdl/sdl_keyboard_mac.h + core/sdl/dreamconn.cpp + core/sdl/dreamconn.h) if((UNIX AND NOT APPLE) OR NINTENDO_SWITCH) find_package(CURL REQUIRED) diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index 9e2eb242c..b0cfcf84b 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -2101,3 +2101,70 @@ maple_device* maple_Create(MapleDeviceType type) } return nullptr; } + +#if defined(_WIN32) && !defined(TARGET_UWP) && defined(USE_SDL) && !defined(LIBRETRO) +#include "sdl/dreamconn.h" + +struct DreamConnVmu : public maple_sega_vmu +{ + DreamConn& dreamconn; + + DreamConnVmu(DreamConn& dreamconn) : dreamconn(dreamconn) { + } + + u32 dma(u32 cmd) override + { + if (cmd == MDCF_BlockWrite && *(u32 *)dma_buffer_in == MFID_2_LCD) + // send the raw maple msg + dreamconn.send(dma_buffer_in - 4, dma_count_in + 4); + return maple_sega_vmu::dma(cmd); + } +}; + +struct DreamConnPurupuru : public maple_sega_purupuru +{ + DreamConn& dreamconn; + + DreamConnPurupuru(DreamConn& dreamconn) : dreamconn(dreamconn) { + } + + u32 dma(u32 cmd) override + { + switch (cmd) + { + case MDCF_BlockWrite: + dreamconn.send(dma_buffer_in - 4, dma_count_in + 4); + break; + + case MDCF_SetCondition: + dreamconn.send(dma_buffer_in - 4, dma_count_in + 4); + break; + } + return maple_sega_purupuru::dma(cmd); + } +}; + +void createDreamConnDevices(DreamConn& dreamconn) +{ + const int bus = dreamconn.getBus(); + if (dreamconn.hasVmu() && dynamic_cast(MapleDevices[bus][0]) == nullptr) + { + delete MapleDevices[bus][0]; + DreamConnVmu *dev = new DreamConnVmu(dreamconn); + dev->Setup((bus << 6) | 1); + dev->config = new MapleConfigMap(dev); + dev->OnSetup(); + MapleDevices[bus][0] = dev; + } + if (dreamconn.hasRumble() && dynamic_cast(MapleDevices[bus][1]) == nullptr) + { + delete MapleDevices[bus][1]; + DreamConnPurupuru *dev = new DreamConnPurupuru(dreamconn); + dev->Setup((bus << 6) | 2); + dev->config = new MapleConfigMap(dev); + dev->OnSetup(); + MapleDevices[bus][1] = dev; + } +} + +#endif diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp new file mode 100644 index 000000000..34a36adad --- /dev/null +++ b/core/sdl/dreamconn.cpp @@ -0,0 +1,127 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#include "dreamconn.h" + +#if defined(_WIN32) && !defined(TARGET_UWP) +#include "hw/maple/maple_devs.h" + +void createDreamConnDevices(DreamConn& dreamconn); + +struct MapleMsg +{ + u8 command; + u8 destAP; + u8 originAP; + u8 size; + u8 data[1024]; + + u32 getDataSize() const { + return size * 4; + } + + template + void setData(const T& p) { + memcpy(data, &p, sizeof(T)); + this->size = (sizeof(T) + 3) / 4; + } + + bool send(sock_t sock) { + u32 sz = getDataSize() + 4; + return ::write(sock, this, sz) == sz; + } + bool receive(sock_t sock) + { + if (::read(sock, this, 4) != 4) + return false; + if (getDataSize() == 0) + return true; + return ::read(sock, data, getDataSize()) == getDataSize(); + } +}; +static_assert(sizeof(MapleMsg) == 1028); + +void DreamConn::connect() +{ + sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (!VALID(sock)) + return; + set_recv_timeout(sock, 1000); + sockaddr_in addr {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(BASE_PORT + bus); + if (::connect(sock, (sockaddr *)&addr, sizeof(addr)) != 0) + { + WARN_LOG(INPUT, "DreamConn[%d] connection failed", bus); + disconnect(); + return; + } + // Now get the controller configuration + MapleMsg msg; + msg.command = MDCF_GetCondition; + msg.destAP = (bus << 6) | 0x20; + msg.originAP = bus << 6; + msg.setData(MFID_0_Input); + if (!msg.send(sock)) + { + WARN_LOG(INPUT, "DreamConn[%d] communication failed", bus); + disconnect(); + return; + } + if (!msg.receive(sock)) { + WARN_LOG(INPUT, "DreamConn[%d] read timeout", bus); + disconnect(); + return; + } + expansionDevs = msg.originAP & 0x1f; + NOTICE_LOG(INPUT, "Connected to DreamConn[%d]: VMU:%d, Rumble Pack:%d", bus, hasVmu(), hasRumble()); + + EventManager::listen(Event::Resume, handleEvent, this); +} + +void DreamConn::disconnect() +{ + EventManager::unlisten(Event::Resume, handleEvent, this); + if (VALID(sock)) { + NOTICE_LOG(INPUT, "Disconnected from DreamConn[%d]", bus); + closesocket(sock); + } + sock = INVALID_SOCKET; +} + +bool DreamConn::send(const u8* data, int size) +{ + if (VALID(sock)) + return write(sock, data, size) == size; + else + return false; +} + +void DreamConn::handleEvent(Event event, void *arg) { + createDreamConnDevices(*static_cast(arg)); +} + +#else + +void DreamConn::connect() { +} +void DreamConn::disconnect() { +} + +#endif diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h new file mode 100644 index 000000000..60bd56cf8 --- /dev/null +++ b/core/sdl/dreamconn.h @@ -0,0 +1,56 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#pragma once +#include "types.h" +#include "network/net_platform.h" +#include "emulator.h" + +// TODO Need a way to detect DreamConn+ controllers +class DreamConn +{ + const int bus; + sock_t sock = INVALID_SOCKET; + u8 expansionDevs = 0; + static constexpr u16 BASE_PORT = 37393; + +public: + DreamConn(int bus) : bus(bus) { + connect(); + } + ~DreamConn() { + disconnect(); + } + + bool send(const u8* data, int size); + + int getBus() const { + return bus; + } + bool hasVmu() { + return expansionDevs & 1; + } + bool hasRumble() { + return expansionDevs & 2; + } + +private: + void connect(); + void disconnect(); + static void handleEvent(Event event, void *arg); +}; diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index 9027dfa25..7f77e452f 100644 --- a/core/sdl/sdl.cpp +++ b/core/sdl/sdl.cpp @@ -30,6 +30,7 @@ #include "nswitch.h" #include "switch_gamepad.h" #endif +#include "dreamconn.h" #include static SDL_Window* window = NULL; @@ -48,6 +49,7 @@ static bool mouseCaptured; static std::string clipboardText; static std::string barcode; static u64 lastBarcodeTime; +static std::unique_ptr dreamconns[4]; static KeyboardLayout detectKeyboardLayout(); static bool handleBarcodeScanner(const SDL_Event& event); @@ -260,10 +262,18 @@ void input_sdl_init() if (settings.input.keyboardLangId == KeyboardLayout::US) settings.input.keyboardLangId = detectKeyboardLayout(); barcode.clear(); + for (unsigned i = 0; i < std::size(dreamconns); i++) + { + std::string key = "DreamConn" + std::to_string(i); + if (cfgLoadBool("input", key.c_str(), false)) + dreamconns[i] = std::make_unique(i); + } } void input_sdl_quit() { + for (auto& dc : dreamconns) + dc.reset(); EventManager::unlisten(Event::Terminate, emuEventCallback); EventManager::unlisten(Event::Pause, emuEventCallback); EventManager::unlisten(Event::Resume, emuEventCallback); From dfd4dbebc49a7d5b6be8b52b8991aca24581e5fb Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 16 Dec 2024 17:34:38 +0100 Subject: [PATCH 68/81] fix windows build --- core/sdl/dreamconn.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 34a36adad..4318e1b3f 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -43,15 +43,15 @@ struct MapleMsg bool send(sock_t sock) { u32 sz = getDataSize() + 4; - return ::write(sock, this, sz) == sz; + return ::send(sock, this, sz, 0) == sz; } bool receive(sock_t sock) { - if (::read(sock, this, 4) != 4) + if (::recv(sock, this, 4, 0) != 4) return false; if (getDataSize() == 0) return true; - return ::read(sock, data, getDataSize()) == getDataSize(); + return ::recv(sock, data, getDataSize(), 0) == getDataSize(); } }; static_assert(sizeof(MapleMsg) == 1028); @@ -108,7 +108,7 @@ void DreamConn::disconnect() bool DreamConn::send(const u8* data, int size) { if (VALID(sock)) - return write(sock, data, size) == size; + return ::send(sock, data, size, 0) == size; else return false; } From 7fee0d2d29fb62ade8549b17758c0236ed2538e7 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 16 Dec 2024 17:54:02 +0100 Subject: [PATCH 69/81] cheat to work around fur fighters hanging Root issue seems to be TCNT0 returning the same value twice, which breaks the game calculation of the game speed/frame rate. Issue #145 --- core/cheats.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/cheats.cpp b/core/cheats.cpp index bf43ccbd4..999be2002 100644 --- a/core/cheats.cpp +++ b/core/cheats.cpp @@ -481,6 +481,15 @@ void CheatManager::reset(const std::string& gameId) else if (gameId == "SAMURAI SPIRITS 6" || gameId == "T0002M") { cheats.emplace_back(Cheat::Type::setValue, "fix depth", true, 16, 0x0003e602, 0x0009, true); // nop (shift by 8 bits instead of 10) } + else if (gameId == "T-8107N") { // Fur Fighters (US) + // force logging on to use more cycles + cheats.emplace_back(Cheat::Type::setValue, "enable logging", true, 32, 0x00314248, 1, true); + } + else if (gameId == "T-8113D-50") { // Fur Fighters (EU) + // force logging on to use more cycles + cheats.emplace_back(Cheat::Type::setValue, "enable logging", true, 32, 0x00314228, 1, true); + } + if (cheats.size() > cheatCount) setActive(true); } From 6c3d77b012b99f9fa79c36c29263e138fdbf55f1 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 16 Dec 2024 18:14:21 +0100 Subject: [PATCH 70/81] more windows build fix --- core/sdl/dreamconn.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 4318e1b3f..1747f2d64 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -43,15 +43,15 @@ struct MapleMsg bool send(sock_t sock) { u32 sz = getDataSize() + 4; - return ::send(sock, this, sz, 0) == sz; + return ::send(sock, (const char *)this, sz, 0) == sz; } bool receive(sock_t sock) { - if (::recv(sock, this, 4, 0) != 4) + if (::recv(sock, (char *)this, 4, 0) != 4) return false; if (getDataSize() == 0) return true; - return ::recv(sock, data, getDataSize(), 0) == getDataSize(); + return ::recv(sock, (char *)data, getDataSize(), 0) == getDataSize(); } }; static_assert(sizeof(MapleMsg) == 1028); @@ -108,7 +108,7 @@ void DreamConn::disconnect() bool DreamConn::send(const u8* data, int size) { if (VALID(sock)) - return ::send(sock, data, size, 0) == size; + return ::send(sock, (const char *)data, size, 0) == size; else return false; } From b9fdd5070bdbc68628450a39593b1ba85fa904c1 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 20 Dec 2024 20:02:58 +0100 Subject: [PATCH 71/81] dreamconn: detect dreamconn+ with VID/PID. Send maple data as text. Create DreamConn gamepad when detected. Send/receive maple data in ascii dump form. Simplify maple device creation. Issue #1305 --- core/hw/maple/maple_cfg.cpp | 5 +- core/hw/maple/maple_devs.cpp | 97 +++++++++++++++++--------- core/hw/maple/maple_devs.h | 4 +- core/hw/maple/maple_helper.cpp | 9 --- core/hw/maple/maple_helper.h | 15 ---- core/hw/maple/maple_if.cpp | 12 +++- core/sdl/dreamconn.cpp | 123 +++++++++++++++++++++++---------- core/sdl/dreamconn.h | 41 ++++++++++- core/sdl/sdl.cpp | 16 ++--- core/sdl/sdl_gamepad.h | 2 - 10 files changed, 209 insertions(+), 115 deletions(-) diff --git a/core/hw/maple/maple_cfg.cpp b/core/hw/maple/maple_cfg.cpp index 4cc0f51ab..72245b9ec 100644 --- a/core/hw/maple/maple_cfg.cpp +++ b/core/hw/maple/maple_cfg.cpp @@ -215,10 +215,7 @@ static void mcfg_Create(MapleDeviceType type, u32 bus, u32 port, s32 player_num { delete MapleDevices[bus][port]; maple_device* dev = maple_Create(type); - dev->Setup(maple_GetAddress(bus, port), player_num); - dev->config = new MapleConfigMap(dev); - dev->OnSetup(); - MapleDevices[bus][port] = dev; + dev->Setup(bus, port, player_num); } static void createNaomiDevices() diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index b0cfcf84b..ed3f210bc 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -32,15 +32,19 @@ const char* maple_densha_controller_name = "TAITO 001 Controller"; const char* maple_sega_brand = "Produced By or Under License From SEGA ENTERPRISES,LTD."; //fill in the info -void maple_device::Setup(u32 port, int playerNum) +void maple_device::Setup(u32 bus, u32 port, int playerNum) { - maple_port = port; - bus_port = maple_GetPort(port); - bus_id = maple_GetBusId(port); + maple_port = (bus << 6) | (1 << port); + bus_port = port; + bus_id = bus; logical_port[0] = 'A' + bus_id; logical_port[1] = bus_port == 5 ? 'x' : '1' + bus_port; logical_port[2] = 0; player_num = playerNum == -1 ? bus_id : playerNum; + + config = new MapleConfigMap(this); + OnSetup(); + MapleDevices[bus][port] = this; } maple_device::~maple_device() { @@ -683,7 +687,7 @@ struct maple_sega_vmu: maple_base case MFID_2_LCD: { DEBUG_LOG(MAPLE, "VMU %s LCD write", logical_port); - r32(); + r32(); // PT, phase, block# rptr(lcd_data,192); u8 white=0xff,black=0x00; @@ -1719,7 +1723,7 @@ struct RFIDReaderWriter : maple_base u32 resp = Dma(command, &buffer_in[1], buffer_in_len - 4, &buffer_out[1], outlen); if (reci & 0x20) - reci |= maple_GetAttachedDevices(maple_GetBusId(reci)); + reci |= maple_GetAttachedDevices(bus_id); verify(u8(outlen / 4) * 4 == outlen); buffer_out[0] = (resp << 0 ) | (reci << 8) | (send << 16) | ((outlen / 4) << 24); @@ -2107,63 +2111,94 @@ maple_device* maple_Create(MapleDeviceType type) struct DreamConnVmu : public maple_sega_vmu { - DreamConn& dreamconn; + std::shared_ptr dreamconn; - DreamConnVmu(DreamConn& dreamconn) : dreamconn(dreamconn) { + DreamConnVmu(std::shared_ptr dreamconn) : dreamconn(dreamconn) { } u32 dma(u32 cmd) override { if (cmd == MDCF_BlockWrite && *(u32 *)dma_buffer_in == MFID_2_LCD) + { // send the raw maple msg - dreamconn.send(dma_buffer_in - 4, dma_count_in + 4); + const MapleMsg *msg = reinterpret_cast(dma_buffer_in - 4); + dreamconn->send(*msg); + } return maple_sega_vmu::dma(cmd); } + + void copy(maple_sega_vmu *other) + { + memcpy(flash_data, other->flash_data, sizeof(flash_data)); + memcpy(lcd_data, other->lcd_data, sizeof(lcd_data)); + memcpy(lcd_data_decoded, other->lcd_data_decoded, sizeof(lcd_data_decoded)); + fullSaveNeeded = other->fullSaveNeeded; + } + + void updateScreen() + { + MapleMsg msg; + msg.command = MDCF_BlockWrite; + msg.destAP = maple_port; + msg.originAP = bus_id << 6; + msg.size = 2 + sizeof(lcd_data) / 4; + *(u32 *)&msg.data[0] = MFID_2_LCD; + *(u32 *)&msg.data[4] = 0; // PT, phase, block# + memcpy(&msg.data[8], lcd_data, sizeof(lcd_data)); + dreamconn->send(msg); + } }; struct DreamConnPurupuru : public maple_sega_purupuru { - DreamConn& dreamconn; + std::shared_ptr dreamconn; - DreamConnPurupuru(DreamConn& dreamconn) : dreamconn(dreamconn) { + DreamConnPurupuru(std::shared_ptr dreamconn) : dreamconn(dreamconn) { } u32 dma(u32 cmd) override { + const MapleMsg *msg = reinterpret_cast(dma_buffer_in - 4); switch (cmd) { case MDCF_BlockWrite: - dreamconn.send(dma_buffer_in - 4, dma_count_in + 4); + dreamconn->send(*msg); break; - case MDCF_SetCondition: - dreamconn.send(dma_buffer_in - 4, dma_count_in + 4); + dreamconn->send(*msg); break; } return maple_sega_purupuru::dma(cmd); } }; -void createDreamConnDevices(DreamConn& dreamconn) +void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart) { - const int bus = dreamconn.getBus(); - if (dreamconn.hasVmu() && dynamic_cast(MapleDevices[bus][0]) == nullptr) + const int bus = dreamconn->getBus(); + if (dreamconn->hasVmu()) { - delete MapleDevices[bus][0]; - DreamConnVmu *dev = new DreamConnVmu(dreamconn); - dev->Setup((bus << 6) | 1); - dev->config = new MapleConfigMap(dev); - dev->OnSetup(); - MapleDevices[bus][0] = dev; - } - if (dreamconn.hasRumble() && dynamic_cast(MapleDevices[bus][1]) == nullptr) + maple_device *dev = MapleDevices[bus][0]; + if (gameStart || (dev != nullptr && dev->get_device_type() == MDT_SegaVMU)) + { + DreamConnVmu *vmu = new DreamConnVmu(dreamconn); + vmu->Setup(bus, 0); + if (!gameStart) { + // if loading a state, copy data from the regular vmu and send a screen update + vmu->copy(static_cast(dev)); + vmu->updateScreen(); + } + delete dev; + } + } + if (dreamconn->hasRumble()) { - delete MapleDevices[bus][1]; - DreamConnPurupuru *dev = new DreamConnPurupuru(dreamconn); - dev->Setup((bus << 6) | 2); - dev->config = new MapleConfigMap(dev); - dev->OnSetup(); - MapleDevices[bus][1] = dev; + maple_device *dev = MapleDevices[bus][1]; + if (gameStart || (dev != nullptr && dev->get_device_type() == MDT_PurupuruPack)) + { + delete dev; + DreamConnPurupuru *rumble = new DreamConnPurupuru(dreamconn); + rumble->Setup(bus, 1); + } } } diff --git a/core/hw/maple/maple_devs.h b/core/hw/maple/maple_devs.h index 7f97e5af4..89f1e5836 100755 --- a/core/hw/maple/maple_devs.h +++ b/core/hw/maple/maple_devs.h @@ -131,7 +131,7 @@ struct maple_device MapleConfigMap* config; //fill in the info - void Setup(u32 port, int playerNum = -1); + void Setup(u32 bus, u32 port = 5, int playerNum = -1); virtual void OnSetup() {}; virtual ~maple_device(); @@ -246,7 +246,7 @@ struct maple_base: maple_device u32 resp = Dma(command, &buffer_in[1], buffer_in_len - 4, &buffer_out[1], outlen); if (reci & 0x20) - reci |= maple_GetAttachedDevices(maple_GetBusId(reci)); + reci |= maple_GetAttachedDevices(bus_id); verify(u8(outlen / 4) * 4 == outlen); buffer_out[0] = (resp << 0 ) | (send << 8) | (reci << 16) | ((outlen / 4) << 24); diff --git a/core/hw/maple/maple_helper.cpp b/core/hw/maple/maple_helper.cpp index 03c509103..6e683d972 100644 --- a/core/hw/maple/maple_helper.cpp +++ b/core/hw/maple/maple_helper.cpp @@ -1,15 +1,6 @@ #include "maple_helper.h" #include "maple_if.h" -u32 maple_GetPort(u32 addr) -{ - for (int i=0;i<6;i++) - { - if ((1<> 6; -} - -u32 maple_GetPort(u32 addr); u32 maple_GetAttachedDevices(u32 bus); - -//device : 0 .. 4 -> subdevice , 5 -> main device :) -static inline u32 maple_GetAddress(u32 bus, u32 port) -{ - u32 rv = bus << 6; - rv |= 1 << port; - - return rv; -} diff --git a/core/hw/maple/maple_if.cpp b/core/hw/maple/maple_if.cpp index 168edeb35..e446057a9 100644 --- a/core/hw/maple/maple_if.cpp +++ b/core/hw/maple/maple_if.cpp @@ -124,6 +124,14 @@ static void maple_SB_MDSTAR_Write(u32 addr, u32 data) } #endif +static u32 getPort(u32 addr) +{ + for (int i = 0; i < 6; i++) + if ((1 << i) & addr) + return i; + return 5; +} + static void maple_DoDma() { verify(SB_MDEN & 1); @@ -202,8 +210,8 @@ static void maple_DoDma() //Number of additional words in frame u32 inlen = (frame_header >> 24) & 0xFF; - u32 port = maple_GetPort(reci); - u32 bus = maple_GetBusId(reci); + u32 port = getPort(reci); + u32 bus = reci >> 6; if (MapleDevices[bus][5] && MapleDevices[bus][port]) { diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 1747f2d64..6d944a462 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -20,41 +20,42 @@ #if defined(_WIN32) && !defined(TARGET_UWP) #include "hw/maple/maple_devs.h" +#include +#include +#include -void createDreamConnDevices(DreamConn& dreamconn); +void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); -struct MapleMsg +bool MapleMsg::send(sock_t sock) const { - u8 command; - u8 destAP; - u8 originAP; - u8 size; - u8 data[1024]; - - u32 getDataSize() const { - return size * 4; - } - - template - void setData(const T& p) { - memcpy(data, &p, sizeof(T)); - this->size = (sizeof(T) + 3) / 4; - } + std::ostringstream out; + out.fill('0'); + out << std::hex << std::uppercase + << std::setw(2) << (u32)command << " " + << std::setw(2) << (u32)destAP << " " + << std::setw(2) << (u32)originAP << " " + << std::setw(2) << (u32)size; + const u32 sz = getDataSize(); + for (u32 i = 0; i < sz; i++) + out << " " << std::setw(2) << (u32)data[i]; + out << "\r\n"; + std::string s = out.str(); + return ::send(sock, s.c_str(), s.length(), 0) == (int)s.length(); +} - bool send(sock_t sock) { - u32 sz = getDataSize() + 4; - return ::send(sock, (const char *)this, sz, 0) == sz; - } - bool receive(sock_t sock) - { - if (::recv(sock, (char *)this, 4, 0) != 4) - return false; - if (getDataSize() == 0) - return true; - return ::recv(sock, (char *)data, getDataSize(), 0) == getDataSize(); - } -}; -static_assert(sizeof(MapleMsg) == 1028); +bool MapleMsg::receive(sock_t sock) +{ + std::string str(11, ' '); + if (::recv(sock, (char *)str.data(), str.length(), 0) != (int)str.length()) + return false; + sscanf(str.c_str(), "%hhx %hhx %hhx %hhx", &command, &destAP, &originAP, &size); + str = std::string(getDataSize() * 3 + 2, ' '); + if (::recv(sock, (char *)str.data(), str.length(), 0) != (int)str.length()) + return false; + for (unsigned i = 0; i < getDataSize(); i++) + sscanf(&str[i * 3 + 1], "%hhx", &data[i]); + return true; +} void DreamConn::connect() { @@ -91,13 +92,10 @@ void DreamConn::connect() } expansionDevs = msg.originAP & 0x1f; NOTICE_LOG(INPUT, "Connected to DreamConn[%d]: VMU:%d, Rumble Pack:%d", bus, hasVmu(), hasRumble()); - - EventManager::listen(Event::Resume, handleEvent, this); } void DreamConn::disconnect() { - EventManager::unlisten(Event::Resume, handleEvent, this); if (VALID(sock)) { NOTICE_LOG(INPUT, "Disconnected from DreamConn[%d]", bus); closesocket(sock); @@ -105,16 +103,54 @@ void DreamConn::disconnect() sock = INVALID_SOCKET; } -bool DreamConn::send(const u8* data, int size) +bool DreamConn::send(const MapleMsg& msg) { if (VALID(sock)) - return ::send(sock, (const char *)data, size, 0) == size; + return msg.send(sock); else return false; } -void DreamConn::handleEvent(Event event, void *arg) { - createDreamConnDevices(*static_cast(arg)); +bool DreamConnGamepad::isDreamConn(int deviceIndex) +{ + char guid_str[33] {}; + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(deviceIndex), guid_str, sizeof(guid_str)); + NOTICE_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str, + guid_str[10], guid_str[11], guid_str[8], guid_str[9], + guid_str[18], guid_str[19], guid_str[16], guid_str[17]); + // DreamConn VID:4457 PID:4443 + return memcmp("5744000043440000", guid_str + 8, 16) == 0; +} + +DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick) + : SDLGamepad(maple_port, joystick_idx, sdl_joystick) +{ + EventManager::listen(Event::Start, handleEvent, this); + EventManager::listen(Event::LoadState, handleEvent, this); +} + +DreamConnGamepad::~DreamConnGamepad() { + EventManager::unlisten(Event::Start, handleEvent, this); + EventManager::unlisten(Event::LoadState, handleEvent, this); +} + +void DreamConnGamepad::set_maple_port(int port) +{ + if (port < 0 || port >= 4) { + dreamconn.reset(); + } + else if (dreamconn == nullptr || dreamconn->getBus() != port) { + dreamconn.reset(); + dreamconn = std::make_shared(port); + } + SDLGamepad::set_maple_port(port); +} + +void DreamConnGamepad::handleEvent(Event event, void *arg) +{ + DreamConnGamepad *gamepad = static_cast(arg); + if (gamepad->dreamconn != nullptr) + createDreamConnDevices(gamepad->dreamconn, event == Event::Start); } #else @@ -124,4 +160,15 @@ void DreamConn::connect() { void DreamConn::disconnect() { } +bool DreamConnGamepad::isDreamConn(int deviceIndex) { + return false; +} +DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick) + : SDLGamepad(maple_port, joystick_idx, sdl_joystick) { +} +DreamConnGamepad::~DreamConnGamepad() { +} +void DreamConnGamepad::set_maple_port(int port) { + SDLGamepad::set_maple_port(port); +} #endif diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index 60bd56cf8..f74e88d4d 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -20,8 +20,31 @@ #include "types.h" #include "network/net_platform.h" #include "emulator.h" +#include "sdl_gamepad.h" + +struct MapleMsg +{ + u8 command; + u8 destAP; + u8 originAP; + u8 size; + u8 data[1024]; + + u32 getDataSize() const { + return size * 4; + } + + template + void setData(const T& p) { + memcpy(data, &p, sizeof(T)); + this->size = (sizeof(T) + 3) / 4; + } + + bool send(sock_t sock) const; + bool receive(sock_t sock); +}; +static_assert(sizeof(MapleMsg) == 1028); -// TODO Need a way to detect DreamConn+ controllers class DreamConn { const int bus; @@ -37,7 +60,7 @@ class DreamConn disconnect(); } - bool send(const u8* data, int size); + bool send(const MapleMsg& msg); int getBus() const { return bus; @@ -52,5 +75,19 @@ class DreamConn private: void connect(); void disconnect(); +}; + +class DreamConnGamepad : public SDLGamepad +{ +public: + DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick); + ~DreamConnGamepad(); + + void set_maple_port(int port) override; + static bool isDreamConn(int deviceIndex); + +private: static void handleEvent(Event event, void *arg); + + std::shared_ptr dreamconn; }; diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index 7f77e452f..a93c19e3c 100644 --- a/core/sdl/sdl.cpp +++ b/core/sdl/sdl.cpp @@ -39,6 +39,7 @@ static u32 windowFlags; #define WINDOW_WIDTH 640 #define WINDOW_HEIGHT 480 +std::map> SDLGamepad::sdl_gamepads; static std::unordered_map> sdl_mice; static std::shared_ptr sdl_keyboard; static bool window_fullscreen; @@ -49,7 +50,6 @@ static bool mouseCaptured; static std::string clipboardText; static std::string barcode; static u64 lastBarcodeTime; -static std::unique_ptr dreamconns[4]; static KeyboardLayout detectKeyboardLayout(); static bool handleBarcodeScanner(const SDL_Event& event); @@ -82,7 +82,11 @@ static void sdl_open_joystick(int index) #ifdef __SWITCH__ std::shared_ptr gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); #else - std::shared_ptr gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); + std::shared_ptr gamepad; + if (DreamConnGamepad::isDreamConn(index)) + gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); + else + gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); #endif SDLGamepad::AddSDLGamepad(gamepad); } catch (const FlycastException& e) { @@ -262,18 +266,10 @@ void input_sdl_init() if (settings.input.keyboardLangId == KeyboardLayout::US) settings.input.keyboardLangId = detectKeyboardLayout(); barcode.clear(); - for (unsigned i = 0; i < std::size(dreamconns); i++) - { - std::string key = "DreamConn" + std::to_string(i); - if (cfgLoadBool("input", key.c_str(), false)) - dreamconns[i] = std::make_unique(i); - } } void input_sdl_quit() { - for (auto& dc : dreamconns) - dc.reset(); EventManager::unlisten(Event::Terminate, emuEventCallback); EventManager::unlisten(Event::Pause, emuEventCallback); EventManager::unlisten(Event::Resume, emuEventCallback); diff --git a/core/sdl/sdl_gamepad.h b/core/sdl/sdl_gamepad.h index 76a0d47a0..46bbd0b45 100644 --- a/core/sdl/sdl_gamepad.h +++ b/core/sdl/sdl_gamepad.h @@ -676,8 +676,6 @@ class SDLGamepad : public GamepadDevice int damperEffectId = -1; }; -std::map> SDLGamepad::sdl_gamepads; - class SDLMouse : public Mouse { public: From 50a10598e46a1ab0dbd46c58ed2022001c9d4fb8 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 26 Dec 2024 11:33:26 +0100 Subject: [PATCH 72/81] picotcp: Resolved memory leak upstream https://github.com/tass-belgium/picotcp/commit/83527c69d3f69ba2b049b827c4f80c0633dce390 --- core/deps/picotcp/modules/pico_ethernet.c | 9 +++++---- core/deps/picotcp/stack/pico_device.c | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/core/deps/picotcp/modules/pico_ethernet.c b/core/deps/picotcp/modules/pico_ethernet.c index cc86e8562..9e246daa8 100644 --- a/core/deps/picotcp/modules/pico_ethernet.c +++ b/core/deps/picotcp/modules/pico_ethernet.c @@ -295,7 +295,7 @@ static int pico_ethernet_ipv6_dst(struct pico_frame *f, struct pico_eth *const d /* Ethernet send, first attempt: try our own address. * Returns 0 if the packet is not for us. - * Returns 1 if the packet is cloned to our own receive queue, so the caller can discard the original frame. + * Returns 1 if the packet is cloned to our own receive queue and the original frame is dicarded. * */ static int32_t pico_ethsend_local(struct pico_frame *f, struct pico_eth_hdr *hdr) { @@ -308,7 +308,9 @@ static int32_t pico_ethsend_local(struct pico_frame *f, struct pico_eth_hdr *hdr dbg("sending out packet destined for our own mac\n"); if (pico_ethernet_receive(clone) < 0) { dbg("pico_ethernet_receive() failed\n"); + return 0; } + pico_frame_discard(f); return 1; } @@ -317,13 +319,12 @@ static int32_t pico_ethsend_local(struct pico_frame *f, struct pico_eth_hdr *hdr /* Ethernet send, second attempt: try bcast. * Returns 0 if the packet is not bcast, so it will be handled somewhere else. - * Returns 1 if the packet is handled by the pico_device_broadcast() function, so it can be discarded. + * Returns 1 if the packet is handled by the pico_device_broadcast() function and is discarded. * */ static int32_t pico_ethsend_bcast(struct pico_frame *f) { if (IS_LIMITED_BCAST(f)) { - (void)pico_device_broadcast(f); /* We can discard broadcast even if it's not sent. */ - return 1; + return (pico_device_broadcast(f) > 0); // Return 1 on success, ret > 0 } return 0; diff --git a/core/deps/picotcp/stack/pico_device.c b/core/deps/picotcp/stack/pico_device.c index e0fb8d1c6..ccd2f928b 100644 --- a/core/deps/picotcp/stack/pico_device.c +++ b/core/deps/picotcp/stack/pico_device.c @@ -451,6 +451,7 @@ int32_t pico_device_broadcast(struct pico_frame *f) { struct pico_tree_node *index; int32_t ret = -1; + int sent = 0; pico_tree_foreach(index, &Device_tree) { @@ -463,14 +464,28 @@ int32_t pico_device_broadcast(struct pico_frame *f) break; copy->dev = dev; - copy->dev->send(copy->dev, copy->start, (int)copy->len); + ret = copy->dev->send(copy->dev, copy->start, (int)copy->len); + /* FIXME: If a device driver returns zero (which means the device + * driver is currently busy) there is no means to retry the + * broadcast operation later. */ pico_frame_discard(copy); } else { ret = f->dev->send(f->dev, f->start, (int)f->len); + /* FIXME: If a device driver returns zero (which means the device + * driver is currently busy) there is no means to retry the + * broadcast operation later. */ + } + + /* FIXME: If at least one device driver was able to sent the frame on + * the wire, the broadcast operation will be considered successful. */ + if (ret > 0) { + sent = 1; } } + ret = sent ? f->len : -1; + pico_frame_discard(f); return ret; } From cfd3677746fff63c7d8599551d6664233bc9a6dd Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 26 Dec 2024 11:45:58 +0100 Subject: [PATCH 73/81] picotcp: add socket event before socket is deleted Socket callback is called with PICO_SOCK_EV_DEL just before the socket is deleted. After returning from the callback, the socket pointer is no longer valid. --- core/deps/picotcp/include/pico_socket.h | 1 + core/deps/picotcp/stack/pico_socket.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/core/deps/picotcp/include/pico_socket.h b/core/deps/picotcp/include/pico_socket.h index 1c70610f1..9ca446e16 100644 --- a/core/deps/picotcp/include/pico_socket.h +++ b/core/deps/picotcp/include/pico_socket.h @@ -170,6 +170,7 @@ struct pico_ip_mreq_source { #define PICO_SOCK_EV_CLOSE 8u #define PICO_SOCK_EV_FIN 0x10u #define PICO_SOCK_EV_ERR 0x80u +#define PICO_SOCK_EV_DEL 0x100u struct pico_msginfo { struct pico_device *dev; diff --git a/core/deps/picotcp/stack/pico_socket.c b/core/deps/picotcp/stack/pico_socket.c index d5a8adf68..60e0a814e 100644 --- a/core/deps/picotcp/stack/pico_socket.c +++ b/core/deps/picotcp/stack/pico_socket.c @@ -473,6 +473,8 @@ static void socket_garbage_collect(pico_time now, void *arg) struct pico_socket *s = (struct pico_socket *) arg; IGNORE_PARAMETER(now); + if (s->wakeup) + s->wakeup(PICO_SOCK_EV_DEL, s); socket_clean_queues(s); PICO_FREE(s); } From f0aa1abf895e5d249c123d499fdaabd28018ab99 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 26 Dec 2024 12:03:09 +0100 Subject: [PATCH 74/81] picotcp: Fixed bug in tcpopt_len_check (rolling back index). upstream https://github.com/virtualsquare/picotcp/commit/f569ed8fb66630fa4a4dc59259253767da67b23a --- core/deps/picotcp/modules/pico_tcp.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/deps/picotcp/modules/pico_tcp.c b/core/deps/picotcp/modules/pico_tcp.c index 3a06599e0..b16b92f33 100644 --- a/core/deps/picotcp/modules/pico_tcp.c +++ b/core/deps/picotcp/modules/pico_tcp.c @@ -826,6 +826,8 @@ static void tcp_rcv_sack(struct pico_socket_tcp *t, uint8_t *opt, int len) static int tcpopt_len_check(uint32_t *idx, uint8_t len, uint8_t expected) { if (len != expected) { + if (len < 2) + return -1; *idx = *idx + len - 2; return -1; } From 6853238c8b6ed7ae6d1bbcbb465e5316ac86e0b2 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 26 Dec 2024 12:12:48 +0100 Subject: [PATCH 75/81] picotcp: Discard incoming TCP segments with zero-len options upstream https://github.com/virtualsquare/picotcp/commit/7aa288510a033691c3f09e2c43e10f07e24434b7 --- core/deps/picotcp/modules/pico_tcp.c | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/core/deps/picotcp/modules/pico_tcp.c b/core/deps/picotcp/modules/pico_tcp.c index b16b92f33..f7b7f7866 100644 --- a/core/deps/picotcp/modules/pico_tcp.c +++ b/core/deps/picotcp/modules/pico_tcp.c @@ -882,7 +882,7 @@ static inline void tcp_parse_option_timestamp(struct pico_socket_tcp *t, struct t->ts_nxt = long_be(tsval); } -static void tcp_parse_options(struct pico_frame *f) +static int tcp_parse_options(struct pico_frame *f) { struct pico_socket_tcp *t = (struct pico_socket_tcp *)f->sock; uint8_t *opt = f->transport_hdr + PICO_SIZE_TCPHDR; @@ -899,6 +899,10 @@ static void tcp_parse_options(struct pico_frame *f) if (f->payload && ((opt + i) > f->payload)) break; + if (len == 0) { + return -1; + } + tcp_dbg_options("Received option '%d', len = %d \n", type, len); switch (type) { case PICO_TCP_OPTION_NOOP: @@ -926,6 +930,7 @@ static void tcp_parse_options(struct pico_frame *f) i = i + len - 2; } } + return 0; } static inline void tcp_send_add_tcpflags(struct pico_socket_tcp *ts, struct pico_frame *f) @@ -1757,7 +1762,8 @@ static int tcp_data_in(struct pico_socket *s, struct pico_frame *f) (void)hdr; if (((hdr->len & 0xf0u) >> 2u) <= f->transport_len) { - tcp_parse_options(f); + if (tcp_parse_options(f) < 0) + return -1; f->payload = f->transport_hdr + ((hdr->len & 0xf0u) >> 2u); f->payload_len = payload_len; tcp_dbg("TCP> Received segment. (exp: %x got: %x)\n", t->rcv_nxt, SEQN(f)); @@ -2132,7 +2138,8 @@ static int tcp_ack(struct pico_socket *s, struct pico_frame *f) tcp_ack_dbg(s, f); #endif - tcp_parse_options(f); + if (tcp_parse_options(f) < 0) + return -1; t->recv_wnd = short_be(hdr->rwnd); acked = (uint16_t)tcp_ack_advance_una(t, f, &acked_timestamp); @@ -2448,7 +2455,8 @@ static int tcp_syn(struct pico_socket *s, struct pico_frame *f) f->sock = &new->sock; mtu = (uint16_t)pico_socket_get_mss(&new->sock); new->mss = (uint16_t)(mtu - PICO_SIZE_TCPHDR); - tcp_parse_options(f); + if (tcp_parse_options(f) < 0) + return -1; new->tcpq_in.max_size = PICO_DEFAULT_SOCKETQ; new->tcpq_out.max_size = PICO_DEFAULT_SOCKETQ; new->tcpq_hold.max_size = 2u * mtu; @@ -2866,6 +2874,11 @@ int pico_tcp_input(struct pico_socket *s, struct pico_frame *f) // tcp_dbg("[sam] TCP> s->state >> 8 = %u\n", s->state >> 8); tcp_dbg("[tcp input] socket: %p state: %d <-- local port:%u remote port: %u seq: 0x%08x ack: 0x%08x flags: 0x%02x t_len: %u, hdr: %u payload: %d\n", s, s->state >> 8, short_be(hdr->trans.dport), short_be(hdr->trans.sport), SEQN(f), ACKN(f), hdr->flags, f->transport_len, (hdr->len & 0xf0) >> 2, f->payload_len ); + if ((f->payload + f->payload_len) > (f->buffer + f->buffer_len)) { + tcp_dbg("TCP> Invalid payload len %04x\n", f->payload_len); + pico_frame_discard(f); + return -1; + } /* This copy of the frame has the current socket as owner */ f->sock = s; s->timestamp = TCP_TIME; From 59e844db15e85b201ce42ad33d29a2da65352eae Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 26 Dec 2024 12:38:26 +0100 Subject: [PATCH 76/81] picotcp: Integer Overflow in pico icmp4 upstream https://github.com/virtualsquare/picotcp/commit/cc923d2a1221d2066dc9270fb86c5c43635ce06e --- core/deps/picotcp/modules/pico_icmp4.c | 4 ++++ core/deps/picotcp/modules/pico_icmp4.h | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/deps/picotcp/modules/pico_icmp4.c b/core/deps/picotcp/modules/pico_icmp4.c index 25c75b68f..e2250ae25 100644 --- a/core/deps/picotcp/modules/pico_icmp4.c +++ b/core/deps/picotcp/modules/pico_icmp4.c @@ -242,6 +242,10 @@ static int8_t pico_icmp4_send_echo(struct pico_icmp4_ping_cookie *cookie) if (!dev) return -1; + // prevent overflow + if (cookie->size > PICO_ICMP_MAXCOOKIE) + return -1; + echo = pico_proto_ipv4.alloc(&pico_proto_ipv4, dev, (uint16_t)(PICO_ICMPHDR_UN_SIZE + cookie->size)); if (!echo) return -1; diff --git a/core/deps/picotcp/modules/pico_icmp4.h b/core/deps/picotcp/modules/pico_icmp4.h index 1d677fb6c..965b9551e 100644 --- a/core/deps/picotcp/modules/pico_icmp4.h +++ b/core/deps/picotcp/modules/pico_icmp4.h @@ -92,7 +92,7 @@ PACKED_STRUCT_DEF pico_icmp4_hdr { #define PICO_ICMP_MASKREPLY 18 #define PICO_ICMP_MAXTYPE 18 - +#define PICO_ICMP_MAXCOOKIE 65528 #define PICO_ICMP_UNREACH_NET 0 #define PICO_ICMP_UNREACH_HOST 1 From 1d290560e0c927a904233a5bc8c82a377492bcbe Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 26 Dec 2024 13:04:07 +0100 Subject: [PATCH 77/81] picotcp: accept PSH flag in SYN+ACK packets Windows CE adds the PSH flag to SYN+ACK packets, which isn't normally valid, so just ignore the flag. See dcgamespy filter in dreampi. --- core/deps/picotcp/modules/pico_tcp.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/deps/picotcp/modules/pico_tcp.c b/core/deps/picotcp/modules/pico_tcp.c index f7b7f7866..b03037d46 100644 --- a/core/deps/picotcp/modules/pico_tcp.c +++ b/core/deps/picotcp/modules/pico_tcp.c @@ -2803,7 +2803,7 @@ static uint8_t invalid_flags(struct pico_socket *s, uint8_t flags) { /* PICO_SOCKET_STATE_TCP_UNDEF */ 0, }, { /* PICO_SOCKET_STATE_TCP_CLOSED */ 0, }, { /* PICO_SOCKET_STATE_TCP_LISTEN */ PICO_TCP_SYN, PICO_TCP_SYN | PICO_TCP_PSH }, - { /* PICO_SOCKET_STATE_TCP_SYN_SENT */ PICO_TCP_SYNACK, PICO_TCP_RST, PICO_TCP_RSTACK}, + { /* PICO_SOCKET_STATE_TCP_SYN_SENT */ PICO_TCP_SYNACK, PICO_TCP_RST, PICO_TCP_RSTACK, PICO_TCP_SYNACK | PICO_TCP_PSH }, { /* PICO_SOCKET_STATE_TCP_SYN_RECV */ PICO_TCP_SYN, PICO_TCP_ACK, PICO_TCP_PSH, PICO_TCP_PSHACK, PICO_TCP_FINACK, PICO_TCP_FINPSHACK, PICO_TCP_RST}, { /* PICO_SOCKET_STATE_TCP_ESTABLISHED*/ PICO_TCP_SYN, PICO_TCP_SYNACK, PICO_TCP_ACK, PICO_TCP_PSH, PICO_TCP_PSHACK, PICO_TCP_FIN, PICO_TCP_FINACK, PICO_TCP_FINPSHACK, PICO_TCP_RST, PICO_TCP_RSTACK}, { /* PICO_SOCKET_STATE_TCP_CLOSE_WAIT */ PICO_TCP_SYNACK, PICO_TCP_ACK, PICO_TCP_PSH, PICO_TCP_PSHACK, PICO_TCP_FIN, PICO_TCP_FINACK, PICO_TCP_FINPSHACK, PICO_TCP_RST}, @@ -2889,7 +2889,9 @@ int pico_tcp_input(struct pico_socket *s, struct pico_frame *f) } else if (flags == PICO_TCP_SYN || flags == (PICO_TCP_SYN | PICO_TCP_PSH)) { tcp_action_call(action->syn, s, f); - } else if (flags == (PICO_TCP_SYN | PICO_TCP_ACK)) { + } else if (flags == (PICO_TCP_SYN | PICO_TCP_ACK) + // Windows CE / DirectPlay sets the PSH flag on SYN|ACK packets, which is normally invalid + || flags == (PICO_TCP_SYN | PICO_TCP_ACK | PICO_TCP_PSH)) { tcp_action_call(action->synack, s, f); } else { ret = tcp_action_by_flags(action, s, f, flags); From 9402981b43af5d72bb4d79c637ff354cce1e3ab1 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 27 Dec 2024 18:39:13 +0100 Subject: [PATCH 78/81] net: add asio. use it for the gdb agent --- .gitmodules | 3 + CMakeLists.txt | 2 +- core/debug/gdb_server.cpp | 748 +++++++++++++++++++------------------- core/deps/asio | 1 + core/util/shared_this.h | 38 ++ 5 files changed, 417 insertions(+), 375 deletions(-) create mode 160000 core/deps/asio create mode 100644 core/util/shared_this.h diff --git a/.gitmodules b/.gitmodules index 14fe5f2ac..fbec754c6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -41,3 +41,6 @@ [submodule "core/deps/googletest"] path = core/deps/googletest url = https://github.com/google/googletest.git +[submodule "core/deps/asio"] + path = core/deps/asio + url = https://github.com/chriskohlhoff/asio.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 76f9e082e..8dd949e61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -265,7 +265,7 @@ if(NOT "${SENTRY_UPLOAD_URL}" STREQUAL "") target_compile_definitions(${PROJECT_NAME} PRIVATE SENTRY_UPLOAD="${SENTRY_UPLOAD_URL}") endif() -target_include_directories(${PROJECT_NAME} PRIVATE core core/deps core/deps/stb core/deps/json) +target_include_directories(${PROJECT_NAME} PRIVATE core core/deps core/deps/stb core/deps/json core/deps/asio/asio/include) if(LIBRETRO) target_include_directories(${PROJECT_NAME} PRIVATE shell/libretro) endif() diff --git a/core/debug/gdb_server.cpp b/core/debug/gdb_server.cpp index 4141a7d20..35bd947b0 100644 --- a/core/debug/gdb_server.cpp +++ b/core/debug/gdb_server.cpp @@ -21,85 +21,168 @@ #ifdef GDB_SERVER #include "gdb_server.h" #include "debug_agent.h" -#include "network/net_platform.h" #include "cfg/option.h" #include "oslib/oslib.h" +#include "util/shared_this.h" +#include #include #include #include -#include #include - -#define MAX_PACKET_LEN 4096 +#include namespace debugger { -static void emuEventCallback(Event event, void *); +constexpr u32 MAX_PACKET_LEN = 4096; -class GdbServer +static u8 unpack(char c) +{ + c = std::tolower(c); + if (c <= '9') + return c - '0'; + else + return c - 'a' + 10; +} + +u32 unpack(const char *s, int l) +{ + u32 r = 0; + for (int i = 0; i < l && *s != '\0'; i += 2, s += 2) { + r |= (unpack(s[0]) << 4 | unpack(s[1])) << (i * 4); + } + return r; +} + +class GdbServer; + +class Connection : public SharedThis { public: - struct Error : public std::runtime_error { - Error(const char *reason) : std::runtime_error(reason) {} - }; + asio::ip::tcp::socket& getSocket() { + return socket; + } - void init(int port) + void start() { + asio::async_read_until(socket, asio::dynamic_string_buffer(message, MAX_PACKET_LEN), packetMatcher, + std::bind(&Connection::handlePacket, shared_from_this(), + asio::placeholders::error, + asio::placeholders::bytes_transferred)); + } + +private: + Connection(GdbServer& server, asio::io_context& io_context) + : server(server), io_context(io_context), socket(io_context) { + } + + using iterator = asio::buffers_iterator; + + std::pair + static packetMatcher(iterator begin, iterator end) { - if (VALID(serverSocket)) - return; + if (begin == end) + return std::make_pair(begin, false); + iterator i = begin; + if (*i == '\03') + // break + return std::make_pair(i + 1, true); + if (*i != '$') { + // unexpected, or ack/nack ('+', '-') + return std::make_pair(i + 1, true); + } + ++i; + while (i != end && *i != '#') + ++i; + if (i + 3 <= end) + // 2 chars for CRC + return std::make_pair(i + 3, true); - serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (!VALID(serverSocket)) - throw Error("gdb: Cannot create server socket"); + return std::make_pair(begin, false); + } - int option = 1; - setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, (const char *)&option, sizeof(option)); + void handlePacket(const std::error_code& ec, size_t len); - struct sockaddr_in serveraddr; - memset(&serveraddr, 0, sizeof(serveraddr)); - serveraddr.sin_family = AF_INET; - serveraddr.sin_port = htons(port); + void send(const std::string& msg) + { + if (msg.empty()) + start(); + else + asio::async_write(socket, asio::buffer(msg), + std::bind(&Connection::writeDone, shared_from_this(), + asio::placeholders::error, + asio::placeholders::bytes_transferred)); - if (::bind(serverSocket, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) - { - closesocket(serverSocket); - throw Error("gdb: bind() failed"); - } - if (::listen(serverSocket, 5) < 0) - { - closesocket(serverSocket); - throw Error("gdb: listen() failed"); - } - EventManager::listen(Event::Resume, emuEventCallback); - EventManager::listen(Event::Terminate, emuEventCallback); + } + + void writeDone(const std::error_code& ec, size_t len) + { + if (ec) + WARN_LOG(COMMON, "Write error: %s", ec.message().c_str()); + else + start(); + } + + GdbServer& server; + asio::io_context& io_context; + asio::ip::tcp::socket socket; + std::string message; + friend super; +}; + +class TcpAcceptor +{ +public: + TcpAcceptor(GdbServer& server, asio::io_context& io_context, u16 port) + : server(server), io_context(io_context), + acceptor(asio::ip::tcp::acceptor(io_context, + asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port))) + { + asio::socket_base::reuse_address option(true); + acceptor.set_option(option); + start(); + } + +private: + void start() + { + Connection::Ptr newConnection = Connection::create(server, io_context); - initialised = true; + acceptor.async_accept(newConnection->getSocket(), + std::bind(&TcpAcceptor::handleAccept, this, newConnection, asio::placeholders::error)); + } + void handleAccept(Connection::Ptr newConnection, const std::error_code& error); + + GdbServer& server; + asio::io_context& io_context; + asio::ip::tcp::acceptor acceptor; +}; + +class GdbServer +{ +public: + struct Error : public std::runtime_error { + Error(const char *reason) : std::runtime_error(reason) {} + }; + + void init(int port) + { + this->port = port; + EventManager::listen(Event::Resume, emuEventCallback, this); + EventManager::listen(Event::Terminate, emuEventCallback, this); } void term() { - if (!initialised) - return; - EventManager::unlisten(Event::Resume, emuEventCallback); - EventManager::unlisten(Event::Terminate, emuEventCallback); + EventManager::unlisten(Event::Resume, emuEventCallback, this); + EventManager::unlisten(Event::Terminate, emuEventCallback, this); stop(); - if (VALID(clientSocket)) - { - closesocket(clientSocket); - clientSocket = INVALID_SOCKET; - } - if (VALID(serverSocket)) - { - closesocket(serverSocket); - serverSocket = INVALID_SOCKET; - } } void run() { - if (!initialised || thread.joinable()) + if (thread.joinable()) return; DEBUG_LOG(COMMON, "GdbServer starting"); + io_context = std::make_unique(); thread = std::thread(&GdbServer::serverThread, this); if (config::GDBWaitForConnection) { @@ -110,19 +193,18 @@ class GdbServer void stop() { - if (!initialised) - return; if (thread.joinable()) { DEBUG_LOG(COMMON, "GdbServer stopping"); agent.resetAgent(); - stopRequested = true; + io_context->stop(); thread.join(); + io_context.reset(); } } bool isRunning() const { - return initialised && thread.joinable(); + return thread.joinable(); } // called on the emu thread @@ -142,69 +224,19 @@ class GdbServer void serverThread() { ThreadName _("GdbServer"); - while (!stopRequested) + try { - fd_set fds; - FD_ZERO(&fds); - sock_t max_fd = serverSocket; - FD_SET(serverSocket, &fds); - if (VALID(clientSocket)) - { - max_fd = std::max(max_fd, clientSocket); - FD_SET(clientSocket, &fds); - } - timeval tv; - tv.tv_sec = 0; - tv.tv_usec = 100 * 1000; - if (::select(max_fd + 1, &fds, nullptr, nullptr, &tv) > 0) - { - if (FD_ISSET(serverSocket, &fds)) - { - try { - acceptClientConnection(); - } catch (const Error& e) { - ERROR_LOG(COMMON, "%s", e.what()); - closesocket(serverSocket); - serverSocket = INVALID_SOCKET; - break; - } - } - else if (FD_ISSET(clientSocket, &fds)) - { - readCommand(); - } - } + TcpAcceptor server(*this, *io_context, port); + io_context->run(); } - if (VALID(clientSocket)) + catch (const std::exception& e) { - closesocket(clientSocket); - clientSocket = INVALID_SOCKET; + ERROR_LOG(COMMON, "Gdb server exception: %s", e.what()); } attached = false; - stopRequested = false; - } - - void acceptClientConnection() - { - if (VALID(clientSocket)) - closesocket(clientSocket); - sockaddr_in src_addr{}; - socklen_t addr_len = sizeof(src_addr); - clientSocket = ::accept(serverSocket, (sockaddr *)&src_addr, &addr_len); - if (!VALID(clientSocket)) - { - if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK) - throw Error("accept failed"); - } - else - { - NOTICE_LOG(NETWORK, "gdb: client connection"); - attached = true; - agentInterrupt(); - } } - void readCommand() + std::string handleCommand(const std::string& packet) { try { if (postDebugTrapNeeded) @@ -216,21 +248,21 @@ class GdbServer throw Error(e.what()); } } - std::string packet = recvPacket(); if (packet.empty()) - return; + return ""; DEBUG_LOG(NETWORK, "gdb: recv %s", packet.c_str()); + std::vector replies; switch (packet[0]) { case '!': // Enable extended mode - sendPacket("OK"); + replies.push_back("OK"); break; case '?': // Sent when connection is first established to query the reason the target halted - reportException(); + replies.push_back(reportException()); break; case 'A': // Initialized argv[] array passed into program. not supported - sendPacket("E01"); + replies.push_back("E01"); break;; case 'b': // Change the serial line speed to baud. deprecated break; @@ -238,53 +270,59 @@ class GdbServer break; case 'c': // Continue at addr, which is the address to resume. // If addr is omitted, resume at current address - sendContinue(packet); + doContinue(packet); break; case 'C': // Continue with signal sig - sendContinue(packet); + doContinue(packet); break; case 'd': // Toggle debug flag. deprecated break; case 'D': // Detach GDB from the remote system - sendPacket("OK"); + replies.push_back("OK"); agent.detach(); break; case 'F': // File-I/O protocol extension not currently supported break;; case 'g': // Read general registers - readAllRegs(); + replies.push_back(readAllRegs()); break; case 'G': // Write general registers - writeAllRegs(packet); + replies.push_back(writeAllRegs(packet)); break; case 'H': // Set thread for subsequent operations - sendPacket("OK"); + replies.push_back("OK"); break; case 'i': // Step the remote target by a single clock cycle case 'I': // Signal, then cycle step // not supported - sendPacket(""); + replies.push_back(""); break; case 'k': // Kill request. Stop process/system agent.kill(); break; case 'm': // Read length addressable memory units - readMem(packet); + replies.push_back(readMem(packet)); break; case 'M': // Write length addressable memory units - writeMem(packet); + replies.push_back(writeMem(packet)); break; case 'p': // Read the value of register - readReg(packet); + replies.push_back(readReg(packet)); break; case 'P': // Write register - writeReg(packet); + replies.push_back(writeReg(packet)); break; case 'q': // General query packets - query(packet); + { + auto v = query(packet); + replies.insert(replies.end(), v.begin(), v.end()); + } break; case 'Q': // General set packets - set(packet); + { + auto v = set(packet); + replies.insert(replies.end(), v.begin(), v.end()); + } break; case 'r': // Reset the entire system. Deprecated (use 'R' instead) break; @@ -292,52 +330,78 @@ class GdbServer restart(); break; case 's': // Single step - step(EXCEPT_NONE); + replies.push_back(step(EXCEPT_NONE)); break; case 'S': // Step with signal - step(); + replies.push_back(step()); break; case 't': // Search backwards. unsupported break; case 'T': // Find out if the thread is alive - sendPacket("OK"); + replies.push_back("OK"); break; case 'v': // 'v' packets to control execution - vpacket(packet); + { + auto v = vpacket(packet); + replies.insert(replies.end(), v.begin(), v.end()); + } break; case 'X': // Write binary data to memory - writeMemBin(packet); + replies.push_back(writeMemBin(packet)); break; case 'z': // Remove a breakpoint/watchpoint. - removeMatchpoint(packet); + replies.push_back(removeMatchpoint(packet)); break; case 'Z': // Insert a breakpoint/watchpoint. - insertMatchpoint(packet); + replies.push_back(insertMatchpoint(packet)); break; case 3: - interrupt(); + replies.push_back(interrupt()); break; default: // Unknown commands are ignored WARN_LOG(COMMON, "Unknown gdb command: %s", packet.c_str()); break;; } + std::string data; + for (const std::string& pkt : replies) + { + data.push_back('$'); + u8 checksum = 0; + for (char c : pkt) + { + if (c == '$' || c == '#' || c == '*' || c == '}') + { + c ^= 0x20; + checksum += (u8)'}'; + data.push_back('}'); + } + checksum += (u8)c; + data.push_back(c); + } + data.push_back('#'); + char s[9]; + sprintf(s, "%02x", checksum); + data += s; + } + DEBUG_LOG(NETWORK, "gdb: sent %s", data.c_str()); + + return data; } catch (const Error& e) { ERROR_LOG(COMMON, "%s", e.what()); - closesocket(clientSocket); - clientSocket = INVALID_SOCKET; attached = false; + throw e; } } - void reportException() + std::string reportException() { char s[4]; sprintf(s, "S%02X", agent.currentException()); - sendPacket(s); + return s; } - void sendContinue(const std::string& pkt) + void doContinue(const std::string& pkt) { if (pkt[0] != 'c') { WARN_LOG(COMMON, "Continue with signal not supported"); @@ -359,35 +423,34 @@ class GdbServer } } - void readAllRegs() + std::string readAllRegs() { u32 *regs; int c = agent.readAllRegs(®s); std::string outpkt; for (int i = 0; i < c; i++) outpkt += pack(regs[i]); - sendPacket(outpkt); + return outpkt; } - void writeAllRegs(const std::string& pkt) + std::string writeAllRegs(const std::string& pkt) { std::vector regs; for (auto it = pkt.begin() + 1; it <= pkt.end() - 8; it += 8) regs.push_back(unpack(&*it, 8)); agent.writeAllRegs(regs); - sendPacket("OK"); + return "OK"; } - void readMem(const std::string& pkt) + std::string readMem(const std::string& pkt) { u32 addr; u32 len; if (sscanf(pkt.c_str(), "m%x,%x:", &addr, &len) != 2) { WARN_LOG(COMMON, "readMem: invalid packet %s", pkt.c_str()); - sendPacket("E01"); - return; + return "E01"; } const u8 *mem = agent.readMem(addr, len); std::string outpkt; @@ -397,18 +460,17 @@ class GdbServer sprintf(s,"%02x", mem[i]); outpkt += s; } - sendPacket(outpkt); + return outpkt; } - void writeMem(const std::string& pkt) + std::string writeMem(const std::string& pkt) { u32 addr; u32 len; if (sscanf(pkt.c_str(), "M%x,%x:", &addr, &len) != 2) { WARN_LOG(COMMON, "writeMem: invalid packet %s", pkt.c_str()); - sendPacket("E01"); - return; + return "E01"; } std::vector data(len); const char *p = &pkt[pkt.find(':')] + 1; @@ -419,18 +481,17 @@ class GdbServer data[i] = (u8)b; } agent.writeMem(addr, data); - sendPacket("OK"); + return "OK"; } - void writeMemBin(const std::string& pkt) + std::string writeMemBin(const std::string& pkt) { u32 addr; u32 len; if (sscanf(pkt.c_str(), "X%x,%x:", &addr, &len) != 2) { WARN_LOG(COMMON, "writeMemBin invalid command: %s", pkt.c_str()); - sendPacket("E01"); - return; + return "E01"; } const char *p = &pkt[pkt.find(':')] + 1; std::vector data; @@ -445,64 +506,62 @@ class GdbServer data.push_back(b); } agent.writeMem(addr, data); - sendPacket("OK"); + return "OK"; } - void readReg(const std::string& pkt) + std::string readReg(const std::string& pkt) { u32 regNum; if (sscanf(pkt.c_str(), "p%x", ®Num) != 1) { WARN_LOG(COMMON, "readReg: invalid packet %s", pkt.c_str()); - sendPacket("E01"); - return; + return "E01"; } u32 v = agent.readReg(regNum); - sendPacket(pack(v)); + return pack(v); } - void writeReg(const std::string& pkt) + std::string writeReg(const std::string& pkt) { u32 regNum; char vstr[9]; if (sscanf(pkt.c_str(), "P%x=%8s", ®Num, vstr) != 2) { WARN_LOG(COMMON, "writeReg: invalid packet %s", pkt.c_str()); - sendPacket("E01"); - return; + return "E01"; } agent.writeReg(regNum, unpack(vstr, 8)); - sendPacket("OK"); + return "OK"; } - void query(const std::string& pkt) + std::vector query(const std::string& pkt) { if (pkt == "qC") // Return the current thread ID. 0 is "any thread" - sendPacket("QC0.01"); + return { "QC0.01" }; else if (pkt.rfind("qCRC", 0) == 0) { WARN_LOG(COMMON, "CRC compute not supported %s", pkt.c_str()); - sendPacket("E01"); + return { "E01" }; } else if (pkt == "qfThreadInfo") // Obtain a list of all active thread IDs (first call) - sendPacket("m0"); + return { "m0" }; else if (pkt == "qsThreadInfo") // Obtain a list of all active thread IDs (subsequent calls -> 'l' == end of list) - sendPacket("l"); + return { "l" }; else if (pkt.rfind("qGetTLSAddr:", 0) == 0) // Fetch the address associated with thread local storage - sendPacket(""); + return { "" }; else if (pkt.rfind("qL", 0) == 0) // Obtain thread information. deprecated - sendPacket("qM001"); + return { "qM001" }; else if (pkt == "qOffsets") // Get section offsets. Not supported - sendPacket(""); + return { "" }; else if (pkt.rfind("qP", 0) == 0) // Returns information on thread. deprecated - sendPacket(""); + return { "" }; else if (pkt.rfind("qRcmd,", 0) == 0) { std::string customCmd; @@ -535,10 +594,10 @@ class GdbServer } } *r = 0; - sendPacket(reply); + return { reply }; } else - sendPacket(""); + return { "" }; } else if (pkt.rfind("qSupported", 0) == 0) { @@ -546,43 +605,43 @@ class GdbServer // and query the stub for features it supports char qsupported[128]; snprintf(qsupported, 128, "PacketSize=%i;vContSupported+", MAX_PACKET_LEN); - sendPacket(qsupported); + return { qsupported }; } else if (pkt.rfind("qSymbol:", 0) == 0) // Notify the target that GDB is prepared to serve symbol lookup requests - sendPacket("OK"); + return { "OK" }; else if (pkt.rfind("qThreadExtraInfo,", 0) == 0) { // Obtain from the target OS a printable string description of thread attributes char s[19]; sprintf(s, "%02x%02x%02x%02x%02x%02x%02x%02x%02x", 'R', 'u', 'n', 'n', 'a', 'b', 'l', 'e', 0); - sendPacket(std::string(s, 18)); + return { std::string(s, 18) }; } else if (pkt.rfind("qXfer:", 0) == 0) // Read uninterpreted bytes from the target’s special data area identified by the keyword object - sendPacket(""); + return { "" }; else if (pkt.rfind("qAttached", 0) == 0) // Return an indication of whether the remote server attached to an existing process // or created a new process - sendPacket("1"); // existing process + return { "1" }; // existing process else if (pkt.rfind("qTfV", 0) == 0) // request data about trace state variables - sendPacket(""); + return { "" }; else if (pkt.rfind("qTfP", 0) == 0) // request data about tracepoints - sendPacket(""); + return { "" }; else if (pkt.rfind("qTStatus", 0) == 0) // Ask the stub if there is a trace experiment running right now - sendPacket(""); - else - WARN_LOG(COMMON, "query not supported %s", pkt.c_str()); + return { "" }; + WARN_LOG(COMMON, "query not supported %s", pkt.c_str()); + return {}; } - void set(const std::string& pkt) + std::vector set(const std::string& pkt) { if (pkt.rfind("QPassSignals:", 0) == 0) // Passing signals not supported - sendPacket(""); + return { "" }; else if (pkt.rfind("QTDP", 0) == 0 || pkt.rfind("QFrame", 0) == 0 || pkt.rfind("QTStart", 0) == 0 @@ -590,210 +649,184 @@ class GdbServer || pkt.rfind("QTinit", 0) == 0 || pkt.rfind("QTro", 0) == 0) // No tracepoint feature supported - sendPacket(""); - else - WARN_LOG(COMMON, "set not supported %s", pkt.c_str()); + return { "" }; + WARN_LOG(COMMON, "set not supported %s", pkt.c_str()); + return {}; } - void vpacket(const std::string& pkt) + std::vector vpacket(const std::string& pkt) { if (pkt.rfind("vAttach;", 0) == 0) - sendPacket("S05"); + return { "S05" }; else if (pkt.rfind("vCont?", 0) == 0) // supported vCont actions - (c)ontinue, (C)ontinue with signal, (s)tep, (S)tep with signal, (r)ange-step - sendPacket("vCont;c;C;s;S;t;r"); + return { "vCont;c;C;s;S;t;r" }; else if (pkt.rfind("vCont", 0) == 0) { std::string vContCmd = pkt.substr(strlen("vCont;")); + std::vector replies; switch (vContCmd[0]) { case 'c': case 'C': - sendContinue(vContCmd); - break; + doContinue(vContCmd); + return {}; case 's': - step(EXCEPT_NONE); - break; + return { step(EXCEPT_NONE) }; case 'S': - step(); + replies.push_back(step()); + [[fallthrough]]; case 'r': { u32 from, to; if (sscanf(vContCmd.c_str(), "r%x,%x", &from, &to) == 2) { - stepRange(from, to); + auto v = stepRange(from, to); + replies.insert(replies.end(), v.begin(), v.end()); } else { WARN_LOG(COMMON, "Unsupported vCont:r format %s", pkt.c_str()); - sendContinue("c"); + doContinue("c"); } - - break; + return replies; } default: WARN_LOG(COMMON, "vCont action not supported %s", pkt.c_str()); + return {}; } } else if (pkt.rfind("vFile:", 0) == 0) // not supported - sendPacket(""); + return { "" }; else if (pkt.rfind("vFlashErase:", 0) == 0) // not supported - sendPacket("E01"); + return { "E01" }; else if (pkt.rfind("vFlashWrite:", 0) == 0) // not supported - sendPacket("E01"); + return { "E01" }; else if (pkt.rfind("vFlashDone:", 0) == 0) // not supported - sendPacket("E01"); + return { "E01" }; else if (pkt.rfind("vRun;", 0) == 0) { if (pkt != "vRun;") WARN_LOG(COMMON, "unexpected vRun args ignored: %s", pkt.c_str()); agent.restart(); - sendPacket("S05"); + return { "S05" }; } else if (pkt.rfind("vKill", 0) == 0) { - sendPacket("OK"); agent.kill(); + return { "OK" }; } else { WARN_LOG(COMMON, "unknown v packet: %s", pkt.c_str()); - sendPacket(""); + return { "" }; } } - void restart() - { + void restart() { agent.restart(); } - void step(u32 what = 0) + std::string step(u32 what = 0) { try { agent.step(); - sendPacket("S05"); + return "S05"; } catch (const FlycastException& e) { throw Error(e.what()); } } - void stepRange(u32 from, u32 to) + std::vector stepRange(u32 from, u32 to) { try { - sendPacket("OK"); agent.stepRange(from, to); - sendPacket("S05"); + return { "OK", "S05" }; } catch (const FlycastException& e) { throw Error(e.what()); } } - void insertMatchpoint(const std::string& pkt) + std::string insertMatchpoint(const std::string& pkt) { u32 type; u32 addr; u32 len; if (sscanf(pkt.c_str(), "Z%1d,%x,%1d", &type, &addr, &len) != 3) { WARN_LOG(COMMON, "insertMatchpoint: unknown packet: %s", pkt.c_str()); - sendPacket("E01"); + return "E01"; } switch (type) { case DebugAgent::Breakpoint::BP_TYPE_SOFTWARE_BREAK: // soft bp if (agent.insertMatchpoint(DebugAgent::Breakpoint::BP_TYPE_SOFTWARE_BREAK, addr, len)) - sendPacket("OK"); + return "OK"; else - sendPacket("E01"); + return "E01"; break; case DebugAgent::Breakpoint::BP_TYPE_HARDWARE_BREAK: // hardware bp - sendPacket(""); + return ""; break; case DebugAgent::Breakpoint::BP_TYPE_WRITE_WATCHPOINT: // write watchpoint - sendPacket(""); + return ""; break; case DebugAgent::Breakpoint::BP_TYPE_READ_WATCHPOINT: // read watchpoint - sendPacket(""); + return ""; break; case DebugAgent::Breakpoint::BP_TYPE_ACCESS_WATCHPOINT: // access watchpoint - sendPacket(""); + return ""; break; default: - sendPacket(""); + return ""; break; } } - void removeMatchpoint(const std::string& pkt) + std::string removeMatchpoint(const std::string& pkt) { u32 type; u32 addr; u32 len; if (sscanf(pkt.c_str(), "z%1d,%x,%1d", &type, &addr, &len) != 3) { WARN_LOG(COMMON, "removeMatchpoint: unknown packet: %s", pkt.c_str()); - sendPacket("E01"); + return "E01"; } switch (type) { case 0: // soft bp if (agent.removeMatchpoint(DebugAgent::Breakpoint::BP_TYPE_SOFTWARE_BREAK, addr, len)) - sendPacket("OK"); + return "OK"; else - sendPacket("E01"); + return "E01"; break; case 1: // hardware bp - sendPacket(""); + return ""; break; case 2: // write watchpoint - sendPacket(""); + return ""; break; case 3: // read watchpoint - sendPacket(""); + return ""; break; case 4: // access watchpoint - sendPacket(""); + return ""; break; default: - sendPacket(""); + return ""; break; } } - void interrupt() + std::string interrupt() { u32 signal = agentInterrupt(); char s[10]; sprintf(s, "S%02x", signal); - sendPacket(s); - } - - char recvChar() - { - char c; - int rc = ::recv(clientSocket, &c, 1, 0); - if (rc <= 0) - throw Error("gdb: I/O error"); - return c; - } - - void sendChar(char c) - { - std::unique_lock lock(outMutex); - int rc = ::send(clientSocket, &c, 1, 0); - if (rc <= 0) - throw Error("gdb: I/O error"); - } - - u8 unpack(char c) - { - c = std::tolower(c); - if (c <= '9') - return c - '0'; - else - return c - 'a' + 10; + return s; } char packnb(u8 b) @@ -811,122 +844,109 @@ class GdbServer return s; } - std::string pack(u32 v) - { + std::string pack(u32 v) { return packb(v & 0xff) + packb((v >> 8) & 0xff) + packb((v >> 16) & 0xff) + packb((v >> 24) & 0xff); } - u32 unpack(const char *s, int l) + u32 agentInterrupt() { - u32 r = 0; - for (int i = 0; i < l && *s != '\0'; i += 2, s += 2) - { - r |= (unpack(s[0]) << 4 | unpack(s[1])) << (i * 4); + try { + return agent.interrupt(); + } catch (const FlycastException& e) { + throw Error(e.what()); } - return r; } - std::string recvPacket() - { - std::string pkt; - // look for start character ('$') or BREAK - char c = recvChar(); - if (c == 3) - return std::string("\03"); - if (c != '$') - return pkt; - - // read until '#' - u8 checksum = 0; - while (!stopRequested) - { - c = recvChar(); - if (c == '$') - { - checksum = 0; - pkt.clear(); - - continue; - } - - if (c == '#') - break; - - checksum += (u8)c; - pkt.push_back(c); - } - if (stopRequested) - { - pkt.clear(); - return pkt; - } - u8 recvchk = unpack(recvChar()) << 4; - recvchk |= unpack(recvChar()); - - // If the checksums don't match print a warning, and put the - // negative ack back to the client. Otherwise put a positive ack. - if (checksum != recvchk) - { - sendChar('-'); // Failed checksum - return ""; - } - else - { - sendChar('+'); // Successful transfer - return pkt; - } + void clientConnected() { + attached = true; + agentInterrupt(); } - void sendPacket(const std::string& pkt) - { DEBUG_LOG(NETWORK, "gdb: sending pkt"); - std::unique_lock lock(outMutex); - std::string data{'$'}; - u8 checksum = 0; - for (char c : pkt) + static void emuEventCallback(Event event, void *arg) + { + GdbServer *gdbServer = static_cast(arg); + switch (event) { - if (c == '$' || c == '#' || c == '*' || c == '}') - { - c ^= 0x20; - checksum += (u8)'}'; - data.push_back('}'); + case Event::Resume: + try { + if (!gdbServer->isRunning()) + gdbServer->run(); + } catch (const GdbServer::Error& e) { + ERROR_LOG(COMMON, "%s", e.what()); } - checksum += (u8)c; - data.push_back(c); + break; + case Event::Terminate: + gdbServer->stop(); + break; + default: + break; } - data.push_back('#'); - char s[9]; - sprintf(s, "%02x", checksum); - data += s; - DEBUG_LOG(NETWORK, "gdb: sent %s", data.c_str()); - int ret = ::send(clientSocket, data.c_str(), data.length(), 0); - if (ret < (int)data.length()) - throw Error("I/O error"); } - u32 agentInterrupt() - { - try { - return agent.interrupt(); - } catch (const FlycastException& e) { - throw Error(e.what()); - } - } - - bool initialised = false; - bool stopRequested = false; bool attached = false; bool postDebugTrapNeeded = false; - sock_t serverSocket = INVALID_SOCKET; - sock_t clientSocket = INVALID_SOCKET; std::thread thread; - std::mutex outMutex; + std::unique_ptr io_context; + int port = DEFAULT_PORT; + friend class TcpAcceptor; + friend class Connection; + public: DebugAgent agent; }; static GdbServer gdbServer; +void TcpAcceptor::handleAccept(Connection::Ptr newConnection, const std::error_code& error) +{ + if (!error) { + server.clientConnected(); + newConnection->start(); + } + start(); +} + +void Connection::handlePacket(const std::error_code& ec, size_t len) +{ + std::string msg = message.substr(0, len); + message = message.substr(len); + if (ec || len == 0) + { + // terminate the connection + if (ec != asio::error::eof && ec != asio::error::operation_aborted) + WARN_LOG(NETWORK, "Read error %s", ec.message().c_str()); + return; + } + try { + if (msg[0] == '\03') { // break + send(server.handleCommand(msg)); + return; + } + if (msg[0] != '$') { + // Ignore unexpected chars + send(""); + return; + } + u8 cksum = 0; + for (unsigned i = 1; i < len - 3; i++) + cksum += (u8)msg[i]; + if (cksum != (unpack(msg[len - 2]) << 4 | unpack(msg[len - 1]))) { + // Invalid checksum + WARN_LOG(COMMON, "Connection::handlePacket: invalid checksum: [%s]", msg.c_str()); + send("-"); + } + else { + // Positive ack + std::string reply = "+"; + reply += server.handleCommand(msg.substr(1, msg.length() - 4)); + send(reply); + } + } catch (...) { + // terminate the connection + } +} + void init(int port) { gdbServer.init(port); @@ -952,25 +972,5 @@ void subroutineReturn() gdbServer.agent.subroutineReturn(); } -static void emuEventCallback(Event event, void *) -{ - switch (event) - { - case Event::Resume: - try { - if (!gdbServer.isRunning()) - gdbServer.run(); - } catch (const GdbServer::Error& e) { - ERROR_LOG(COMMON, "%s", e.what()); - } - break; - case Event::Terminate: - gdbServer.stop(); - break; - default: - break; - } -} - } #endif diff --git a/core/deps/asio b/core/deps/asio new file mode 160000 index 000000000..03ae834ed --- /dev/null +++ b/core/deps/asio @@ -0,0 +1 @@ +Subproject commit 03ae834edbace31a96157b89bf50e5ee464e5ef9 diff --git a/core/util/shared_this.h b/core/util/shared_this.h new file mode 100644 index 000000000..400f17c85 --- /dev/null +++ b/core/util/shared_this.h @@ -0,0 +1,38 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#pragma once +#include + +template +class SharedThis : public std::enable_shared_from_this +{ +public: + using Ptr = std::shared_ptr; + + template + static Ptr create(Args&&... args) { + return Ptr(new T(std::forward(args)...)); + } + +protected: + using super = SharedThis; + + SharedThis() { + } +}; From 553f77c675586ea8e8c5e0c715b4dc3f5653e5d8 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 27 Dec 2024 18:47:12 +0100 Subject: [PATCH 79/81] dreamconn: send buzzer messages. Rename controller. Use asio iostream Issue #1305 --- core/hw/maple/maple_devs.cpp | 22 ++++++------ core/sdl/dreamconn.cpp | 70 +++++++++++++++++------------------- core/sdl/dreamconn.h | 9 ++--- 3 files changed, 48 insertions(+), 53 deletions(-) diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index ed3f210bc..33e76b772 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -2118,11 +2118,15 @@ struct DreamConnVmu : public maple_sega_vmu u32 dma(u32 cmd) override { - if (cmd == MDCF_BlockWrite && *(u32 *)dma_buffer_in == MFID_2_LCD) + if (dma_count_in >= 4) { - // send the raw maple msg - const MapleMsg *msg = reinterpret_cast(dma_buffer_in - 4); - dreamconn->send(*msg); + const u32 functionId = *(u32 *)dma_buffer_in; + if ((cmd == MDCF_BlockWrite && functionId == MFID_2_LCD) // LCD screen + || (cmd == MDCF_SetCondition && functionId == MFID_3_Clock)) // Buzzer + { + const MapleMsg *msg = reinterpret_cast(dma_buffer_in - 4); + dreamconn->send(*msg); + } } return maple_sega_vmu::dma(cmd); } @@ -2158,15 +2162,9 @@ struct DreamConnPurupuru : public maple_sega_purupuru u32 dma(u32 cmd) override { - const MapleMsg *msg = reinterpret_cast(dma_buffer_in - 4); - switch (cmd) - { - case MDCF_BlockWrite: - dreamconn->send(*msg); - break; - case MDCF_SetCondition: + if (cmd == MDCF_BlockWrite || cmd == MDCF_SetCondition) { + const MapleMsg *msg = reinterpret_cast(dma_buffer_in - 4); dreamconn->send(*msg); - break; } return maple_sega_purupuru::dma(cmd); } diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 6d944a462..c54f0d654 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -20,102 +20,97 @@ #if defined(_WIN32) && !defined(TARGET_UWP) #include "hw/maple/maple_devs.h" +#include #include #include -#include void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); -bool MapleMsg::send(sock_t sock) const +bool MapleMsg::send(std::ostream& stream) const { - std::ostringstream out; - out.fill('0'); - out << std::hex << std::uppercase + stream.fill('0'); + stream << std::hex << std::uppercase << std::setw(2) << (u32)command << " " << std::setw(2) << (u32)destAP << " " << std::setw(2) << (u32)originAP << " " << std::setw(2) << (u32)size; const u32 sz = getDataSize(); for (u32 i = 0; i < sz; i++) - out << " " << std::setw(2) << (u32)data[i]; - out << "\r\n"; - std::string s = out.str(); - return ::send(sock, s.c_str(), s.length(), 0) == (int)s.length(); + stream << " " << std::setw(2) << (u32)data[i]; + stream << "\r\n"; + return !stream.fail(); } -bool MapleMsg::receive(sock_t sock) +bool MapleMsg::receive(std::istream& stream) { - std::string str(11, ' '); - if (::recv(sock, (char *)str.data(), str.length(), 0) != (int)str.length()) + std::string response; + if (!std::getline(stream, response)) return false; - sscanf(str.c_str(), "%hhx %hhx %hhx %hhx", &command, &destAP, &originAP, &size); - str = std::string(getDataSize() * 3 + 2, ' '); - if (::recv(sock, (char *)str.data(), str.length(), 0) != (int)str.length()) + sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &command, &destAP, &originAP, &size); + if ((getDataSize() - 1) * 3 + 13 >= response.length()) return false; for (unsigned i = 0; i < getDataSize(); i++) - sscanf(&str[i * 3 + 1], "%hhx", &data[i]); - return true; + sscanf(&response[i * 3 + 12], "%hhx", &data[i]); + return !stream.fail(); } void DreamConn::connect() { - sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (!VALID(sock)) - return; - set_recv_timeout(sock, 1000); - sockaddr_in addr {}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr.sin_port = htons(BASE_PORT + bus); - if (::connect(sock, (sockaddr *)&addr, sizeof(addr)) != 0) - { - WARN_LOG(INPUT, "DreamConn[%d] connection failed", bus); + iostream = asio::ip::tcp::iostream("localhost", std::to_string(BASE_PORT + bus)); + if (!iostream) { + WARN_LOG(INPUT, "DreamConn[%d] connection failed: %s", bus, iostream.error().message().c_str()); disconnect(); return; } + iostream.expires_from_now(std::chrono::seconds(1)); // Now get the controller configuration MapleMsg msg; msg.command = MDCF_GetCondition; msg.destAP = (bus << 6) | 0x20; msg.originAP = bus << 6; msg.setData(MFID_0_Input); - if (!msg.send(sock)) + if (!msg.send(iostream)) { WARN_LOG(INPUT, "DreamConn[%d] communication failed", bus); disconnect(); return; } - if (!msg.receive(sock)) { + if (!msg.receive(iostream)) { WARN_LOG(INPUT, "DreamConn[%d] read timeout", bus); disconnect(); return; } + iostream.expires_from_now(std::chrono::duration::max()); // don't use a 64-bit based duration to avoid overflow expansionDevs = msg.originAP & 0x1f; NOTICE_LOG(INPUT, "Connected to DreamConn[%d]: VMU:%d, Rumble Pack:%d", bus, hasVmu(), hasRumble()); + config::MapleExpansionDevices[bus][0] = hasVmu() ? MDT_SegaVMU : MDT_None; + config::MapleExpansionDevices[bus][1] = hasRumble() ? MDT_PurupuruPack : MDT_None; } void DreamConn::disconnect() { - if (VALID(sock)) { + if (iostream) { + iostream.close(); NOTICE_LOG(INPUT, "Disconnected from DreamConn[%d]", bus); - closesocket(sock); } - sock = INVALID_SOCKET; } bool DreamConn::send(const MapleMsg& msg) { - if (VALID(sock)) - return msg.send(sock); - else + if (!iostream) return false; + if (!msg.send(iostream)) { + WARN_LOG(INPUT, "DreamConn[%d] send failed: %s", bus, iostream.error().message().c_str()); + return false; + } + return true; } bool DreamConnGamepad::isDreamConn(int deviceIndex) { char guid_str[33] {}; SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(deviceIndex), guid_str, sizeof(guid_str)); - NOTICE_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str, + INFO_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str, guid_str[10], guid_str[11], guid_str[8], guid_str[9], guid_str[18], guid_str[19], guid_str[16], guid_str[17]); // DreamConn VID:4457 PID:4443 @@ -125,6 +120,7 @@ bool DreamConnGamepad::isDreamConn(int deviceIndex) DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick) : SDLGamepad(maple_port, joystick_idx, sdl_joystick) { + _name = "DreamConn+ Controller"; EventManager::listen(Event::Start, handleEvent, this); EventManager::listen(Event::LoadState, handleEvent, this); } diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index f74e88d4d..279eccb5b 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -18,9 +18,10 @@ */ #pragma once #include "types.h" -#include "network/net_platform.h" #include "emulator.h" #include "sdl_gamepad.h" +#include +#include struct MapleMsg { @@ -40,15 +41,15 @@ struct MapleMsg this->size = (sizeof(T) + 3) / 4; } - bool send(sock_t sock) const; - bool receive(sock_t sock); + bool send(std::ostream& stream) const; + bool receive(std::istream& stream); }; static_assert(sizeof(MapleMsg) == 1028); class DreamConn { const int bus; - sock_t sock = INVALID_SOCKET; + asio::ip::tcp::iostream iostream; u8 expansionDevs = 0; static constexpr u16 BASE_PORT = 37393; From 10aaec16193eba2d3ec1f5939dc2a91c3a4daa5b Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 28 Dec 2024 12:21:26 +0100 Subject: [PATCH 80/81] dreamconn: send data with socket to avoid fragmentation. Build fix Issue #1305 --- core/sdl/dreamconn.cpp | 47 ++++++++++++++++++++++++------------------ core/sdl/dreamconn.h | 9 ++++---- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index c54f0d654..7685ef26a 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -18,39 +18,46 @@ */ #include "dreamconn.h" -#if defined(_WIN32) && !defined(TARGET_UWP) +#ifdef USE_DREAMCONN #include "hw/maple/maple_devs.h" #include #include +#include #include +#include void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); -bool MapleMsg::send(std::ostream& stream) const +static bool sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream) { - stream.fill('0'); - stream << std::hex << std::uppercase - << std::setw(2) << (u32)command << " " - << std::setw(2) << (u32)destAP << " " - << std::setw(2) << (u32)originAP << " " - << std::setw(2) << (u32)size; - const u32 sz = getDataSize(); + std::ostringstream s; + s.fill('0'); + s << std::hex << std::uppercase + << std::setw(2) << (u32)msg.command << " " + << std::setw(2) << (u32)msg.destAP << " " + << std::setw(2) << (u32)msg.originAP << " " + << std::setw(2) << (u32)msg.size; + const u32 sz = msg.getDataSize(); for (u32 i = 0; i < sz; i++) - stream << " " << std::setw(2) << (u32)data[i]; - stream << "\r\n"; - return !stream.fail(); + s << " " << std::setw(2) << (u32)msg.data[i]; + s << "\r\n"; + + asio::ip::tcp::socket& sock = static_cast(stream.socket()); + asio::error_code ec; + asio::write(sock, asio::buffer(s.str()), ec); + return !ec; } -bool MapleMsg::receive(std::istream& stream) +static bool receiveMsg(MapleMsg& msg, std::istream& stream) { std::string response; if (!std::getline(stream, response)) return false; - sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &command, &destAP, &originAP, &size); - if ((getDataSize() - 1) * 3 + 13 >= response.length()) + sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); + if ((msg.getDataSize() - 1) * 3 + 13 >= response.length()) return false; - for (unsigned i = 0; i < getDataSize(); i++) - sscanf(&response[i * 3 + 12], "%hhx", &data[i]); + for (unsigned i = 0; i < msg.getDataSize(); i++) + sscanf(&response[i * 3 + 12], "%hhx", &msg.data[i]); return !stream.fail(); } @@ -69,13 +76,13 @@ void DreamConn::connect() msg.destAP = (bus << 6) | 0x20; msg.originAP = bus << 6; msg.setData(MFID_0_Input); - if (!msg.send(iostream)) + if (!sendMsg(msg, iostream)) { WARN_LOG(INPUT, "DreamConn[%d] communication failed", bus); disconnect(); return; } - if (!msg.receive(iostream)) { + if (!receiveMsg(msg, iostream)) { WARN_LOG(INPUT, "DreamConn[%d] read timeout", bus); disconnect(); return; @@ -99,7 +106,7 @@ bool DreamConn::send(const MapleMsg& msg) { if (!iostream) return false; - if (!msg.send(iostream)) { + if (!sendMsg(msg, iostream)) { WARN_LOG(INPUT, "DreamConn[%d] send failed: %s", bus, iostream.error().message().c_str()); return false; } diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index 279eccb5b..08ae9083b 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -20,8 +20,10 @@ #include "types.h" #include "emulator.h" #include "sdl_gamepad.h" +#if defined(_WIN32) && !defined(TARGET_UWP) +#define USE_DREAMCONN 1 #include -#include +#endif struct MapleMsg { @@ -40,16 +42,15 @@ struct MapleMsg memcpy(data, &p, sizeof(T)); this->size = (sizeof(T) + 3) / 4; } - - bool send(std::ostream& stream) const; - bool receive(std::istream& stream); }; static_assert(sizeof(MapleMsg) == 1028); class DreamConn { const int bus; +#ifdef USE_DREAMCONN asio::ip::tcp::iostream iostream; +#endif u8 expansionDevs = 0; static constexpr u16 BASE_PORT = 37393; From ee1a7167f609340f6f916b7811f1fd7a22608cf4 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 28 Dec 2024 15:39:11 +0100 Subject: [PATCH 81/81] dreamconn: open pause menu when LT+RT+Start is pressed Issue #1305 --- core/sdl/dreamconn.cpp | 48 ++++++++++++++++++++++++++++++++++++++++++ core/sdl/dreamconn.h | 6 ++++++ 2 files changed, 54 insertions(+) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 7685ef26a..c379042fb 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -20,6 +20,7 @@ #ifdef USE_DREAMCONN #include "hw/maple/maple_devs.h" +#include "ui/gui.h" #include #include #include @@ -156,6 +157,47 @@ void DreamConnGamepad::handleEvent(Event event, void *arg) createDreamConnDevices(gamepad->dreamconn, event == Event::Start); } +bool DreamConnGamepad::gamepad_btn_input(u32 code, bool pressed) +{ + if (!is_detecting_input() && input_mapper) + { + DreamcastKey key = input_mapper->get_button_id(0, code); + if (key == DC_BTN_START) { + startPressed = pressed; + checkKeyCombo(); + } + } + else { + startPressed = false; + } + return SDLGamepad::gamepad_btn_input(code, pressed); +} + +bool DreamConnGamepad::gamepad_axis_input(u32 code, int value) +{ + if (!is_detecting_input()) + { + if (code == leftTrigger) { + ltrigPressed = value > 0; + checkKeyCombo(); + } + else if (code == rightTrigger) { + rtrigPressed = value > 0; + checkKeyCombo(); + } + } + else { + ltrigPressed = false; + rtrigPressed = false; + } + return SDLGamepad::gamepad_axis_input(code, value); +} + +void DreamConnGamepad::checkKeyCombo() { + if (ltrigPressed && rtrigPressed && startPressed) + gui_open_settings(); +} + #else void DreamConn::connect() { @@ -174,4 +216,10 @@ DreamConnGamepad::~DreamConnGamepad() { void DreamConnGamepad::set_maple_port(int port) { SDLGamepad::set_maple_port(port); } +bool DreamConnGamepad::gamepad_btn_input(u32 code, bool pressed) { + return SDLGamepad::gamepad_btn_input(code, pressed); +} +bool DreamConnGamepad::gamepad_axis_input(u32 code, int value) { + return SDLGamepad::gamepad_axis_input(code, value); +} #endif diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index 08ae9083b..bf2e32680 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -86,10 +86,16 @@ class DreamConnGamepad : public SDLGamepad ~DreamConnGamepad(); void set_maple_port(int port) override; + bool gamepad_btn_input(u32 code, bool pressed) override; + bool gamepad_axis_input(u32 code, int value) override; static bool isDreamConn(int deviceIndex); private: static void handleEvent(Event event, void *arg); + void checkKeyCombo(); std::shared_ptr dreamconn; + bool ltrigPressed = false; + bool rtrigPressed = false; + bool startPressed = false; };