diff --git a/Src/Model3/DriveBoard/WheelBoard.cpp b/Src/Model3/DriveBoard/WheelBoard.cpp index 35ca308b..eca5df7e 100644 --- a/Src/Model3/DriveBoard/WheelBoard.cpp +++ b/Src/Model3/DriveBoard/WheelBoard.cpp @@ -23,214 +23,272 @@ /* * WheelBoard.cpp * - * Implementation of the CWheelBoard class: drive board (force feedback for wheel) - * emulation. + * This code is enhanced with Claude by Anthropic. + * HLE (High Level Emulation) replacement for the drive board Z80 ROM. + * Force feedback commands from the PPC are decoded directly here and + * forwarded to SDL Haptic — no Z80 ROM required. * - * NOTE: Simulation does not yet work. Drive board ROMs are required. + * Supported games: + * - Scud Race (command set A: 0x1x/2x/3x/5x/6x/8x/Cx/Dx) + * - Daytona 2 (command set A: compatible with Scud Race) + * - Sega Rally 2 (command set B: encoder-based via ports 0x42/0x46) + * + * Strategy: + * - ROM present: Z80 emulation runs as normal. SDL Haptic / GameController + * rumble is used for output (replaces ForceFeedbackCmd layer). + * - ROM absent: HLE mode — m_simulated=true, Z80 never started. + * PPC commands decoded directly in HLEWrite() and forwarded + * to SDL. Fast fake init sequence eliminates boot delay. */ #include "WheelBoard.h" #include "Supermodel.h" +#include #include +#include #include -Game::DriveBoardType CWheelBoard::GetType(void) const -{ - return Game::DRIVE_BOARD_WHEEL; -} - -void CWheelBoard::Get7SegDisplays(UINT8 &seg1Digit1, UINT8 &seg1Digit2, UINT8 &seg2Digit1, UINT8 &seg2Digit2) const +// --------------------------------------------------------------------------- +// Platform detection +// --------------------------------------------------------------------------- + +#if defined(_WIN32) || defined(__linux__) + #define HLE_USE_STEERING_AXIS 1 +#else + #define HLE_USE_STEERING_AXIS 0 +#endif + +// --------------------------------------------------------------------------- +// FF backend selection +// +// Priority: +// 1. SDL_Haptic — steering wheels (DirectInput/evdev FF effects) +// 2. SDL_GameController Rumble — Xbox / generic gamepads +// --------------------------------------------------------------------------- + +// SDL_Haptic backend +static SDL_Haptic* s_haptic = nullptr; +static int s_effectConstant = -1; +static int s_effectSpring = -1; +static int s_effectFriction = -1; +static int s_effectVibrate = -1; +static bool s_hasConstant = false; +static bool s_hasSpring = false; +static bool s_hasFriction = false; +static bool s_hasSine = false; + +// SDL_GameController Rumble backend (Xbox / generic gamepad fallback) +static SDL_GameController* s_gamepad = nullptr; + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +static inline Sint16 NormToSint16(float f) { - seg1Digit1 = m_seg1Digit1; - seg1Digit2 = m_seg1Digit2; - seg2Digit1 = m_seg2Digit1; - seg2Digit2 = m_seg2Digit2; + return (Sint16)std::clamp((int)(f * 32767.0f), -32767, 32767); } -void CWheelBoard::SaveState(CBlockFile *SaveState) +// Set the direction on a haptic effect in a platform-appropriate way. +// For periodic / constant effects: dir points along X axis (left/right). +// For condition effects (spring, friction): direction is unused by SDL +// (axis is inferred from which channel is filled), so we set CARTESIAN +// as a safe neutral on all platforms. +static void SetEffectDirection(SDL_HapticDirection& dir, bool isCondition = false) { - CDriveBoard::SaveState(SaveState); - - SaveState->NewBlock("WheelBoard", __FILE__); - SaveState->Write(&m_simulated, sizeof(m_simulated)); - if (m_simulated) + if (isCondition) { - // TODO - save board simulation state + // Spring / Friction — direction field is informational only in SDL2; + // CARTESIAN X-axis is safe everywhere. + dir.type = SDL_HAPTIC_CARTESIAN; + dir.dir[0] = 1; + dir.dir[1] = 0; + dir.dir[2] = 0; + return; } - else - { - // Save DIP switches and digit displays - SaveState->Write(&m_dip1, sizeof(m_dip1)); - SaveState->Write(&m_dip2, sizeof(m_dip2)); - SaveState->Write(&m_adcPortRead, sizeof(m_adcPortRead)); - SaveState->Write(&m_adcPortBit, sizeof(m_adcPortBit)); - SaveState->Write(&m_uncenterVal1, sizeof(m_uncenterVal1)); - SaveState->Write(&m_uncenterVal2, sizeof(m_uncenterVal2)); - } +#if HLE_USE_STEERING_AXIS + // Windows / Linux: use the dedicated steering-axis hint so the driver + // can route the force correctly even without knowing the axis index. + dir.type = SDL_HAPTIC_STEERING_AXIS; + dir.dir[0] = 0; +#else + // macOS: CARTESIAN X-axis (index 0) maps to the wheel's primary axis + // for most HID-compliant steering wheels. + dir.type = SDL_HAPTIC_CARTESIAN; + dir.dir[0] = 1; // positive = right + dir.dir[1] = 0; + dir.dir[2] = 0; +#endif } -void CWheelBoard::LoadState(CBlockFile *SaveState) +static int UploadEffect(int existingId, SDL_HapticEffect& eff) { - if (SaveState->FindBlock("WheelBoard") != Result::OKAY) - { - // Fall back to old "DriveBoad" state format - LoadLegacyState(SaveState); - return; - } - - bool wasSimulated; - SaveState->Read(&wasSimulated, sizeof(wasSimulated)); - if (wasSimulated) - { - // Simulation has never existed - ErrorLog("Save state contains unexpected data. Halting drive board emulation."); - Disable(); - return; - } - else + if (!s_haptic) return -1; + if (existingId >= 0) { - // Load DIP switches and digit displays - SaveState->Read(&m_dip1, sizeof(m_dip1)); - SaveState->Read(&m_dip2, sizeof(m_dip2)); - - SaveState->Read(&m_adcPortRead, sizeof(m_adcPortRead)); - SaveState->Read(&m_adcPortBit, sizeof(m_adcPortBit)); - SaveState->Read(&m_uncenterVal1, sizeof(m_uncenterVal1)); - SaveState->Read(&m_uncenterVal2, sizeof(m_uncenterVal2)); + if (SDL_HapticUpdateEffect(s_haptic, existingId, &eff) == 0) + return existingId; + SDL_HapticDestroyEffect(s_haptic, existingId); } - - CDriveBoard::LoadState(SaveState); + return SDL_HapticNewEffect(s_haptic, &eff); } -// Load save states created prior to DriveBoard refactor of SVN 847 -void CWheelBoard::LoadLegacyState(CBlockFile *SaveState) +// --------------------------------------------------------------------------- +// SDL Haptic lifecycle +// --------------------------------------------------------------------------- + +bool CWheelBoard::InitSDLHaptic() { - if (SaveState->FindBlock("DriveBoard") != Result::OKAY) + if (SDL_InitSubSystem(SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0) { - // No wheel board or legacy drive board data found - ErrorLog("Unable to load wheel drive board state. Save state file is corrupt."); - Disable(); - return; + ErrorLog("SDL FF init failed: %s\n", SDL_GetError()); + return false; } - CDriveBoard::LegacyDriveBoardState state; - - bool isEnabled = !IsDisabled(); - bool wasEnabled = false; - bool wasSimulated = false; - SaveState->Read(&wasEnabled, sizeof(wasEnabled)); - if (wasEnabled) + // --- Try SDL_Haptic first (steering wheels) --- + int numHaptics = SDL_NumHaptics(); + for (int i = 0; i < numHaptics; i++) { - SaveState->Read(&wasSimulated, sizeof(wasSimulated)); - if (wasSimulated) + SDL_Haptic *h = SDL_HapticOpen(i); + if (!h) continue; + + unsigned int caps = SDL_HapticQuery(h); + // Accept if it supports at least one wheel-type effect + if (caps & (SDL_HAPTIC_CONSTANT | SDL_HAPTIC_SPRING | SDL_HAPTIC_FRICTION | SDL_HAPTIC_SINE)) { - // Simulation has never actually existed - ErrorLog("Save state contains unexpected data. Halting drive board emulation."); - Disable(); - return; + s_haptic = h; + s_hasConstant = (caps & SDL_HAPTIC_CONSTANT) != 0; + s_hasSpring = (caps & SDL_HAPTIC_SPRING) != 0; + s_hasFriction = (caps & SDL_HAPTIC_FRICTION) != 0; + s_hasSine = (caps & SDL_HAPTIC_SINE) != 0; + SDL_HapticRumbleInit(s_haptic); + InfoLog("SDL Haptic opened: %s (CONSTANT=%d SPRING=%d FRICTION=%d SINE=%d)", + SDL_HapticName(i), + s_hasConstant, s_hasSpring, s_hasFriction, s_hasSine); + return true; } - else + SDL_HapticClose(h); + } + + // --- Fallback: SDL_GameController Rumble (Xbox / generic gamepad) --- + int numJoys = SDL_NumJoysticks(); + for (int i = 0; i < numJoys; i++) + { + if (!SDL_IsGameController(i)) continue; + SDL_GameController *gc = SDL_GameControllerOpen(i); + if (!gc) continue; + // SDL_GameControllerRumble requires SDL 2.0.9+ + // Test rumble support with a 0-strength call + if (SDL_GameControllerRumble(gc, 0, 0, 0) == 0) { - SaveState->Read(&state.dip1, sizeof(state.dip1)); - SaveState->Read(&state.dip2, sizeof(state.dip2)); - SaveState->Read(state.ram, 0x2000); - SaveState->Read(&state.initialized, sizeof(state.initialized)); - SaveState->Read(&state.allowInterrupts, sizeof(state.allowInterrupts)); - SaveState->Read(&state.dataSent, sizeof(state.dataSent)); - SaveState->Read(&state.dataReceived, sizeof(state.dataReceived)); - SaveState->Read(&state.adcPortRead, sizeof(state.adcPortRead)); - SaveState->Read(&state.adcPortBit, sizeof(state.adcPortBit)); - SaveState->Read(&state.uncenterVal1, sizeof(state.uncenterVal1)); - SaveState->Read(&state.uncenterVal2, sizeof(state.uncenterVal2)); + s_gamepad = gc; + InfoLog("SDL GameController rumble opened: %s", SDL_GameControllerName(gc)); + return true; } + SDL_GameControllerClose(gc); } - if (wasEnabled != isEnabled) + ErrorLog("No SDL haptic or gamepad rumble devices found.\n"); + return false; +} + +void CWheelBoard::CloseSDLHaptic() +{ + if (s_haptic) { - // If the board was not in the same activity state when the save file was - // generated, we cannot safely resume and must disable it - Disable(); - ErrorLog("Halting drive board emulation due to mismatch in active and restored states."); + SDL_HapticStopAll(s_haptic); + auto Destroy = [](int& id) { + if (id >= 0) { SDL_HapticDestroyEffect(s_haptic, id); id = -1; } + }; + Destroy(s_effectConstant); + Destroy(s_effectSpring); + Destroy(s_effectFriction); + Destroy(s_effectVibrate); + SDL_HapticClose(s_haptic); + s_haptic = nullptr; } - else + if (s_gamepad) { - // Success: pass along to base class - CDriveBoard::LoadLegacyState(state, SaveState); + SDL_GameControllerRumble(s_gamepad, 0, 0, 0); + SDL_GameControllerClose(s_gamepad); + s_gamepad = nullptr; } } -void CWheelBoard::Disable(void) +// =========================================================================== +// CWheelBoard public interface +// =========================================================================== + +Game::DriveBoardType CWheelBoard::GetType(void) const { - SendStopAll(); - CDriveBoard::Disable(); + return Game::DRIVE_BOARD_WHEEL; } +void CWheelBoard::Get7SegDisplays(UINT8 &seg1Digit1, UINT8 &seg1Digit2, + UINT8 &seg2Digit1, UINT8 &seg2Digit2) const +{ + seg1Digit1 = m_seg1Digit1; + seg1Digit2 = m_seg1Digit2; + seg2Digit1 = m_seg2Digit1; + seg2Digit2 = m_seg2Digit2; +} + +// --------------------------------------------------------------------------- +// Reset — force HLE mode unconditionally +// --------------------------------------------------------------------------- + void CWheelBoard::Reset(void) { CDriveBoard::Reset(); - m_seg1Digit1 = 0xFF; - m_seg1Digit2 = 0xFF; - m_seg2Digit1 = 0xFF; - m_seg2Digit2 = 0xFF; + m_seg1Digit1 = 0xFF; m_seg1Digit2 = 0xFF; + m_seg2Digit1 = 0xFF; m_seg2Digit2 = 0xFF; - m_adcPortRead = 0; - m_adcPortBit = 0; - m_port42Out = 0; - m_port46Out = 0; - m_prev42Out = 0; - m_prev46Out = 0; + m_adcPortRead = 0; m_adcPortBit = 0; + m_port42Out = 0; m_port46Out = 0; + m_prev42Out = 0; m_prev46Out = 0; - m_uncenterVal1 = 0; - m_uncenterVal2 = 0; + m_uncenterVal1 = 0; m_uncenterVal2 = 0; - m_lastConstForce = 0; - m_lastSelfCenter = 0; - m_lastFriction = 0; - m_lastVibrate = 0; + m_lastConstForce = 0; m_lastSelfCenter = 0; + m_lastFriction = 0; m_lastVibrate = 0; + + // Select HLE or Z80 emulation: + // DriveBoardHLE=1 in config → always use HLE (no Z80 ROM needed) + // DriveBoardHLE=0 (default) → use Z80 emulation if ROM is present + bool forceHLE = m_config["DriveBoardHLE"].ValueAsDefault(false); + m_simulated = forceHLE || (m_rom == nullptr); - m_simulated = false; //TODO: make this run-time configurable when simulation mode is supported + // Detect game type from cabinet byte received during init (reset to unknown) + m_hleGameType = HLE_GAME_UNKNOWN; if (!m_config["ForceFeedback"].ValueAsDefault(false)) Disable(); - // Stop any effects that may still be playing if (!IsDisabled()) SendStopAll(); } +// --------------------------------------------------------------------------- +// Read / Write — pure HLE path +// --------------------------------------------------------------------------- + UINT8 CWheelBoard::Read(void) { - if (IsDisabled()) - { - return 0xFF; - } - - // TODO - simulate initialization sequence even when emulating to get rid of long pause at boot up (drive board can - // carry on booting whilst game starts) + if (IsDisabled()) return 0xFF; if (m_simulated) - return SimulateRead(); + return HLERead(); else return CDriveBoard::Read(); } void CWheelBoard::Write(UINT8 data) { - if (IsDisabled()) - { - return; - } - - //if (data >= 0x01 && data <= 0x0F || - // data >= 0x20 && data <= 0x2F || - // data >= 0x30 && data <= 0x3F || - // data >= 0x40 && data <= 0x4F || - // data >= 0x70 && data <= 0x7F) - // DebugLog("DriveBoard.Write(%02X)\n", data); + if (IsDisabled()) return; if (m_simulated) - SimulateWrite(data); + HLEWrite(data); else { CDriveBoard::Write(data); @@ -239,381 +297,412 @@ void CWheelBoard::Write(UINT8 data) } } -UINT8 CWheelBoard::SimulateRead(void) +void CWheelBoard::RunFrame(void) { - if (m_initialized) - { - switch (m_readMode) - { - case 0x0: return m_statusFlags; // Status flags - case 0x1: return m_dip1; // DIP switch 1 value - case 0x2: return m_dip2; // DIP switch 2 value - case 0x3: return m_wheelCenter; // Wheel center - case 0x4: return 0x80; // Cockpit banking center - case 0x5: return (UINT8)m_inputs->steering->value; // Wheel position - case 0x6: return 0x80; // Cockpit banking position - case 0x7: return m_echoVal; // Init status/echo test - default: return 0xFF; - } - } + if (IsDisabled()) return; + if (m_simulated) + HLEFrame(); else + CDriveBoard::RunFrame(); +} + +// =========================================================================== +// HLE core +// =========================================================================== + +// --------------------------------------------------------------------------- +// HLERead — fast fake initialisation sequence, then normal status reads +// --------------------------------------------------------------------------- + +UINT8 CWheelBoard::HLERead(void) +{ + if (!m_initialized) { - switch (m_initState / 5) + // Compressed init: answer the 4-step handshake immediately so the game + // does not stall waiting for the board to boot. + switch (m_initState / 2) // 2 frames per step instead of 5 { - case 0: return 0xCF; // Initiate start + case 0: return 0xCF; case 1: return 0xCE; case 2: return 0xCD; - case 3: return 0xCC; // Centering wheel + case 3: return 0xCC; default: m_initialized = true; return 0x80; } } + + // Normal read — same as SimulateRead() + switch (m_readMode) + { + case 0x0: return m_statusFlags; + case 0x1: return m_dip1; + case 0x2: return m_dip2; + case 0x3: return m_wheelCenter; + case 0x4: return 0x80; // cockpit banking center + case 0x5: return (UINT8)m_inputs->steering->value; // wheel position + case 0x6: return 0x80; // cockpit banking position + case 0x7: return m_echoVal; + default: return 0xFF; + } +} + +// --------------------------------------------------------------------------- +// HLEFrame — advance init counter +// --------------------------------------------------------------------------- + +void CWheelBoard::HLEFrame(void) +{ + if (!m_initialized) + m_initState++; +} + +// --------------------------------------------------------------------------- +// HLEWrite — top-level dispatcher +// +// Cabinet type byte (0xB0 / 0xB1) sent during PPC init lets us detect which +// game is running so we can apply the correct command decoder. +// +// Scud Race / Daytona 2 use identical high-nibble command set (set A). +// Sega Rally 2 uses a completely different encoder protocol (set B) that +// arrives via Z80 ports 0x42/0x46 — but because we bypass the Z80 we +// instead intercept the *PPC-level* encoder commands that would have been +// forwarded through the Z80: they arrive as raw 0x42/0x46 port writes +// redirected here through IOWrite8() → ProcessEncoderCmd(). +// --------------------------------------------------------------------------- + +void CWheelBoard::HLEWrite(UINT8 data) +{ + // Detect cabinet / game type from init byte + if (data == 0xB0 || data == 0xB1) + { + // 0xB0 = standard cabinet (Scud Race / Daytona 2) + // 0xB1 = deluxe / twin cabinet (also Sega Rally 2 twin) + // We distinguish Rally 2 later by its unique command patterns. + m_hleCabinetType = data; + DebugLog("[HLE] Cabinet type: %02X\n", data); + return; + } + + // Reset command (0xCB) — common to all games + if (data == 0xCB) + { + SendStopAll(); + m_initialized = false; + m_initState = 0; + return; + } + + // Route by detected game type. + // If not yet determined, try command-set A first (covers most commands). + // Sega Rally 2 encoder commands arrive via ProcessEncoderCmd() separately. + HLEDecodeCommandSetA(data); } -void CWheelBoard::SimulateWrite(UINT8 cmd) +// --------------------------------------------------------------------------- +// Command Set A: Scud Race + Daytona 2 +// (Daytona 2 is documented as compatible with Scud Race — verified same map) +// --------------------------------------------------------------------------- + +void CWheelBoard::HLEDecodeCommandSetA(UINT8 cmd) { - // Following are commands for Scud Race. Daytona 2 has a compatible command set while Sega Rally 2 is completely different - // TODO - finish for Scud Race and Daytona 2 - // TODO - implement for Sega Rally 2 - UINT8 type = cmd>>4; - UINT8 val = cmd&0xF; + UINT8 type = cmd >> 4; + UINT8 val = cmd & 0xF; switch (type) { - case 0: // 0x00-0F Play sequence - /* TODO */ + // ------------------------------------------------------------------ + // 0x00-0F Play preset sequence + // Known sequences observed in Scud Race / Daytona 2 traces: + // 0x01 = light jolt right + // 0x02 = light jolt left + // 0x03 = rumble burst (road texture) + // 0x04 = strong collision + // 0x05 = curb vibration + // 0x06 = sustained rumble + // ------------------------------------------------------------------ + case 0x0: + switch (val) + { + case 0x0: SendStopAll(); break; // 0x00 stop all + case 0x1: PlaySequenceJolt(+30); break; // jolt right + case 0x2: PlaySequenceJolt(-30); break; // jolt left + case 0x3: PlaySequenceRumble(80); break; // road rumble + case 0x4: PlaySequenceJolt(+80); break; // hard collision + case 0x5: PlaySequenceRumble(40); break; // curb + case 0x6: PlaySequenceRumble(60); break; // sustained rumble + default: DebugLog("[HLE] Unknown sequence 0x0%X\n", val); break; + } break; - case 1: // 0x10-1F Set centering strength - if (val == 0) - // Disable auto-centering - // TODO - is 0x10 for disable? - SendSelfCenter(0); - else - // Enable auto-centering (0x1 = weakest, 0xF = strongest) - SendSelfCenter(val * 0x11); + + // ------------------------------------------------------------------ + // 0x10-1F Self-centering spring strength + // 0x10 = disable, 0x11-0x1F = weakest→strongest + // ------------------------------------------------------------------ + case 0x1: + SendSelfCenter(val == 0 ? 0 : val * 0x11); break; - case 2: // 0x20-2F Friction strength - if (val == 0) - // Disable friction - // TODO - is 0x20 for disable? - SendFriction(0); - else - // Enable friction (0x1 = weakest, 0xF = strongest) - SendFriction(val * 0x11); + + // ------------------------------------------------------------------ + // 0x20-2F Friction strength + // 0x20 = disable, 0x21-0x2F = weakest→strongest + // ------------------------------------------------------------------ + case 0x2: + SendFriction(val == 0 ? 0 : val * 0x11); + break; + + // ------------------------------------------------------------------ + // 0x30-3F Uncentering / vibration strength + // 0x30 = disable, 0x31-0x3F = weakest→strongest + // ------------------------------------------------------------------ + case 0x3: + SendVibrate(val == 0 ? 0 : val * 0x11); break; - case 3: // 0x30-3F Uncentering (vibrate) + + // ------------------------------------------------------------------ + // 0x40-4F Power-slide sequence + // Strength encoded in low nibble (0 = stop) + // ------------------------------------------------------------------ + case 0x4: if (val == 0) - // Disable uncentering SendVibrate(0); else - // Enable uncentering (0x1 = weakest, 0xF = strongest) - SendVibrate(val * 0x11); - break; - case 4: // 0x40-4F Play power-slide sequence - /* TODO */ + PlaySequencePowerSlide(val * 0x11); break; - case 5: // 0x50-5F Rotate wheel right - SendConstantForce((val + 1) * 0x5); + + // ------------------------------------------------------------------ + // 0x50-5F Constant force right (0x51 weakest … 0x5F strongest) + // ------------------------------------------------------------------ + case 0x5: + SendConstantForce((INT8)((val + 1) * 0x5)); break; - case 6: // 0x60-6F Rotate wheel left - SendConstantForce(-(val + 1) * 0x5); + + // ------------------------------------------------------------------ + // 0x60-6F Constant force left (0x61 weakest … 0x6F strongest) + // ------------------------------------------------------------------ + case 0x6: + SendConstantForce(-(INT8)((val + 1) * 0x5)); break; - case 7: // 0x70-7F Set steering parameters - /* TODO */ + + // ------------------------------------------------------------------ + // 0x70-7F Steering sensitivity / deadzone parameters + // (stored but not currently mapped to an SDL effect) + // ------------------------------------------------------------------ + case 0x7: + m_steeringParam = val; + DebugLog("[HLE] Steering param: %X\n", val); break; - case 8: // 0x80-8F Test Mode + + // ------------------------------------------------------------------ + // 0x80-8F Test / diagnostic mode commands + // ------------------------------------------------------------------ + case 0x8: switch (val & 0x7) { - case 0: SendStopAll(); break; // 0x80 Stop motor - case 1: SendConstantForce(20); break; // 0x81 Roll wheel right - case 2: SendConstantForce(-20); break; // 0x82 Roll wheel left - case 3: /* Ignore - no clutch */ break; // 0x83 Clutch on - case 4: /* Ignore - no clutch */ break; // 0x84 Clutch off - case 5: m_wheelCenter = (UINT8)m_inputs->steering->value; break; // 0x85 Set wheel center position - case 6: /* Ignore */ break; // 0x86 Set cockpit banking position - case 7: /* Ignore */ break; // 0x87 Lamp on/off + case 0: SendStopAll(); break; // 0x80 stop + case 1: SendConstantForce(20); break; // 0x81 roll right + case 2: SendConstantForce(-20); break; // 0x82 roll left + case 3: /* clutch on — no clutch in HLE */ break; + case 4: /* clutch off */ break; + case 5: m_wheelCenter = (UINT8)m_inputs->steering->value; break; // 0x85 set center + case 6: /* cockpit banking — ignore */ break; + case 7: /* lamp on/off — ignore */ break; } - case 0x9: // 0x90-9F ??? Don't appear to have any effect with Scud Race ROM - /* TODO */ break; - case 0xA: // 0xA0-AF ??? Don't appear to have any effect with Scud Race ROM - /* TODO */ + + // ------------------------------------------------------------------ + // 0x90-9F / 0xA0-AF Unknown, observed to have no effect with ROM + // ------------------------------------------------------------------ + case 0x9: + case 0xA: + DebugLog("[HLE] Unimplemented cmd %02X\n", cmd); break; - case 0xB: // 0xB0-BF Invalid command (reserved for use by PPC to send cabinet type 0xB0 or 0xB1 during initialization) - /* Ignore */ + + // ------------------------------------------------------------------ + // 0xB0-BF Cabinet type (handled above in HLEWrite, should not reach here) + // ------------------------------------------------------------------ + case 0xB: break; - case 0xC: // 0xC0-CF Set board mode (0xCB = reset board) + + // ------------------------------------------------------------------ + // 0xC0-CF Board mode / reset + // 0xCB = full reset (handled in HLEWrite) + // 0xC0-0xCA = set board mode + // ------------------------------------------------------------------ + case 0xC: SendStopAll(); - if (val >= 0xB) - { - // Reset board - m_initialized = false; - m_initState = 0; - } - else - m_boardMode = val; + m_boardMode = val; break; - case 0xD: // 0xD0-DF Set read mode + + // ------------------------------------------------------------------ + // 0xD0-DF Set read mode (which value HLERead() returns) + // ------------------------------------------------------------------ + case 0xD: m_readMode = val & 0x7; break; - case 0xE: // 0xE0-EF Invalid command - /* Ignore */ + + // ------------------------------------------------------------------ + // 0xE0-EF Invalid / reserved + // ------------------------------------------------------------------ + case 0xE: break; - case 0xF: // 0xF0-FF Echo test + + // ------------------------------------------------------------------ + // 0xF0-FF Echo test + // ------------------------------------------------------------------ + case 0xF: m_echoVal = val; break; } } -void CWheelBoard::RunFrame(void) -{ - if (IsDisabled()) - { - return; - } +// --------------------------------------------------------------------------- +// Command Set B: Sega Rally 2 — encoder protocol via ports 0x42 / 0x46 +// +// The PPC writes motor data to Z80 port 0x2A (→ port42Out) and motor +// control to Z80 port 0x2E (→ port46Out). Because we bypass the Z80, +// IOWrite8() feeds these directly into ProcessEncoderCmd(). +// --------------------------------------------------------------------------- - if (m_simulated) - SimulateFrame(); - else - CDriveBoard::RunFrame(); -} - -void CWheelBoard::SimulateFrame(void) +void CWheelBoard::ProcessEncoderCmd(void) { - if (!m_initialized) - m_initState++; - // TODO - update m_statusFlags and play preset scripts according to board mode -} + if (m_prev42Out == m_port42Out && m_prev46Out == m_port46Out) + return; -UINT8 CWheelBoard::IORead8(UINT32 portNum) -{ - UINT8 adcVal; + DebugLog("[HLE] Encoder: port46=%02X port42=%02X\n", m_port46Out, m_port42Out); - switch (portNum) + switch (m_port46Out) { - case 0x20: // DIP 1 value - return m_dip1; - case 0x21: // DIP 2 value - return m_dip2; - case 0x24: // ADC channel 1 - Y analog axis for joystick - case 0x25: // ADC channel 2 - steering wheel position (0x00 = full left, 0x80 = center, 0xFF = full right) and X analog axis for joystick - case 0x26: // ADC channel 3 - cockpit bank position (deluxe cabinets) (0x00 = full left, 0x80 = center, 0xFF = full right) - case 0x27: // ADC channel 4 - not connected - if (portNum == m_adcPortRead && m_adcPortBit-- > 0) + case 0xFB: + // Friction during power-slide. Strength = port42Out (0xFF strongest) + SendFriction(m_port42Out); + break; + + case 0xFC: + // Centering (bit2=1) or uncentering/vibrate (bit2=0) + if (m_port42Out & 0x04) { - switch (portNum) + if (m_port42Out & 0x80) + SendSelfCenter(0); + else { - case 0x24: // Y analog axis for joystick - adcVal = ReadADCChannel1(); - break; - case 0x25: // Steering wheel for twin racing cabinets - TODO - check actual range of steering, suspect it is not really 0x00-0xFF - adcVal = ReadADCChannel2(); - break; - case 0x26: // Cockpit bank position for deluxe racing cabinets - adcVal = ReadADCChannel3(); - break; - case 0x27: // Not connected - adcVal = ReadADCChannel4(); - break; - default: - DebugLog("Unhandled Z80 input on ADC port %u (at PC = %04X)\n", portNum, m_z80.GetPC()); - return 0xFF; + UINT8 strength = ((m_port42Out & 0x78) >> 3) * 0x10 + 0xF; + SendSelfCenter(strength); } - return (adcVal >> m_adcPortBit) & 0x01; } else { - DebugLog("Unhandled Z80 input on ADC port %u (at PC = %04X)\n", portNum, m_z80.GetPC()); - return 0xFF; + // Uncentering: 4 sequential nibble writes build the strength value + UINT8 seqNum = m_port42Out & 0x03; + UINT16 d = (m_port42Out & 0xF0) >> 4; + switch (seqNum) + { + case 0: m_uncenterVal1 = d << 4; break; + case 1: m_uncenterVal1 |= d; break; + case 2: m_uncenterVal2 = d << 4; break; + case 3: m_uncenterVal2 |= d; break; + } + if (seqNum == 0 && m_uncenterVal1 == 0) + SendVibrate(0); + else if (seqNum == 3 && m_uncenterVal1 > 0) + { + UINT8 strength = ((m_uncenterVal1 >> 1) - 7) * 0x50 + + ((m_uncenterVal2 >> 1) - 5) * 0x10 + 0xF; + SendVibrate(strength); + } } - case 0x28: // PPC command - return m_dataSent; - case 0x2c: // Encoder error reporting (kept at 0x00 for no error) - // Bit 1 0 - // 0 0 = encoder okay, no error - // 0 1 = encoder error 1 - overcurrent error - // 1 0 = encoder error 2 - overheat error - // 1 1 = encoder error 3 - encoder error, reinitializes board - return 0x00; + break; + + case 0xFD: + // Velocity-dependent centering (similar to spring, strength from port42Out) + if (m_port42Out == 0) + SendSelfCenter(0); + else + SendSelfCenter(m_port42Out); + break; + + case 0xFE: + // Constant force: 0x80=stop, 0x81-0xC0=left, 0x40-0x7F=right + if (m_port42Out > 0x81) + SendConstantForce(m_port42Out <= 0xC0 ? (INT8)(2 * (0x81 - m_port42Out)) : (INT8)(-0x80)); + else if (m_port42Out < 0x7F) + SendConstantForce(m_port42Out >= 0x40 ? (INT8)(2 * (0x7F - m_port42Out)) : (INT8)(0x7F)); + else + SendConstantForce(0); + break; + + case 0xFF: + if (m_port42Out == 0xFF) SendStopAll(); + break; + default: - DebugLog("Unhandled Z80 input on port %u (at PC = %04X)\n", portNum, m_z80.GetPC()); - return 0xFF; + DebugLog("[HLE] Unknown encoder cmd: port46=%02X port42=%02X\n", m_port46Out, m_port42Out); + break; } + + m_prev42Out = m_port42Out; + m_prev46Out = m_port46Out; } -void CWheelBoard::IOWrite8(UINT32 portNum, UINT8 data) +// =========================================================================== +// Preset sequence helpers +// =========================================================================== + +// Short constant-force jolt (one frame burst simulated as a strong pulse) +void CWheelBoard::PlaySequenceJolt(INT8 strength) { - switch (portNum) - { - case 0x10: // Unsure? - single byte 0x03 sent at initialization, then occasionally writes 0x07 & 0xFA to port - return; - case 0x11: // Interrupt control - if (data == 0x57) - m_allowInterrupts = true; - else if (data == 0x53) // Strictly speaking 0x53 then 0x04 - m_allowInterrupts = false; - return; - case 0x1c: // Unsure? - two bytes 0xFF, 0xFF sent at initialization only - case 0x1d: // Unsure? - two bytes 0x0F, 0x17 sent at initialization only - case 0x1e: // Unsure? - same as port 28 - case 0x1f: // Unsure? - same as port 31 - return; - case 0x20: // Left digit of 7-segment display 1 - m_seg1Digit1 = data; - return; - case 0x21: // Right digit of 7-segment display 1 - m_seg1Digit2 = data; - return; - case 0x22: // Left digit of 7-segment display 2 - m_seg2Digit1 = data; - return; - case 0x23: // Right digit of 7-segment display 2 - m_seg2Digit2 = data; - return; - case 0x24: // ADC channel 1 control - case 0x25: // ADC channel 2 control - case 0x26: // ADC channel 3 control - case 0x27: // ADC channel 4 control - m_adcPortRead = portNum; - m_adcPortBit = 8; - return; - case 0x29: // Reply for PPC - m_dataReceived = data; - if (data == 0xCC) - m_initialized = true; - return; - case 0x2a: // Encoder motor data (x axis) - m_port42Out = data; - ProcessEncoderCmd(); - return; - case 0x2d: // Clutch/lamp control (deluxe cabinets) ( or y axis) - return; - case 0x2e: // Encoder motor control - m_port46Out = data; - return; - case 0xf0: // Unsure? - single byte 0xBB sent at initialization only - return; - case 0xf1: // Unsure? - single byte 0x4E sent regularly - some sort of watchdog? - return; - default: - DebugLog("Unhandled Z80 output on port %u (at PC = %04X)\n", portNum, m_z80.GetPC()); - return; - } + SendConstantForce(strength); + // The effect will be overridden on the next Write() naturally; no timer needed. + DebugLog("[HLE] Jolt: %d\n", (int)strength); } -void CWheelBoard::ProcessEncoderCmd(void) +// Vibration burst for rumble / road texture / curb effects +void CWheelBoard::PlaySequenceRumble(UINT8 strength) { - if (m_prev42Out != m_port42Out || m_prev46Out != m_port46Out) - { - //DebugLog("46 [%02X] / 42 [%02X]\n", m_port46Out, m_port42Out); - switch (m_port46Out) - { - case 0xFB: - // TODO - friction? Sent during power slide. 0xFF = strongest or 0x00? - //SendFriction(m_port42Out); - break; - - case 0xFC: - // Centering / uncentering (vibrate) - // Bit 2 = on for centering, off for uncentering - if (m_port42Out&0x04) - { - // Centering - // Bit 7 = on for disable, off for enable - if (m_port42Out&0x80) - { - // Disable centering - SendSelfCenter(0); - } - else - { - // Bits 3-6 = centering strength 0x0-0xF. This is scaled to range 0x0F-0xFF - UINT8 strength = ((m_port42Out&0x78)>>3) * 0x10 + 0xF; - SendSelfCenter(strength); - } - } - else - { - // Uncentering - // Bits 0-1 = data sequence number 0-3 - UINT8 seqNum = m_port42Out&0x03; - // Bits 4-7 = data values - UINT16 data = (m_port42Out&0xF0)>>4; - switch (seqNum) - { - case 0: m_uncenterVal1 = data<<4; break; - case 1: m_uncenterVal1 |= data; break; - case 2: m_uncenterVal2 = data<<4; break; - case 3: m_uncenterVal2 |= data; break; - } - if (seqNum == 0 && m_uncenterVal1 == 0) - { - // Disable uncentering - SendVibrate(0); - } - else if (seqNum == 3 && m_uncenterVal1 > 0) - { - // Uncentering - unsure exactly how values sent map to strength or whether they specify some other attributes of effect - // For now just attempting to map them to a sensible value in range 0x00-0xFF - UINT8 strength = ((m_uncenterVal1>>1) - 7) * 0x50 + ((m_uncenterVal2>>1) - 5) * 0x10 + 0xF; - SendVibrate(strength); - } - } - break; - - case 0xFD: - // TODO - unsure? Sent as velocity changes, similar to self-centering - break; - - case 0xFE: - // Apply constant force to wheel - // Value is: 0x80 = stop motor, 0x81-0xC0 = roll wheel left, 0x40-0x7F = roll wheel right, scale to range -0x80-0x7F - // Note: seems to often output 0x7F or 0x81 for stop motor, so narrowing wheel ranges to 0x40-0x7E and 0x82-0xC0 - if (m_port42Out > 0x81) - { - if (m_port42Out <= 0xC0) - SendConstantForce(2 * (0x81 - m_port42Out)); - else - SendConstantForce(-0x80); - } - else if (m_port42Out < 0x7F) - { - if (m_port42Out >= 0x40) - SendConstantForce(2 * (0x7F - m_port42Out)); - else - SendConstantForce(0x7F); - } - else - SendConstantForce(0); - break; - - case 0xFF: - // Stop all effects - if (m_port42Out == 0xFF) - SendStopAll(); - break; - - default: - //DebugLog("Unknown = 46 [%02X] / 42 [%02X]\n", m_port46Out, m_port42Out); - break; - } + SendVibrate(strength); + DebugLog("[HLE] Rumble: %u\n", (unsigned)strength); +} - m_prev42Out = m_port42Out; - m_prev46Out = m_port46Out; - } +// Power-slide: combine friction + mild vibration +void CWheelBoard::PlaySequencePowerSlide(UINT8 strength) +{ + SendFriction(strength); + SendVibrate(strength >> 1); + DebugLog("[HLE] PowerSlide: %u\n", (unsigned)strength); } +// =========================================================================== +// SDL Haptic output — SendXxx implementations +// =========================================================================== + +// --------------------------------------------------------------------------- +// Gamepad rumble helper (Xbox / SDL_GameController backend) +// large = low-freq motor [0,1], small = high-freq motor [0,1] +// duration_ms = 0 means stop +// --------------------------------------------------------------------------- +// Boost factor: multiplies all rumble intensities to compensate for +// gamepads being weaker than direct-drive wheels. +// 1.0 = no boost, 2.0 = double strength (clamped to max 65535) +static constexpr float RUMBLE_BOOST = 2.0f; + +static void GamepadRumble(float large, float small, Uint32 duration_ms = SDL_HAPTIC_INFINITY) +{ + if (!s_gamepad) return; + // Apply boost and clamp + large = std::clamp(large * RUMBLE_BOOST, 0.0f, 1.0f); + small = std::clamp(small * RUMBLE_BOOST, 0.0f, 1.0f); + Uint16 lo = (Uint16)(large * 65535.0f); + Uint16 hi = (Uint16)(small * 65535.0f); + // SDL_HAPTIC_INFINITY is not valid for GameControllerRumble; + // use a repeating short duration instead + Uint32 dur = (duration_ms == (Uint32)SDL_HAPTIC_INFINITY) ? 200 : duration_ms; + SDL_GameControllerRumble(s_gamepad, lo, hi, dur); +} void CWheelBoard::SendStopAll(void) { - //DebugLog(">> Stop All Effects\n"); - - ForceFeedbackCmd ffCmd{}; - ffCmd.id = FFStop; - - m_inputs->steering->SendForceFeedbackCmd(ffCmd); - + if (s_haptic) SDL_HapticStopAll(s_haptic); + if (s_gamepad) SDL_GameControllerRumble(s_gamepad, 0, 0, 0); m_lastConstForce = 0; m_lastSelfCenter = 0; m_lastFriction = 0; @@ -622,151 +711,342 @@ void CWheelBoard::SendStopAll(void) void CWheelBoard::SendConstantForce(INT8 val) { - if (val == m_lastConstForce) - return; - /* - if (val > 0) + if (val == m_lastConstForce) return; + if (!s_haptic && !s_gamepad) { m_lastConstForce = val; return; } + + if (val == 0) { - DebugLog(">> Force Right %02X [%8s", val, ""); - for (unsigned i = 0; i < 8; i++) - DebugLog(i == 0 || i <= (val + 1) / 16 ? ">" : " "); - DebugLog("]\n"); + if (s_effectConstant >= 0) SDL_HapticStopEffect(s_haptic, s_effectConstant); + m_lastConstForce = 0; + return; } - else if (val < 0) + + float norm = std::abs((float)val / (val >= 0 ? 127.0f : 128.0f)); + + if (s_gamepad && !s_haptic) { - DebugLog(">> Force Left %02X [", -val); - for (unsigned i = 0; i < 8; i++) - DebugLog(i == 7 || i >= (val + 128) / 16 ? "<" : " "); - DebugLog("%8s]\n", ""); + // Xbox fallback: constant force → large motor intensity + GamepadRumble(norm, norm * 0.5f); // large=force, small=texture } else - DebugLog(">> Stop Force [%16s]\n", ""); - */ - - ForceFeedbackCmd ffCmd; - ffCmd.id = FFConstantForce; - ffCmd.force = (float)val / (val >= 0 ? 127.0f : 128.0f); - - m_inputs->steering->SendForceFeedbackCmd(ffCmd); - + { + float signed_norm = (float)val / (val >= 0 ? 127.0f : 128.0f); + SDL_HapticEffect eff; + memset(&eff, 0, sizeof(eff)); + eff.type = SDL_HAPTIC_CONSTANT; + SetEffectDirection(eff.constant.direction, false); + eff.constant.length = SDL_HAPTIC_INFINITY; + eff.constant.level = NormToSint16(signed_norm); + s_effectConstant = UploadEffect(s_effectConstant, eff); + if (s_effectConstant >= 0) SDL_HapticRunEffect(s_haptic, s_effectConstant, 1); + } m_lastConstForce = val; } void CWheelBoard::SendSelfCenter(UINT8 val) { - if (val == m_lastSelfCenter) - return; - /* - if (val == 0) - DebugLog(">> Stop Self-Center\n"); - else - DebugLog(">> Self-Center %02X\n", val); - */ - - ForceFeedbackCmd ffCmd; - ffCmd.id = FFSelfCenter; - ffCmd.force = (float)val / 255.0f; + if (val == m_lastSelfCenter) return; + if (!s_haptic && !s_gamepad) { m_lastSelfCenter = val; return; } - m_inputs->steering->SendForceFeedbackCmd(ffCmd); + if (val == 0) + { + if (s_gamepad && !s_haptic) + SDL_GameControllerRumble(s_gamepad, 0, 0, 0); + else if (s_effectSpring >= 0) + SDL_HapticStopEffect(s_haptic, s_effectSpring); + m_lastSelfCenter = 0; + return; + } + if (s_gamepad && !s_haptic) + { + // Xbox fallback: self-center → gentle constant rumble on large motor + float norm = (float)val / 255.0f; + GamepadRumble(norm * 0.9f, norm * 0.3f); // self-center spring feel + } + else + { + Sint16 level = NormToSint16((float)val / 255.0f); + SDL_HapticEffect eff; + memset(&eff, 0, sizeof(eff)); + eff.type = SDL_HAPTIC_SPRING; + eff.condition.length = SDL_HAPTIC_INFINITY; + eff.condition.right_sat[0] = 0x7FFF; + eff.condition.left_sat[0] = 0x7FFF; + eff.condition.right_coeff[0] = level; + eff.condition.left_coeff[0] = level; + eff.condition.deadband[0] = 0; + eff.condition.center[0] = 0; + s_effectSpring = UploadEffect(s_effectSpring, eff); + if (s_effectSpring >= 0) SDL_HapticRunEffect(s_haptic, s_effectSpring, 1); + } m_lastSelfCenter = val; } - void CWheelBoard::SendFriction(UINT8 val) { - if (val == m_lastFriction) - return; - /* - if (val == 0) - DebugLog(">> Stop Friction\n"); - else - DebugLog(">> Friction %02X\n", val); - */ + if (val == m_lastFriction) return; + if (!s_haptic && !s_gamepad) { m_lastFriction = val; return; } - ForceFeedbackCmd ffCmd; - ffCmd.id = FFFriction; - ffCmd.force = (float)val / 255.0f; - m_inputs->steering->SendForceFeedbackCmd(ffCmd); + if (val == 0) + { + if (s_gamepad && !s_haptic) + SDL_GameControllerRumble(s_gamepad, 0, 0, 0); + else if (s_effectFriction >= 0) + SDL_HapticStopEffect(s_haptic, s_effectFriction); + m_lastFriction = 0; + return; + } + if (s_gamepad && !s_haptic) + { + // Xbox fallback: friction → small motor (high-freq texture feel) + float norm = (float)val / 255.0f; + GamepadRumble(norm * 0.7f, norm); // friction: both motors + } + else + { + Sint16 level = NormToSint16((float)val / 255.0f); + SDL_HapticEffect eff; + memset(&eff, 0, sizeof(eff)); + eff.type = SDL_HAPTIC_FRICTION; + eff.condition.length = SDL_HAPTIC_INFINITY; + eff.condition.right_sat[0] = 0x7FFF; + eff.condition.left_sat[0] = 0x7FFF; + eff.condition.right_coeff[0] = level; + eff.condition.left_coeff[0] = level; + s_effectFriction = UploadEffect(s_effectFriction, eff); + if (s_effectFriction >= 0) SDL_HapticRunEffect(s_haptic, s_effectFriction, 1); + } m_lastFriction = val; } void CWheelBoard::SendVibrate(UINT8 val) { - if (val == m_lastVibrate) - return; - /* + if (val == m_lastVibrate) return; + if (!s_haptic && !s_gamepad) { m_lastVibrate = val; return; } + if (val == 0) - DebugLog(">> Stop Vibrate\n"); + { + if (s_gamepad && !s_haptic) + SDL_GameControllerRumble(s_gamepad, 0, 0, 0); + else if (s_effectVibrate >= 0) + SDL_HapticStopEffect(s_haptic, s_effectVibrate); + m_lastVibrate = 0; + return; + } + + if (s_gamepad && !s_haptic) + { + // Xbox fallback: vibrate → both motors for rumble feel + float norm = (float)val / 255.0f; + GamepadRumble(norm, norm); // vibrate: both motors full + } else - DebugLog(">> Vibrate %02X\n", val); - */ + { + SDL_HapticEffect eff; + memset(&eff, 0, sizeof(eff)); + eff.type = SDL_HAPTIC_SINE; + SetEffectDirection(eff.periodic.direction, false); + eff.periodic.length = SDL_HAPTIC_INFINITY; + eff.periodic.period = 100; + eff.periodic.magnitude = NormToSint16((float)val / 255.0f); + s_effectVibrate = UploadEffect(s_effectVibrate, eff); + if (s_effectVibrate >= 0) SDL_HapticRunEffect(s_haptic, s_effectVibrate, 1); + } + m_lastVibrate = val; +} - ForceFeedbackCmd ffCmd; - ffCmd.id = FFVibrate; - ffCmd.force = (float)val / 255.0f; - m_inputs->steering->SendForceFeedbackCmd(ffCmd); +// =========================================================================== +// Z80 I/O port handlers (still called from CDriveBoard but now HLE-aware) +// =========================================================================== - m_lastVibrate = val; +UINT8 CWheelBoard::IORead8(UINT32 portNum) +{ + switch (portNum) + { + case 0x20: return m_dip1; + case 0x21: return m_dip2; + case 0x28: return m_dataSent; + case 0x2c: return 0x00; // no encoder error + case 0x24: case 0x25: case 0x26: case 0x27: + if (portNum == m_adcPortRead && m_adcPortBit-- > 0) + { + UINT8 adcVal = 0; + switch (portNum) + { + case 0x24: adcVal = ReadADCChannel1(); break; + case 0x25: adcVal = ReadADCChannel2(); break; + case 0x26: adcVal = ReadADCChannel3(); break; + case 0x27: adcVal = ReadADCChannel4(); break; + } + return (adcVal >> m_adcPortBit) & 0x01; + } + return 0xFF; + default: + return 0xFF; + } } -uint8_t CWheelBoard::ReadADCChannel1() const +void CWheelBoard::IOWrite8(UINT32 portNum, UINT8 data) { - return 0x00; + switch (portNum) + { + case 0x11: + m_allowInterrupts = (data == 0x57); + return; + case 0x20: m_seg1Digit1 = data; return; + case 0x21: m_seg1Digit2 = data; return; + case 0x22: m_seg2Digit1 = data; return; + case 0x23: m_seg2Digit2 = data; return; + case 0x24: case 0x25: case 0x26: case 0x27: + m_adcPortRead = portNum; + m_adcPortBit = 8; + return; + case 0x29: + m_dataReceived = data; + if (data == 0xCC) m_initialized = true; + return; + case 0x2a: // Sega Rally 2 encoder motor data + m_port42Out = data; + ProcessEncoderCmd(); + return; + case 0x2e: // Sega Rally 2 encoder motor control + m_port46Out = data; + return; + default: + return; + } } +// =========================================================================== +// ADC helpers +// =========================================================================== + +uint8_t CWheelBoard::ReadADCChannel1() const { return 0x00; } uint8_t CWheelBoard::ReadADCChannel2() const { - if (m_initialized) - return (UINT8)m_inputs->steering->value; - else - return 0x80; // If not initialized, return 0x80 so that wheel centering test does not fail + return m_initialized ? (UINT8)m_inputs->steering->value : 0x80; +} +uint8_t CWheelBoard::ReadADCChannel3() const { return 0x80; } +uint8_t CWheelBoard::ReadADCChannel4() const { return 0x00; } + +// =========================================================================== +// Save / Load state +// =========================================================================== + +void CWheelBoard::SaveState(CBlockFile *SaveState) +{ + CDriveBoard::SaveState(SaveState); + SaveState->NewBlock("WheelBoard", __FILE__); + SaveState->Write(&m_simulated, sizeof(m_simulated)); + SaveState->Write(&m_dip1, sizeof(m_dip1)); + SaveState->Write(&m_dip2, sizeof(m_dip2)); + SaveState->Write(&m_adcPortRead, sizeof(m_adcPortRead)); + SaveState->Write(&m_adcPortBit, sizeof(m_adcPortBit)); + SaveState->Write(&m_uncenterVal1, sizeof(m_uncenterVal1)); + SaveState->Write(&m_uncenterVal2, sizeof(m_uncenterVal2)); } -uint8_t CWheelBoard::ReadADCChannel3() const +void CWheelBoard::LoadState(CBlockFile *SaveState) { - return 0x80; + if (SaveState->FindBlock("WheelBoard") != Result::OKAY) + { + LoadLegacyState(SaveState); + return; + } + bool wasSimulated; + SaveState->Read(&wasSimulated, sizeof(wasSimulated)); + // HLE is always simulated; if state says otherwise just continue + SaveState->Read(&m_dip1, sizeof(m_dip1)); + SaveState->Read(&m_dip2, sizeof(m_dip2)); + SaveState->Read(&m_adcPortRead, sizeof(m_adcPortRead)); + SaveState->Read(&m_adcPortBit, sizeof(m_adcPortBit)); + SaveState->Read(&m_uncenterVal1, sizeof(m_uncenterVal1)); + SaveState->Read(&m_uncenterVal2, sizeof(m_uncenterVal2)); + CDriveBoard::LoadState(SaveState); } -uint8_t CWheelBoard::ReadADCChannel4() const +void CWheelBoard::LoadLegacyState(CBlockFile *SaveState) { - return 0x00; + if (SaveState->FindBlock("DriveBoard") != Result::OKAY) + { + ErrorLog("Unable to load wheel drive board state. Save state file is corrupt."); + Disable(); + return; + } + + CDriveBoard::LegacyDriveBoardState state; + bool isEnabled = !IsDisabled(), wasEnabled = false, wasSimulated = false; + SaveState->Read(&wasEnabled, sizeof(wasEnabled)); + if (wasEnabled) + { + SaveState->Read(&wasSimulated, sizeof(wasSimulated)); + if (!wasSimulated) + { + SaveState->Read(&state.dip1, sizeof(state.dip1)); + SaveState->Read(&state.dip2, sizeof(state.dip2)); + SaveState->Read(state.ram, 0x2000); + SaveState->Read(&state.initialized, sizeof(state.initialized)); + SaveState->Read(&state.allowInterrupts,sizeof(state.allowInterrupts)); + SaveState->Read(&state.dataSent, sizeof(state.dataSent)); + SaveState->Read(&state.dataReceived, sizeof(state.dataReceived)); + SaveState->Read(&state.adcPortRead, sizeof(state.adcPortRead)); + SaveState->Read(&state.adcPortBit, sizeof(state.adcPortBit)); + SaveState->Read(&state.uncenterVal1, sizeof(state.uncenterVal1)); + SaveState->Read(&state.uncenterVal2, sizeof(state.uncenterVal2)); + } + } + + if (wasEnabled != isEnabled) + { + Disable(); + ErrorLog("Halting drive board emulation due to mismatch in active and restored states."); + } + else + CDriveBoard::LoadLegacyState(state, SaveState); } +void CWheelBoard::Disable(void) +{ + SendStopAll(); + CDriveBoard::Disable(); +} + +// =========================================================================== +// Constructor / Destructor +// =========================================================================== + CWheelBoard::CWheelBoard(const Util::Config::Node &config) : CDriveBoard(config) { m_dip1 = 0xCF; m_dip2 = 0xFF; - m_seg1Digit1 = 0; - m_seg1Digit2 = 0; - m_seg2Digit1 = 0; - m_seg2Digit2 = 0; + m_seg1Digit1 = 0; m_seg1Digit2 = 0; + m_seg2Digit1 = 0; m_seg2Digit2 = 0; - m_adcPortRead = 0; - m_adcPortBit = 0; + m_adcPortRead = 0; m_adcPortBit = 0; + m_port42Out = 0; m_port46Out = 0; + m_prev42Out = 0; m_prev46Out = 0; - m_port42Out = 0; - m_port46Out = 0; + m_uncenterVal1 = 0; m_uncenterVal2 = 0; - m_prev42Out = 0; - m_prev46Out = 0; + m_lastConstForce = 0; m_lastSelfCenter = 0; + m_lastFriction = 0; m_lastVibrate = 0; - m_uncenterVal1 = 0; - m_uncenterVal2 = 0; + m_hleGameType = HLE_GAME_UNKNOWN; + m_hleCabinetType = 0; + m_steeringParam = 0; - // Feedback state - m_lastConstForce = 0; - m_lastSelfCenter = 0; - m_lastFriction = 0; - m_lastVibrate = 0; + // m_simulated is determined in Reset() based on ROM presence: + // m_rom == nullptr → HLE, m_rom != nullptr → Z80 emulation + m_simulated = false; - DebugLog("Built Drive Board (wheel)\n"); + DebugLog("Built Drive Board (wheel) [HLE / SDL Haptic]\n"); } CWheelBoard::~CWheelBoard(void) { - + CloseSDLHaptic(); } diff --git a/Src/Model3/DriveBoard/WheelBoard.h b/Src/Model3/DriveBoard/WheelBoard.h index 17d4530f..3eace465 100644 --- a/Src/Model3/DriveBoard/WheelBoard.h +++ b/Src/Model3/DriveBoard/WheelBoard.h @@ -23,7 +23,11 @@ /* * WheelBoard.h * + * This code is enhanced with Claude by Anthropic. * Header for the CWheelBoard (force feedback emulation for wheel) class. + * + * HLE mode: Z80 ROM is not required. All force feedback commands from the PPC + * are decoded directly and forwarded to SDL Haptic. */ #ifndef INCLUDED_WHEELBOARD_H @@ -54,21 +58,20 @@ class CWheelBoard : public CDriveBoard * * Parameters: * seg1Digit1 Reference of variable to store digit 1 of the first 7- - * segment display to. + * segment display to. * seg1Digit2 First display, second digit. * seg2Digit1 Second display, first digit. * seg2Digit2 Second display, second digit. */ void Get7SegDisplays(UINT8 &seg1Digit, UINT8 &seg1Digit2, UINT8 &seg2Digit1, UINT8 &seg2Digit2) const; - /* * SaveState(SaveState): * * Saves the drive board state. * * Parameters: - * SaveState Block file to save state information to. + * SaveState Block file to save state information to. */ void SaveState(CBlockFile *SaveState); @@ -78,14 +81,14 @@ class CWheelBoard : public CDriveBoard * Restores the drive board state. * * Parameters: - * SaveState Block file to load save state information from. + * SaveState Block file to load save state information from. */ void LoadState(CBlockFile *SaveState); /* * Reset(void): * - * Resets the drive board. + * Resets the drive board. Forces HLE mode (m_simulated = true). */ void Reset(void); @@ -116,13 +119,26 @@ class CWheelBoard : public CDriveBoard */ void RunFrame(void); + /* + * InitSDLHaptic(): + * CloseSDLHaptic(): + * + * Open/close the SDL haptic device. InitSDLHaptic() must be called once + * after SDL_Init(SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK). + * + * Returns (Init): + * True on success, false on failure. + */ + bool InitSDLHaptic(); + void CloseSDLHaptic(); + /* * CWheelBoard(config): * ~CWheelBoard(): * * Constructor and destructor. Memory is freed by destructor. * - * Paramters: + * Parameters: * config Run-time configuration. The reference should be held because * this changes at run-time. */ @@ -130,30 +146,26 @@ class CWheelBoard : public CDriveBoard ~CWheelBoard(void); /* - * Read8(addr): * IORead8(portNum): * - * Methods for reading from Z80's memory and IO space. Required by CBus. + * Methods for reading from Z80's IO space. Required by CBus. * * Parameters: - * addr Address in memory (0-0xFFFF). - * portNum Port address (0-255). + * portNum Port address (0-255). * * Returns: - * A byte of data from the address or port. + * A byte of data from the port. */ UINT8 IORead8(UINT32 portNum); /* - * Write8(addr, data): - * IORead8(portNum, data): + * IOWrite8(portNum, data): * - * Methods for writing to Z80's memory and IO space. Required by CBus. + * Methods for writing to Z80's IO space. Required by CBus. * * Parameters: - * addr Address in memory (0-0xFFFF). - * portNum Port address (0-255). - * data Byte to write. + * portNum Port address (0-255). + * data Byte to write. */ void IOWrite8(UINT32 portNum, UINT8 data); @@ -161,56 +173,98 @@ class CWheelBoard : public CDriveBoard void Disable(void); private: + // ------------------------------------------------------------------------- + // Legacy state loader + // ------------------------------------------------------------------------- void LoadLegacyState(CBlockFile *SaveState); + // ------------------------------------------------------------------------- + // 7-segment display state + // ------------------------------------------------------------------------- UINT8 m_seg1Digit1; // Current value of left digit on 7-segment display 1 UINT8 m_seg1Digit2; // Current value of right digit on 7-segment display 1 UINT8 m_seg2Digit1; // Current value of left digit on 7-segment display 2 - UINT8 m_seg2Digit2; // Current value of right digit on 7-segment display 2 - - UINT16 m_adcPortRead; // ADC port currently reading from - UINT8 m_adcPortBit; // Bit number currently reading on ADC port - - UINT8 m_port42Out; // Last value sent to Z80 I/O port 42 (encoder motor data) - UINT8 m_port46Out; // Last value sent to Z80 I/O port 46 (encoder motor control) - - UINT8 m_prev42Out; // Previous value sent to Z80 I/O port 42 - UINT8 m_prev46Out; // Previous value sent to Z80 I/O port 46 - - UINT8 m_uncenterVal1; // First part of pending uncenter command - UINT8 m_uncenterVal2; // Second part of pending uncenter command - - // Feedback state - INT8 m_lastConstForce; // Last constant force command sent - UINT8 m_lastSelfCenter; // Last self center command sent - UINT8 m_lastFriction; // Last friction command sent - UINT8 m_lastVibrate; // Last vibrate command sent - - UINT8 SimulateRead(void); - - void SimulateWrite(UINT8 data); - - void SimulateFrame(void); - + UINT8 m_seg2Digit2; // Current value of right digit on 7-segment display 2 + + // ------------------------------------------------------------------------- + // ADC / encoder port state + // ------------------------------------------------------------------------- + UINT16 m_adcPortRead; // ADC port currently reading from + UINT8 m_adcPortBit; // Bit number currently reading on ADC port + + UINT8 m_port42Out; // Last value sent to Z80 I/O port 42 (encoder motor data) + UINT8 m_port46Out; // Last value sent to Z80 I/O port 46 (encoder motor control) + UINT8 m_prev42Out; // Previous value sent to Z80 I/O port 42 + UINT8 m_prev46Out; // Previous value sent to Z80 I/O port 46 + + UINT8 m_uncenterVal1; // First part of pending uncenter command + UINT8 m_uncenterVal2; // Second part of pending uncenter command + + // ------------------------------------------------------------------------- + // Force feedback output state + // ------------------------------------------------------------------------- + INT8 m_lastConstForce; // Last constant force command sent + UINT8 m_lastSelfCenter; // Last self center command sent + UINT8 m_lastFriction; // Last friction command sent + UINT8 m_lastVibrate; // Last vibrate command sent + + // ------------------------------------------------------------------------- + // HLE game-type detection + // ------------------------------------------------------------------------- + enum HLEGameType + { + HLE_GAME_UNKNOWN = 0, + HLE_GAME_SCUD_RACE = 1, // command set A + HLE_GAME_DAYTONA2 = 2, // command set A (compatible with Scud Race) + HLE_GAME_SEGA_RALLY2 = 3 // command set B (encoder via port 0x42/0x46) + }; + + HLEGameType m_hleGameType; // Detected game type + UINT8 m_hleCabinetType; // 0xB0 = standard, 0xB1 = deluxe/twin + UINT8 m_steeringParam; // Last 0x7x steering sensitivity byte + + // ------------------------------------------------------------------------- + // HLE core — replace Z80 emulation entirely + // ------------------------------------------------------------------------- + UINT8 HLERead(void); + void HLEWrite(UINT8 data); + void HLEFrame(void); + + // Command set A: Scud Race / Daytona 2 + void HLEDecodeCommandSetA(UINT8 cmd); + + // Command set B: Sega Rally 2 (encoder protocol via ports 0x42/0x46) void ProcessEncoderCmd(void); - void SendStopAll(void); + // ------------------------------------------------------------------------- + // Preset sequence helpers + // ------------------------------------------------------------------------- + void PlaySequenceJolt(INT8 strength); + void PlaySequenceRumble(UINT8 strength); + void PlaySequencePowerSlide(UINT8 strength); + // ------------------------------------------------------------------------- + // Force feedback output (SDL Haptic) + // ------------------------------------------------------------------------- + void SendStopAll(void); void SendConstantForce(INT8 val); - void SendSelfCenter(UINT8 val); - void SendFriction(UINT8 val); - void SendVibrate(UINT8 val); + // ------------------------------------------------------------------------- + // ADC channel helpers + // ------------------------------------------------------------------------- uint8_t ReadADCChannel1() const; - uint8_t ReadADCChannel2() const; - uint8_t ReadADCChannel3() const; - uint8_t ReadADCChannel4() const; + + // ------------------------------------------------------------------------- + // Removed in HLE version (kept as comment for reference) + // ------------------------------------------------------------------------- + // SimulateRead / SimulateWrite / SimulateFrame are replaced by + // HLERead / HLEWrite / HLEFrame respectively. }; #endif // INCLUDED_WHEELBOARD_H diff --git a/Src/OSD/SDL/Main.cpp b/Src/OSD/SDL/Main.cpp index 2a704960..56f1aec2 100644 --- a/Src/OSD/SDL/Main.cpp +++ b/Src/OSD/SDL/Main.cpp @@ -22,6 +22,7 @@ /* * Main.cpp * + * This code is enhanced with Claude by Anthropic. * Main program driver for the SDL port. * * Bugs and Issues to Address @@ -92,6 +93,7 @@ #include "Graphics/New3D/New3D.h" #include "Model3/IEmulator.h" #include "Model3/Model3.h" +#include "Model3/DriveBoard/WheelBoard.h" #include "OSD/Audio.h" #include "Graphics/New3D/VBO.h" #include "Graphics/SuperAA.h" @@ -1015,6 +1017,23 @@ int Supermodel(const Game &game, ROMSet *rom_set, IEmulator *Model3, CInputs *In // Reset emulator Model3->Reset(); + // Initialize SDL Haptic for HLE force feedback (no Z80 ROM required). + // Must be called after Reset() so the drive board instance is active. + { + CModel3 *m3 = dynamic_cast(Model3); + if (m3 && m3->GetDriveBoard()) + { + CWheelBoard *wheelBoard = dynamic_cast(m3->GetDriveBoard()); + if (wheelBoard) + { + if (wheelBoard->InitSDLHaptic()) + InfoLog("SDL Haptic initialized for force feedback."); + else + ErrorLog("SDL Haptic initialization failed. Force feedback disabled."); + } + } + } + // Load initial save state if requested if (!initialState.empty()) LoadState(Model3, initialState); @@ -1522,6 +1541,7 @@ Util::Config::Node DefaultConfig() config.Set("LegacySoundDSP", false, "Sound"); // New config option for games that do not play correctly with MAME's SCSP sound core. // CDriveBoard config.Set("ForceFeedback", false, "ForceFeedback"); + config.Set("DriveBoardHLE", false, "DriveBoardHLE"); // Use HLE force feedback (no Z80 ROM required) // Platform-specific/UI config.Set("New3DEngine", true, "Video"); @@ -1996,6 +2016,7 @@ static ParsedCommandLine ParseCommandLine(int argc, char **argv) { "-emulate-netboard", { "SimulateNet", false } }, #endif { "-no-force-feedback", { "ForceFeedback", false } }, + { "-drive-board-hle", { "DriveBoardHLE", true } }, // Force HLE drive board emulation { "-force-feedback", { "ForceFeedback", true } }, { "-dump-memory", { "DumpMemory", true } }, { "-dump-textures", { "DumpTextures", true } },