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
This commit is contained in:
naari3 2026-02-20 00:53:15 +09:00
parent 2eeff845bd
commit 402042b9bc
2 changed files with 196 additions and 9 deletions

View File

@ -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<DiscIO::BlobReader> s_dimm_disc;
static std::array<u8, 0x200000> s_firmware;
static std::array<u32, 0xc0> s_media_buffer_32;
static u8* const s_media_buffer = reinterpret_cast<u8*>(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<u32, 8> s_exec1_last_response{};
static std::array<u32, 8> s_exec2_last_response{};
static CoreTiming::EventType* s_et_test_hw_phase2 = nullptr;
static std::array<u8, 0x4ffe00> s_network_command_buffer;
static std::array<u8, 0x80000> s_network_buffer;
static std::array<u8, 0x1000> s_allnet_buffer;
@ -214,6 +222,9 @@ static std::array<SOCKET, SOCKET_FD_MAX> 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<u32, 3>& 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<const u8*>(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<const u8*>(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<u32, 3>& 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<u32, 3>& 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<u16>(ammb_command));
ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Command Unhandled!");
// Commands with 0x80 in the high byte are cleanup acknowledgments.
if (static_cast<u16>(ammb_command) & 0x8000)
{
DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Cleanup command {:04x} (Execute2)",
static_cast<u16>(ammb_command));
}
else
{
ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Command:{0:04x}", static_cast<u16>(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<u32, 3>& 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<u16>(ammb_command));
// Commands with 0x80 in the high byte are cleanup acknowledgments.
if (static_cast<u16>(ammb_command) & 0x8000)
{
DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Cleanup command {:04x} (Execute1)",
static_cast<u16>(ammb_command));
}
else
{
PanicAlertFmtT("Unhandled Media Board Command:{0:04x}", static_cast<u16>(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<u32, 3>& 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<u32, 3>& 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<u32, 3>& 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<u32, 3>& 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);

View File

@ -95,7 +95,7 @@ enum class AMMBCommand : u16
GetSegaBootVersion = 0x101,
GetSystemFlags = 0x102,
GetMediaBoardSerial = 0x103,
Unknown_104 = 0x104,
GetNetworkConfig = 0x104,
NetworkReInit = 0x204,