From 402042b9bc30b97dd452584322ba6816d732e0f1 Mon Sep 17 00:00:00 2001 From: naari3 Date: Fri, 20 Feb 2026 00:53:15 +0900 Subject: [PATCH] AMMediaboard: add Triforce NETWORK TEST support Implement the MediaBoard commands and response protocol needed for the segaboot NETWORK TEST to pass: - TestHardware (0x0301) two-phase response via CoreTiming: phase 1 echoes test_type with 0x80 acknowledgment flag, phase 2 sends result with testStatus=2 and checkProgress=100 - GetNetworkConfig (0x0104) with trinetcfg.bin read intercept - Separate Execute1/Execute2 last-response buffers to prevent cross-clobber on the shared s_media_buffer - Generic 0x80xx cleanup command acknowledgment - Network config persistence for SET IP ADDRESS --- Source/Core/Core/HW/DVD/AMMediaboard.cpp | 203 ++++++++++++++++++++++- Source/Core/Core/HW/DVD/AMMediaboard.h | 2 +- 2 files changed, 196 insertions(+), 9 deletions(-) diff --git a/Source/Core/Core/HW/DVD/AMMediaboard.cpp b/Source/Core/Core/HW/DVD/AMMediaboard.cpp index 30308874bd..4a327449eb 100644 --- a/Source/Core/Core/HW/DVD/AMMediaboard.cpp +++ b/Source/Core/Core/HW/DVD/AMMediaboard.cpp @@ -21,6 +21,7 @@ #include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" #include "Core/Core.h" +#include "Core/CoreTiming.h" #include "Core/HLE/HLE.h" #include "Core/HW/EXI/EXI_DeviceBaseboard.h" #include "Core/HW/Memmap.h" @@ -146,6 +147,13 @@ static std::unique_ptr s_dimm_disc; static std::array s_firmware; static std::array s_media_buffer_32; static u8* const s_media_buffer = reinterpret_cast(s_media_buffer_32.data()); + +// Both Execute paths write responses to s_media_buffer, so one overwrites the other. +// Keep separate copies so each path's DMA Read returns its own response. +static std::array s_exec1_last_response{}; +static std::array s_exec2_last_response{}; + +static CoreTiming::EventType* s_et_test_hw_phase2 = nullptr; static std::array s_network_command_buffer; static std::array s_network_buffer; static std::array s_allnet_buffer; @@ -214,6 +222,9 @@ static std::array s_sockets; // TODO: Verify this. static constexpr u32 FIRST_VALID_FD = 1; +// Flag: next 128-byte DMA Read from the media buffer should return network config +static bool s_netconfig_read_pending = false; + static GuestSocket GetAvailableGuestSocket() { for (u32 i = FIRST_VALID_FD; i < std::size(s_sockets); ++i) @@ -427,9 +438,28 @@ static File::IOFile OpenOrCreateFile(const std::string& filename) return File::IOFile(filename, "wb+"); } +static void TestHwPhase2Callback(Core::System& system, u64 userdata, s64 cycles_late) +{ + const bool is_exec2 = (userdata != 0); + auto& response = is_exec2 ? s_exec2_last_response : s_exec1_last_response; + + response.fill(0); + response[0] = 0x03020000; // sub_cmd=0x02, cmd_class=0x03 + response[1] = 2; // testStatus = GOOD + response[2] = 100; // checkProgress + + DEBUG_LOG_FMT(AMMEDIABOARD, + "GC-AM: TestHardware phase 2 ({}): sending result response " + "(testStatus=2, checkProgress=100)", + is_exec2 ? "Execute2" : "Execute1"); + ExpansionInterface::GenerateInterrupt(is_exec2 ? 0x10 : 0x04); +} + void Init() { s_media_buffer_32.fill(0); + s_exec1_last_response.fill(0); + s_exec2_last_response.fill(0); s_network_buffer.fill(0); s_network_command_buffer.fill(0); s_firmware.fill(-1); @@ -438,6 +468,10 @@ void Init() s_allnet_settings.fill(0); s_game_modified_ip_address = {}; + s_netconfig_read_pending = false; + + auto& core_timing = Core::System::GetInstance().GetCoreTiming(); + s_et_test_hw_phase2 = core_timing.RegisterEvent("AMMediaboardTestHwPhase2", TestHwPhase2Callback); s_board_status = LoadingGameProgram; s_load_progress = 80; @@ -1462,6 +1496,52 @@ u32 ExecuteCommand(std::array& dicmd_buf, u32* diimm_buf, u32 address, u return 0; } + // Intercept 128-byte read after GetNetworkConfig: serve network config from trinetcfg.bin + if (s_netconfig_read_pending && length == 0x80) + { + for (const auto& range : s_mediaboard_ranges) + { + if (offset >= range.start && offset < range.end) + { + s_netconfig_read_pending = false; + + u8 config[0x80] = {}; + if (s_netcfg.IsOpen()) + { + s_netcfg.Seek(0, File::SeekOrigin::Begin); + s_netcfg.ReadBytes(config, sizeof(config)); + } + + // config[0] is used as a menu table index. Entry 0 is NULL, + // which causes a NULL dereference. Default to 2 (valid entry). + if (config[0] == 0) + config[0] = 2; + + DEBUG_LOG_FMT(AMMEDIABOARD, + "GC-AM: NetConfig Read (intercepted) offset={:08x} config[0]={}", offset, + config[0]); + memory.CopyToEmu(address, config, sizeof(config)); + return 0; + } + } + } + + // Return saved response for each Execute path (they share s_media_buffer). + if (offset == DIMMCommandVersion2 && length == 0x20) + { + DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Read Execute1 response (saved)"); + memory.CopyToEmu(address, reinterpret_cast(s_exec1_last_response.data()), + sizeof(s_exec1_last_response)); + return 0; + } + if (offset == DIMMCommandVersion2_2 && length == 0x20) + { + DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Read Execute2 response (saved)"); + memory.CopyToEmu(address, reinterpret_cast(s_exec2_last_response.data()), + sizeof(s_exec2_last_response)); + return 0; + } + for (const auto& range : s_mediaboard_ranges) { if (offset >= range.start && offset < range.end) @@ -1503,9 +1583,45 @@ u32 ExecuteCommand(std::array& dicmd_buf, u32* diimm_buf, u32 address, u // Empty reply case AMMBCommand::Unknown_103: break; - case AMMBCommand::Unknown_104: + case AMMBCommand::GetNetworkConfig: s_media_buffer[4] = 1; + // The game will do a 128-byte DMA Read for the network config. + // We intercept that read and provide data from trinetcfg.bin. + s_netconfig_read_pending = true; break; + case AMMBCommand::NetworkReInit: + break; + case AMMBCommand::TestHardware: + { + // Execute2 layout: buf[1] = test type, buf[2] = string pointer + // (differs from Execute1 where they're at indices 11 and 12) + const u32 test_type = s_media_buffer_32[1]; + const u32 string_ptr = s_media_buffer_32[2]; + + DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: TestHardware (Execute2): type={:08x} str_ptr={:08x}", + test_type, string_ptr); + + // Write "TEST OK\0" to the string buffer. + // Data must be LE because the PPC display code byte-swaps when reading. + if (string_ptr != 0) + { + memory.Write_U32_Swap(0x54455354, string_ptr); + memory.Write_U32_Swap(0x204F4B00, string_ptr + 4); + } + + // Phase 1: Echo test_type back. The 0x80 flag is set below in the generic path. + s_media_buffer_32[1] = test_type; + + // Schedule phase 2 result after a short delay. + { + auto& core_timing = Core::System::GetInstance().GetCoreTiming(); + core_timing.RemoveEvent(s_et_test_hw_phase2); + constexpr s64 phase2_delay = 50000; // ~1ms at 486MHz + core_timing.ScheduleEvent(phase2_delay, s_et_test_hw_phase2, 1); // 1 = Execute2 + } + + break; + } case AMMBCommand::Accept: AMMBCommandAccept(2, NetworkCommandAddress2); break; @@ -1653,13 +1769,25 @@ u32 ExecuteCommand(std::array& dicmd_buf, u32* diimm_buf, u32 address, u NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: AllNetInit"); break; default: - ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Command:{0:04x}", static_cast(ammb_command)); - ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Command Unhandled!"); + // Commands with 0x80 in the high byte are cleanup acknowledgments. + if (static_cast(ammb_command) & 0x8000) + { + DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Cleanup command {:04x} (Execute2)", + static_cast(ammb_command)); + } + else + { + ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Command:{0:04x}", static_cast(ammb_command)); + ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Command Unhandled!"); + } break; } s_media_buffer[3] |= 0x80; // Command complete flag + // Save Execute2 response before it gets clobbered by subsequent operations + memcpy(s_exec2_last_response.data(), s_media_buffer_32.data(), sizeof(s_exec2_last_response)); + memory.Memset(address, 0, length); ExpansionInterface::GenerateInterrupt(0x10); @@ -1823,16 +1951,59 @@ u32 ExecuteCommand(std::array& dicmd_buf, u32* diimm_buf, u32 address, u case AMMBCommand::GetMediaBoardSerial: memcpy(s_media_buffer + 4, "A89E-27A50364511", 16); break; - case AMMBCommand::Unknown_104: + case AMMBCommand::GetNetworkConfig: s_media_buffer[4] = 1; break; + case AMMBCommand::TestHardware: + { + // Execute1 command buffer layout (result slot at +0x20): + // [8] = command word, [9] = test_type, [10] = string_ptr + const u32 test_type = s_media_buffer_32[9]; + const u32 string_ptr = s_media_buffer_32[10]; + + DEBUG_LOG_FMT(AMMEDIABOARD, + "GC-AM: TestHardware (Execute1 inner): type={:08x} str_ptr={:08x}", + test_type, string_ptr); + + // PPC display code reads with lwz + manual bswap32; data must be LE. + if (string_ptr != 0) + { + memory.Write_U32_Swap(0x54455354, string_ptr); + memory.Write_U32_Swap(0x204F4B00, string_ptr + 4); + } + + // Phase 1: Echo test_type back. The 0x80 flag is set below. + s_media_buffer_32[1] = test_type; + + // Schedule phase 2 via CoreTiming. + { + auto& core_timing = Core::System::GetInstance().GetCoreTiming(); + core_timing.RemoveEvent(s_et_test_hw_phase2); + constexpr s64 phase2_delay = 50000; // ~1ms at 486MHz + core_timing.ScheduleEvent(phase2_delay, s_et_test_hw_phase2, 0); // 0 = Execute1 + } + break; + } default: - PanicAlertFmtT("Unhandled Media Board Command:{0:04x}", static_cast(ammb_command)); + // Commands with 0x80 in the high byte are cleanup acknowledgments. + if (static_cast(ammb_command) & 0x8000) + { + DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Cleanup command {:04x} (Execute1)", + static_cast(ammb_command)); + } + else + { + PanicAlertFmtT("Unhandled Media Board Command:{0:04x}", static_cast(ammb_command)); + } break; } memset(s_media_buffer + 0x20, 0, 0x20); + // Save Execute1 response before it gets clobbered by Execute2 operations + memcpy(s_exec1_last_response.data(), s_media_buffer_32.data(), + sizeof(s_exec1_last_response)); + ExpansionInterface::GenerateInterrupt(0x04); return 0; } @@ -1842,6 +2013,16 @@ u32 ExecuteCommand(std::array& dicmd_buf, u32* diimm_buf, u32 address, u { if (offset >= range.start && offset < range.end) { + // Persist network config to trinetcfg.bin for SET IP ADDRESS. + // The DMA Write (0x80 bytes) arrives before the corresponding command (0x0204), + // so we detect it here by size and non-zero status byte. + if (length == 0x80 && memory.Read_U8(address) != 0 && s_netcfg.IsOpen()) + { + DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: NetConfig persist to trinetcfg.bin (status={})", + memory.Read_U8(address)); + FileWriteData(memory, &s_netcfg, 0, address, length); + } + DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Write MediaBoard ({:08x},{:08x},{:08x})", offset, range.start, length); SafeCopyFromEmu(memory, range.buffer, address, range.buffer_size, offset - range.start, @@ -1914,7 +2095,7 @@ u32 ExecuteCommand(std::array& dicmd_buf, u32* diimm_buf, u32 address, u case AMMBCommand::GetMediaBoardSerial: memcpy(s_media_buffer + 4, "A89E-27A50364511", 16); break; - case AMMBCommand::Unknown_104: + case AMMBCommand::GetNetworkConfig: s_media_buffer[4] = 1; break; case AMMBCommand::NetworkReInit: @@ -1931,8 +2112,8 @@ u32 ExecuteCommand(std::array& dicmd_buf, u32* diimm_buf, u32 address, u // On real systems it shows the status about the DIMM/GD-ROM here // We just show "TEST OK" - memory.Write_U32(0x54534554, s_media_buffer_32[12]); - memory.Write_U32(0x004B4F20, s_media_buffer_32[12] + 4); + memory.Write_U32(0x54455354, s_media_buffer_32[12]); + memory.Write_U32(0x204F4B00, s_media_buffer_32[12] + 4); s_media_buffer_32[1] = s_media_buffer_32[9]; break; @@ -2041,6 +2222,9 @@ u32 ExecuteCommand(std::array& dicmd_buf, u32* diimm_buf, u32 address, u } memset(s_media_buffer + 0x20, 0, 0x20); + + // Save Execute1 response for DI Execute path + memcpy(s_exec1_last_response.data(), s_media_buffer_32.data(), sizeof(s_exec1_last_response)); return 0; } @@ -2141,12 +2325,15 @@ void DoState(PointerWrap& p) p.Do(s_gcam_key_c); p.Do(s_firmware); p.Do(s_media_buffer_32); + p.Do(s_exec1_last_response); + p.Do(s_exec2_last_response); p.Do(s_network_command_buffer); p.Do(s_network_buffer); p.Do(s_allnet_buffer); p.Do(s_allnet_settings); p.Do(s_game_modified_ip_address); + p.Do(s_netconfig_read_pending); p.Do(s_board_status); p.Do(s_load_progress); diff --git a/Source/Core/Core/HW/DVD/AMMediaboard.h b/Source/Core/Core/HW/DVD/AMMediaboard.h index 4fa649c8bb..77cf9bce70 100644 --- a/Source/Core/Core/HW/DVD/AMMediaboard.h +++ b/Source/Core/Core/HW/DVD/AMMediaboard.h @@ -95,7 +95,7 @@ enum class AMMBCommand : u16 GetSegaBootVersion = 0x101, GetSystemFlags = 0x102, GetMediaBoardSerial = 0x103, - Unknown_104 = 0x104, + GetNetworkConfig = 0x104, NetworkReInit = 0x204,