IOS LLE: checkpoint NAND conformance and diagnostic tooling
Preserve formatted NAND boot and Wii Remote pairing, add DMA/SRAM conformance tests and a Wii comparison probe. Keep P1 profiling tools and native update A/B evidence; reject the unhelpful targeted SRAM shortcut.
This commit is contained in:
1 parent
b931671fde
commit
c230971f07
64 files changed
+6488
-238
No files matched your search
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
@@ -12,6 +13,7 @@
|
||||
|
||||
#include "Common/ChunkFile.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Swap.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/WII_IPC.h"
|
||||
#include "Core/IOS/Starlet/ARMCore.h"
|
||||
@@ -176,6 +178,7 @@ public:
|
||||
|
||||
const u8* GetDirectMemoryPointer(u32 address, u32 size) const override
|
||||
{
|
||||
++m_direct_pointer_count;
|
||||
const size_t offset = ToOffset(address);
|
||||
if (size == 0 || offset > m_memory.size() || size > m_memory.size() - offset)
|
||||
return nullptr;
|
||||
@@ -192,7 +195,6 @@ public:
|
||||
void SetSRAMFastmemEnabled(bool enabled) { m_sram_fastmem_enabled = enabled; }
|
||||
void SetBoot0Mapped(bool mapped) { m_boot0_mapped = mapped; }
|
||||
void SetSRAMSplitMode(bool split) { m_sram_split_mode = split; }
|
||||
|
||||
void WriteSRAM32(u32 offset, u32 value)
|
||||
{
|
||||
ASSERT_LT(offset + 3, SRAM_SIZE);
|
||||
@@ -241,6 +243,7 @@ public:
|
||||
u64 GetRead8Count() const { return m_read8_count; }
|
||||
u64 GetRead16Count() const { return m_read16_count; }
|
||||
u64 GetRead32Count() const { return m_read32_count; }
|
||||
u64 GetDirectPointerCount() const { return m_direct_pointer_count; }
|
||||
u32 GetMMIOWord() const { return m_mmio_word; }
|
||||
void ResetReadCounts()
|
||||
{
|
||||
@@ -270,6 +273,7 @@ private:
|
||||
u64 m_read8_count = 0;
|
||||
u64 m_read16_count = 0;
|
||||
u64 m_read32_count = 0;
|
||||
mutable u64 m_direct_pointer_count = 0;
|
||||
u32 m_mmio_word = 0;
|
||||
bool m_idle_poll_safe = true;
|
||||
bool m_slice_stable_poll_safe = false;
|
||||
@@ -2662,7 +2666,7 @@ TEST(StarletARMCore, ThumbJitBusMemoryPreservesBoot0AndSRAMHoles)
|
||||
{
|
||||
SCOPED_TRACE(address);
|
||||
TestBus bus;
|
||||
ARMCore core(bus);
|
||||
ARMCore core(bus, true);
|
||||
bus.SetFastmemEnabled(true);
|
||||
bus.SetSRAMFastmemEnabled(true);
|
||||
bus.SetBoot0Mapped(true);
|
||||
@@ -2735,7 +2739,7 @@ TEST(StarletARMCore, ARMJitProfilesSlowSRAMPagesByDirection)
|
||||
{
|
||||
#if defined(_M_X86_64)
|
||||
TestBus bus;
|
||||
ARMCore core(bus);
|
||||
ARMCore core(bus, true);
|
||||
bus.SetFastmemEnabled(true);
|
||||
bus.SetSRAMFastmemEnabled(true);
|
||||
bus.SetBoot0Mapped(false);
|
||||
@@ -2764,7 +2768,7 @@ TEST(StarletARMCore, ARMJitKeepsSplitSRAMMirrorWritesOnExactBusPath)
|
||||
{
|
||||
#if defined(_M_X86_64)
|
||||
TestBus bus;
|
||||
ARMCore core(bus);
|
||||
ARMCore core(bus, true);
|
||||
bus.SetFastmemEnabled(true);
|
||||
bus.SetSRAMFastmemEnabled(true);
|
||||
bus.SetBoot0Mapped(false);
|
||||
@@ -3377,6 +3381,56 @@ TEST(StarletARMCore, MMUSectionTranslation)
|
||||
// The first fetch walks the section table; the second hits the 1 KiB
|
||||
// translation cache.
|
||||
EXPECT_EQ(bus.GetRead32Count(), 3u);
|
||||
EXPECT_EQ(bus.GetDirectPointerCount(), 0u);
|
||||
}
|
||||
|
||||
TEST(StarletARMCore, InterpreterPageWalksUseBusWithoutResolvingValidationPointers)
|
||||
{
|
||||
for (const bool big_endian : {false, true})
|
||||
{
|
||||
// Section; coarse large/small/extended-small; fine large/small/tiny pages.
|
||||
for (const auto [first_type, second_type] :
|
||||
{std::pair{2u, 0u}, {1u, 1u}, {1u, 2u}, {1u, 3u}, {3u, 1u}, {3u, 2u}, {3u, 3u}})
|
||||
{
|
||||
SCOPED_TRACE(testing::Message()
|
||||
<< "be=" << big_endian << " first=" << first_type << " second=" << second_type);
|
||||
TestBus bus(0x20000);
|
||||
ARMCore core(bus);
|
||||
core.SetBigEndian(big_endian);
|
||||
const auto word = [&](u32 address, u32 value) {
|
||||
bus.WriteARM(address, big_endian ? value : Common::swap32(value));
|
||||
};
|
||||
word(0, 0xe5921000); // ldr r1, [r2]
|
||||
word(4, 0xe5923000); // ldr r3, [r2]: hit both TLB and instruction cache
|
||||
word(0x4000, 0x00000c02); // Identity-map code.
|
||||
constexpr u32 virtual_address = 0x80012344;
|
||||
constexpr u32 physical_address = 0x12344;
|
||||
if (first_type == 2)
|
||||
{
|
||||
word(0x6000, 0x00000c02);
|
||||
}
|
||||
else
|
||||
{
|
||||
word(0x6000, 0x8000 | first_type);
|
||||
const u32 index =
|
||||
first_type == 1 ? (virtual_address >> 10) & 0x3fc : (virtual_address >> 8) & 0xffc;
|
||||
const u32 mask = second_type == 1 ? 0xffff0000u :
|
||||
(first_type == 3 && second_type == 3) ? 0xfffffc00u :
|
||||
0xfffff000u;
|
||||
const u32 permissions = first_type == 3 && second_type == 3 ? 0x30 : 0xff0;
|
||||
word(0x8000 + index, (physical_address & mask) | permissions | second_type);
|
||||
}
|
||||
word(physical_address, 0x12345678);
|
||||
core.GetCP15State().translation_table_base = 0x4000;
|
||||
core.GetCP15State().control |= 1;
|
||||
core.SetRegister(2, virtual_address);
|
||||
core.Run(2);
|
||||
EXPECT_EQ(core.GetRegister(1), 0x12345678u);
|
||||
EXPECT_EQ(core.GetRegister(3), 0x12345678u);
|
||||
EXPECT_EQ(bus.GetDirectPointerCount(), 0u);
|
||||
EXPECT_EQ(bus.GetRead32Count(), first_type == 2 ? 6u : 7u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(StarletARMCore, JitCachesProvisionalIdentityTranslationForFaultPages)
|
||||
@@ -3402,6 +3456,7 @@ TEST(StarletARMCore, JitCachesProvisionalIdentityTranslationForFaultPages)
|
||||
EXPECT_EQ(core.GetRegister(3), 0x12345678u);
|
||||
EXPECT_EQ(core.GetJitFallbackInstructionCount(), 0u);
|
||||
EXPECT_EQ(core.GetJitNativeExecutedInstructions(), 3u);
|
||||
EXPECT_GT(bus.GetDirectPointerCount(), 0u); // Native descriptor validation is still present.
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Common/Config/Config.h"
|
||||
#include "Common/Crypto/SHA1.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Core/Config/MainSettings.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/Memmap.h"
|
||||
#include "Core/HW/WII_IPC.h"
|
||||
#include "Core/IOS/Starlet/StarletMemory.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
namespace IOS::LLE
|
||||
{
|
||||
// Compare the block helpers with the original byte bus, using synthetic memory only.
|
||||
class StarletDMATest : public testing::Test
|
||||
{
|
||||
protected:
|
||||
static void SetUpTestSuite()
|
||||
{
|
||||
Config::Init();
|
||||
SConfig::Init();
|
||||
auto& system = Core::System::GetInstance();
|
||||
system.SetIsWii(true);
|
||||
system.GetMemory().Init();
|
||||
Core::DeclareAsCPUThread();
|
||||
}
|
||||
|
||||
static void TearDownTestSuite()
|
||||
{
|
||||
auto& system = Core::System::GetInstance();
|
||||
system.GetMemory().Shutdown();
|
||||
system.SetIsWii(false);
|
||||
Core::UndeclareAsCPUThread();
|
||||
SConfig::Shutdown();
|
||||
Config::Shutdown();
|
||||
}
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
auto& system = Core::System::GetInstance();
|
||||
system.GetWiiIPC().Reset();
|
||||
m_memory = std::make_unique<StarletMemory>(system);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
m_memory.reset();
|
||||
if (!m_temp_directory.empty())
|
||||
File::DeleteDirRecursively(m_temp_directory); // Only this fixture's unique temporary SD.
|
||||
}
|
||||
|
||||
std::vector<u8> ReadBytes(u32 address, size_t size)
|
||||
{
|
||||
std::vector<u8> result(size);
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
result[i] = m_memory->ReadDMA8(address + static_cast<u32>(i));
|
||||
return result;
|
||||
}
|
||||
|
||||
void WriteBytes(u32 address, std::span<const u8> data)
|
||||
{
|
||||
for (size_t i = 0; i < data.size(); ++i)
|
||||
m_memory->WriteDMA8(address + static_cast<u32>(i), data[i]);
|
||||
}
|
||||
|
||||
void CompareRanges()
|
||||
{
|
||||
for (const bool split : {false, true})
|
||||
{
|
||||
for (const bool boot0 : {false, true})
|
||||
{
|
||||
m_memory->m_sram_split_mode = split;
|
||||
m_memory->m_boot0_mapped = boot0;
|
||||
for (const u32 address :
|
||||
{0U, 1U, 0x10003U, 0x017ffff0U, 0x01800000U, 0x0fffffffU,
|
||||
0x10000000U, 0x10000001U, 0x13fffff0U, 0x14000000U, 0x0d3ffff0U, 0x0d400000U,
|
||||
0x0d400003U, 0x0d40fff0U, 0x0d410000U, 0x0d417ff0U, 0x0d418000U, 0x0d41fff0U,
|
||||
0xfff00000U, 0xfff0fff0U, 0xfff17ff0U, 0xfffefff0U, 0xffff0000U, 0xffff7ff0U,
|
||||
0xfffffff0U, 0x0d800020U})
|
||||
{
|
||||
for (const size_t size : {0U, 1U, 2U, 3U, 4U, 7U, 16U, 63U, 512U, 2048U, 8192U})
|
||||
{
|
||||
SCOPED_TRACE(testing::Message() << "address=" << address << " size=" << size
|
||||
<< " split=" << split << " boot0=" << boot0);
|
||||
std::vector<u8> initial(size + 32);
|
||||
std::vector<u8> payload(size);
|
||||
for (size_t i = 0; i < initial.size(); ++i)
|
||||
initial[i] = static_cast<u8>((i * 37) ^ (i >> 8));
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
payload[i] = static_cast<u8>((i * 13) ^ (i >> 7) ^ 0xab);
|
||||
WriteBytes(address - 16, initial);
|
||||
std::vector<u8> actual(size);
|
||||
m_memory->ReadDMABuffer(address, actual);
|
||||
ASSERT_EQ(actual, ReadBytes(address, size));
|
||||
|
||||
WriteBytes(address, payload);
|
||||
const auto expected = ReadBytes(address - 16, size + 32);
|
||||
const auto expected_sram = m_memory->m_sram;
|
||||
WriteBytes(address - 16, initial);
|
||||
m_memory->WriteDMABuffer(address, payload);
|
||||
ASSERT_EQ(ReadBytes(address - 16, size + 32), expected);
|
||||
ASSERT_EQ(m_memory->m_sram, expected_sram);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckPointerGuards()
|
||||
{
|
||||
for (const u32 address : {0U, 0x10000000U, 0x0d400000U, 0x0d410000U})
|
||||
{
|
||||
EXPECT_NE(m_memory->GetDMAMemoryPointer(address, 16), nullptr);
|
||||
EXPECT_EQ(m_memory->GetDMAMemoryPointer(address, std::numeric_limits<size_t>::max()),
|
||||
nullptr);
|
||||
}
|
||||
for (const u32 address : {0x017ffff0U, 0x13fffff0U, 0x0d417ff0U})
|
||||
{
|
||||
EXPECT_NE(m_memory->GetDMAMemoryPointer(address, 16), nullptr);
|
||||
EXPECT_EQ(m_memory->GetDMAMemoryPointer(address, 17), nullptr);
|
||||
}
|
||||
for (const u32 address : {0x0d418000U, 0x0d800038U, 0xfff00000U, 0xfffffff0U})
|
||||
EXPECT_EQ(m_memory->GetDMAMemoryPointer(address, 4), nullptr);
|
||||
}
|
||||
|
||||
void CheckOHCI()
|
||||
{
|
||||
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
|
||||
{
|
||||
// The second physical page can precede the first and need not be adjacent.
|
||||
const u32 first = base + 0x3fe3;
|
||||
const u32 end = base + 0x0017;
|
||||
std::vector<u8> payload(70);
|
||||
for (size_t i = 0; i < payload.size(); ++i)
|
||||
payload[i] = static_cast<u8>(i + 1);
|
||||
WriteBytes(first - 1, std::vector<u8>(100, 0xcc));
|
||||
WriteBytes(base - 1, std::vector<u8>(100, 0xdd));
|
||||
m_memory->WriteOHCIBuffer(first, end, payload.data(), payload.size());
|
||||
payload.resize(29 + 24);
|
||||
EXPECT_EQ(m_memory->ReadOHCIBuffer(first, end), payload);
|
||||
EXPECT_EQ(ReadBytes(first - 1, 1)[0], 0xcc);
|
||||
EXPECT_EQ(ReadBytes(first + 29, 1)[0], 0xcc);
|
||||
EXPECT_EQ(ReadBytes(base - 1, 1)[0], 0xdd);
|
||||
EXPECT_EQ(ReadBytes(end + 1, 1)[0], 0xdd);
|
||||
// Short packet must not fill the remainder of either page.
|
||||
m_memory->WriteOHCIBuffer(first, end, payload.data(), 3);
|
||||
EXPECT_EQ(m_memory->ReadOHCIBuffer(first, first + 2), (std::vector<u8>{1, 2, 3}));
|
||||
m_memory->WriteOHCIMemory32(base + 1, 0x12345678);
|
||||
EXPECT_EQ(ReadBytes(base + 1, 4), (std::vector<u8>{0x78, 0x56, 0x34, 0x12}));
|
||||
EXPECT_EQ(m_memory->ReadOHCIMemory32(base + 1), 0x12345678U);
|
||||
}
|
||||
EXPECT_TRUE(m_memory->ReadOHCIBuffer(0, 0x1234).empty());
|
||||
EXPECT_TRUE(m_memory->ReadOHCIBuffer(0x1234, 0x1233).empty());
|
||||
}
|
||||
|
||||
void CheckNAND()
|
||||
{
|
||||
StarletMemory::NANDPage raw;
|
||||
for (size_t i = 0; i < raw.size(); ++i)
|
||||
raw[i] = static_cast<u8>((i * 37) ^ (i >> 8));
|
||||
m_memory->m_nand_overlay[7] = raw;
|
||||
m_memory->m_nand_read_pending = true;
|
||||
m_memory->m_nand_read_page = 7;
|
||||
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
|
||||
{
|
||||
const u32 spare = base + 0x2000;
|
||||
m_memory->WriteRegister(0x0d010010, base);
|
||||
m_memory->WriteRegister(0x0d010014, spare);
|
||||
m_memory->WriteRegister(0x0d01000c, 7);
|
||||
for (const u32 column : {0U, 1U, 0x7f0U, 0x800U, 0x830U})
|
||||
{
|
||||
for (const u32 length : {1U, 0x840U - column})
|
||||
{
|
||||
SCOPED_TRACE(testing::Message()
|
||||
<< "base=" << base << " column=" << column << " length=" << length);
|
||||
WriteBytes(base, std::vector<u8>(0x900, 0xcc));
|
||||
WriteBytes(spare, std::vector<u8>(0x100, 0xdd));
|
||||
m_memory->m_nand_read_column = column;
|
||||
ASSERT_TRUE(m_memory->ReadNANDPage(0x2000 | length));
|
||||
// Independent old byte-address formula, including spare-only reads via NAND_DATA.
|
||||
for (u32 i = 0; i < length; ++i)
|
||||
{
|
||||
const u32 address =
|
||||
column < 0x800 && column + i >= 0x800 ? spare + column + i - 0x800 : base + i;
|
||||
ASSERT_EQ(m_memory->ReadDMA8(address), raw[column + i]);
|
||||
}
|
||||
m_memory->WriteRegister(0x0d010008, column);
|
||||
ASSERT_TRUE(m_memory->StageNANDProgram(0x4000 | length, false));
|
||||
auto expected = raw;
|
||||
std::fill(expected.begin(), expected.begin() + column, 0xff);
|
||||
std::fill(expected.begin() + column + length, expected.end(), 0xff);
|
||||
EXPECT_EQ(m_memory->m_nand_program_data, expected);
|
||||
}
|
||||
}
|
||||
// ECC output remains separate from the transferred spare bytes.
|
||||
m_memory->m_nand_read_column = 0;
|
||||
ASSERT_TRUE(m_memory->ReadNANDPage(0x3000 | 0x840));
|
||||
const auto ecc = StarletMemory::CalculateNANDECC(raw.data());
|
||||
EXPECT_EQ(ReadBytes(spare ^ 0x40, ecc.size()), (std::vector<u8>(ecc.begin(), ecc.end())));
|
||||
EXPECT_EQ(ReadBytes(spare, 0x40), (std::vector<u8>(raw.begin() + 0x800, raw.end())));
|
||||
m_memory->WriteRegister(0x0d010008, 0);
|
||||
ASSERT_TRUE(m_memory->StageNANDProgram(0x5800, false));
|
||||
EXPECT_TRUE(m_memory->m_nand_program_ecc_enabled);
|
||||
EXPECT_EQ(ReadBytes(spare ^ 0x40, ecc.size()), (std::vector<u8>(ecc.begin(), ecc.end())));
|
||||
// Random spare input uses NAND_DATA and preserves the already staged page data.
|
||||
WriteBytes(base, std::span(raw).subspan(0x800));
|
||||
m_memory->WriteRegister(0x0d010008, 0x800);
|
||||
ASSERT_TRUE(m_memory->StageNANDProgram(0x4040, true));
|
||||
EXPECT_EQ(m_memory->m_nand_program_data, raw);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckOverlappingAES()
|
||||
{
|
||||
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
|
||||
{
|
||||
for (const u32 offset : {0U, 16U})
|
||||
{
|
||||
std::vector<u8> input(2048);
|
||||
for (size_t i = 0; i < input.size(); ++i)
|
||||
input[i] = static_cast<u8>((i * 37) ^ (i >> 8));
|
||||
WriteBytes(base, input);
|
||||
m_memory->Write32(0x0d020004, base);
|
||||
m_memory->Write32(0x0d020008, base + offset);
|
||||
m_memory->Write32(0x0d020000, 0x8000007f);
|
||||
EXPECT_EQ(ReadBytes(base + offset, input.size()), input);
|
||||
EXPECT_EQ(m_memory->Read32(0x0d020004), base + input.size());
|
||||
EXPECT_EQ(m_memory->Read32(0x0d020008), base + offset + input.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckSHA()
|
||||
{
|
||||
std::vector<u8> input(257);
|
||||
for (size_t i = 0; i < input.size(); ++i)
|
||||
input[i] = static_cast<u8>(i * 37);
|
||||
const auto digest = Common::SHA1::CalculateDigest(input);
|
||||
input.resize(320, 0);
|
||||
input[257] = 0x80;
|
||||
input[318] = 0x08;
|
||||
input[319] = 0x08; // 257 * 8 bits, big endian.
|
||||
constexpr std::array<u32, 5> initial = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476,
|
||||
0xc3d2e1f0};
|
||||
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
|
||||
{
|
||||
WriteBytes(base, input);
|
||||
for (u32 i = 0; i < initial.size(); ++i)
|
||||
m_memory->Write32(0x0d030008 + i * 4, initial[i]);
|
||||
m_memory->Write32(0x0d030004, base);
|
||||
m_memory->Write32(0x0d030000, 0x80000004); // Five blocks.
|
||||
EXPECT_EQ(ReadBytes(0x0d030008, digest.size()),
|
||||
(std::vector<u8>(digest.begin(), digest.end())));
|
||||
EXPECT_EQ(m_memory->Read32(0x0d030004), base + input.size());
|
||||
}
|
||||
}
|
||||
|
||||
void CheckSD()
|
||||
{
|
||||
m_temp_directory = File::CreateTempDir();
|
||||
ASSERT_FALSE(m_temp_directory.empty());
|
||||
ASSERT_TRUE(m_memory->m_sd_card.Open(m_temp_directory + "/dma-test.raw", "w+b"));
|
||||
std::vector<u8> image(4096);
|
||||
for (size_t i = 0; i < image.size(); ++i)
|
||||
image[i] = static_cast<u8>((i * 37) ^ (i >> 8));
|
||||
ASSERT_TRUE(m_memory->m_sd_card.WriteBytes(image.data(), image.size()));
|
||||
ASSERT_TRUE(m_memory->m_sd_card.Flush());
|
||||
m_memory->m_sd_card_inserted = true;
|
||||
for (const bool high_capacity : {false, true})
|
||||
{
|
||||
m_memory->m_sd_card_high_capacity = high_capacity;
|
||||
const u32 argument = high_capacity ? 1 : 512;
|
||||
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
|
||||
{
|
||||
ASSERT_TRUE(m_memory->TransferSDCardBlocks(true, argument, 512, 3, base));
|
||||
EXPECT_EQ(ReadBytes(base, 1536),
|
||||
(std::vector<u8>(image.begin() + 512, image.begin() + 2048)));
|
||||
EXPECT_EQ(m_memory->ReadRegister(0x0d070000), base + 1536);
|
||||
Config::SetCurrent(Config::MAIN_ALLOW_SD_WRITES, false);
|
||||
EXPECT_FALSE(m_memory->TransferSDCardBlocks(false, argument, 512, 3, base));
|
||||
Config::SetCurrent(Config::MAIN_ALLOW_SD_WRITES, true);
|
||||
auto changed = ReadBytes(base, 1536);
|
||||
for (auto& byte : changed)
|
||||
byte ^= 0x5a;
|
||||
WriteBytes(base, changed);
|
||||
ASSERT_TRUE(m_memory->TransferSDCardBlocks(false, argument, 512, 3, base));
|
||||
std::copy(changed.begin(), changed.end(), image.begin() + 512);
|
||||
ASSERT_TRUE(m_memory->m_sd_card.Seek(0, File::SeekOrigin::Begin));
|
||||
std::vector<u8> actual(image.size());
|
||||
ASSERT_TRUE(m_memory->m_sd_card.ReadBytes(actual.data(), actual.size()));
|
||||
EXPECT_EQ(actual, image);
|
||||
EXPECT_FALSE(m_memory->TransferSDCardBlocks(true, argument, 4096, 1, base));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckCPUSRAMReads()
|
||||
{
|
||||
for (size_t i = 0; i < m_memory->m_boot_rom.size(); ++i)
|
||||
m_memory->m_boot_rom[i] = static_cast<u8>((i * 29) ^ (i >> 5) ^ 0xa5);
|
||||
for (size_t i = 0; i < m_memory->m_sram.size(); ++i)
|
||||
m_memory->m_sram[i] = static_cast<u8>((i * 13) ^ (i >> 8) ^ 0x5a);
|
||||
// The byte CPU path is unchanged by the aligned wide-read optimization. Use it as the
|
||||
// reference, including nonzero boot ROM, all mirrors, live mapping changes and wrap to MEM1.
|
||||
for (const bool split : {false, true, false})
|
||||
{
|
||||
for (const bool boot0 : {false, true, false})
|
||||
{
|
||||
m_memory->Write32(0x0d800060, split ? 0x20 : 0);
|
||||
m_memory->Write32(0x0d80018c, boot0 ? 0 : 0x1000);
|
||||
for (const u32 base : {0x0d400000U, 0xfff00000U, 0xfff20000U, 0xfff40000U, 0xfff60000U,
|
||||
0xfff80000U, 0xfffa0000U, 0xfffc0000U, 0xfffe0000U})
|
||||
{
|
||||
for (u32 page = 0; page < 32; ++page)
|
||||
{
|
||||
for (const u32 offset : {0U, 1U, 2U, 3U, 0xffcU, 0xffdU, 0xffeU, 0xfffU})
|
||||
{
|
||||
const u32 address = base + page * 0x1000 + offset;
|
||||
const u16 expected16 =
|
||||
static_cast<u16>((m_memory->Read8(address) << 8) | m_memory->Read8(address + 1));
|
||||
const u32 expected32 = (static_cast<u32>(expected16) << 16) |
|
||||
(m_memory->Read8(address + 2) << 8) |
|
||||
m_memory->Read8(address + 3);
|
||||
ASSERT_EQ(m_memory->Read16(address), expected16)
|
||||
<< "address=" << address << " split=" << split << " boot0=" << boot0;
|
||||
ASSERT_EQ(m_memory->Read32(address), expected32)
|
||||
<< "address=" << address << " split=" << split << " boot0=" << boot0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckVersionReads()
|
||||
{
|
||||
constexpr u32 version = 0x0d800214;
|
||||
m_memory->Reset();
|
||||
ASSERT_EQ(m_memory->Read32(version), 0x11U);
|
||||
for (const u32 value : {0x12345678U, 0xdeadbeefU, 0U, 0xffffffffU})
|
||||
{
|
||||
m_memory->Write32(version, value);
|
||||
for (u32 i = 0; i < 4; ++i)
|
||||
EXPECT_EQ(m_memory->Read8(version + i), static_cast<u8>(value >> (24 - i * 8)));
|
||||
EXPECT_EQ(m_memory->Read32(version), value);
|
||||
m_memory->Write8(version + 1, 0x5a);
|
||||
EXPECT_EQ(m_memory->Read32(version), (value & 0xff00ffffU) | 0x005a0000U);
|
||||
}
|
||||
m_memory->Reset();
|
||||
EXPECT_EQ(m_memory->Read32(version), 0x11U);
|
||||
}
|
||||
|
||||
std::unique_ptr<StarletMemory> m_memory;
|
||||
std::string m_temp_directory;
|
||||
};
|
||||
|
||||
TEST_F(StarletDMATest, BuffersMatchByteBusAcrossMappingsBoundariesAndWrap)
|
||||
{
|
||||
CompareRanges();
|
||||
}
|
||||
|
||||
TEST_F(StarletDMATest, DirectRangesExcludeHolesRegistersMirrorsAndOverflow)
|
||||
{
|
||||
CheckPointerGuards();
|
||||
}
|
||||
|
||||
TEST_F(StarletDMATest, OHCIUsesTwoPhysicalPagesAndLittleEndianDescriptors)
|
||||
{
|
||||
CheckOHCI();
|
||||
}
|
||||
|
||||
TEST_F(StarletDMATest, NANDSeparatesDataSpareAndECCIncludingPartialTransfers)
|
||||
{
|
||||
CheckNAND();
|
||||
}
|
||||
|
||||
TEST_F(StarletDMATest, AESStagesAllInputBeforeWritingOverlappingOutput)
|
||||
{
|
||||
CheckOverlappingAES();
|
||||
}
|
||||
|
||||
TEST_F(StarletDMATest, SHAMultipleBlocksMatchDigestInEveryPhysicalBank)
|
||||
{
|
||||
CheckSHA();
|
||||
}
|
||||
|
||||
TEST_F(StarletDMATest, SDTransfersPreserveOffsetsContentsAndWriteProtection)
|
||||
{
|
||||
CheckSD();
|
||||
}
|
||||
|
||||
TEST_F(StarletDMATest, CPUWideSRAMReadsMatchBytesWithBootROMAndEveryMirror)
|
||||
{
|
||||
CheckCPUSRAMReads();
|
||||
}
|
||||
|
||||
TEST_F(StarletDMATest, VersionWordReadsPreserveLiveByteBackingAndReset)
|
||||
{
|
||||
CheckVersionReads();
|
||||
}
|
||||
} // namespace IOS::LLE
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Common/Crypto/AES.h"
|
||||
#include "Common/Crypto/HMAC.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/WII_IPC.h"
|
||||
#include "Core/IOS/Starlet/StarletMemory.h"
|
||||
#include "Core/IOS/USB/Bluetooth/BTBase.h"
|
||||
#include "Core/System.h"
|
||||
#include "DiscIO/NANDImporter.h"
|
||||
|
||||
namespace IOS::LLE
|
||||
{
|
||||
class StarletPairingTest : public testing::Test
|
||||
{
|
||||
protected:
|
||||
using Superblock = DiscIO::NANDImporter::NANDSuperblock;
|
||||
static constexpr u32 SUPERBLOCK_START = 0x1fc00000;
|
||||
static constexpr u16 SYSCONF_CLUSTER = 0x20;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
Core::DeclareAsCPUThread();
|
||||
auto& system = Core::System::GetInstance();
|
||||
system.GetWiiIPC().Reset();
|
||||
m_memory = std::make_unique<StarletMemory>(system);
|
||||
m_memory->Reset();
|
||||
m_superblock = std::make_unique<Superblock>();
|
||||
m_superblock->magic = {'S', 'F', 'F', 'S'};
|
||||
m_superblock->version = 1;
|
||||
for (auto& entry : m_superblock->fst)
|
||||
{
|
||||
entry.sub = 0xffff;
|
||||
entry.sib = 0xffff;
|
||||
}
|
||||
m_superblock->fst[0].mode = 2;
|
||||
m_superblock->fat[SYSCONF_CLUSTER] = 0xfffb;
|
||||
// All sixteen raw superblock copies can be read, but only copy zero is valid.
|
||||
const std::vector<u8> empty(16 * sizeof(Superblock));
|
||||
PutLogicalData(SUPERBLOCK_START, empty);
|
||||
}
|
||||
|
||||
void PutLogicalData(u32 offset, std::span<const u8> bytes)
|
||||
{
|
||||
ASSERT_EQ(offset % 0x800, 0u);
|
||||
ASSERT_EQ(bytes.size() % 0x800, 0u);
|
||||
for (u32 i = 0; i < bytes.size(); i += 0x800)
|
||||
{
|
||||
NANDJournal::Page page{};
|
||||
std::copy_n(bytes.begin() + i, 0x800, page.begin());
|
||||
m_memory->m_nand_overlay.insert_or_assign((offset + i) / 0x800, page);
|
||||
}
|
||||
}
|
||||
|
||||
void AddPath(u32 depth)
|
||||
{
|
||||
constexpr std::array<std::string_view, 3> names = {"shared2", "sys", "SYSCONF"};
|
||||
for (u32 i = 0; i < depth; ++i)
|
||||
{
|
||||
m_superblock->fst[i].sub = static_cast<u16>(i + 1);
|
||||
auto& child = m_superblock->fst[i + 1];
|
||||
std::ranges::copy(names[i], child.name);
|
||||
child.mode = i == 2 ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
void SetSysconf(bool with_pairings)
|
||||
{
|
||||
AddPath(3);
|
||||
m_superblock->fst[3].sub = SYSCONF_CLUSTER;
|
||||
m_superblock->fst[3].size = 0x4000;
|
||||
std::vector<u8> plain(0x4000);
|
||||
std::ranges::copy(std::array{'S', 'C', 'v', '0'}, plain.begin());
|
||||
if (with_pairings)
|
||||
{
|
||||
// Valid but empty BT.DINF: this takes the existing auto-pairing write path.
|
||||
const auto pairing = IOS::HLE::GetEmulatedWiimotePairingData();
|
||||
plain[5] = 1;
|
||||
plain[7] = 0x10;
|
||||
plain[0x10] = 0x26;
|
||||
constexpr std::string_view name = "BT.DINF";
|
||||
std::ranges::copy(name, plain.begin() + 0x11);
|
||||
plain[0x18] = static_cast<u8>((pairing.size() - 1) >> 8);
|
||||
plain[0x19] = static_cast<u8>(pairing.size() - 1);
|
||||
}
|
||||
const std::array<u8, 16> synthetic_key{};
|
||||
auto aes = Common::AES::CreateContextEncrypt(synthetic_key.data());
|
||||
std::vector<u8> encrypted(plain.size());
|
||||
ASSERT_TRUE(aes->CryptIvZero(plain.data(), encrypted.data(), plain.size()));
|
||||
PutLogicalData(SYSCONF_CLUSTER * 0x4000, encrypted);
|
||||
}
|
||||
|
||||
void StoreSuperblock(u32 copy = 0)
|
||||
{
|
||||
PutLogicalData(SUPERBLOCK_START + copy * sizeof(Superblock),
|
||||
{reinterpret_cast<const u8*>(m_superblock.get()), sizeof(Superblock)});
|
||||
}
|
||||
|
||||
void CheckWithoutWrites(bool expected)
|
||||
{
|
||||
StoreSuperblock();
|
||||
const auto before = m_memory->m_nand_overlay;
|
||||
EXPECT_EQ(m_memory->InstallEmulatedWiimotePairings(), expected);
|
||||
EXPECT_TRUE(m_memory->m_nand_overlay == before);
|
||||
}
|
||||
|
||||
void RemoveFirstSuperblockPage() { m_memory->m_nand_overlay.erase(SUPERBLOCK_START / 0x800); }
|
||||
|
||||
void UseUnwritableJournal() { m_memory->m_nand_journal = std::make_unique<NANDJournal>(); }
|
||||
|
||||
bool Install() { return m_memory->InstallEmulatedWiimotePairings(); }
|
||||
|
||||
NANDJournal::Page ReadSysconfPage(u32 page)
|
||||
{
|
||||
NANDJournal::Page raw{};
|
||||
EXPECT_TRUE(m_memory->ReadRawNANDPage(SYSCONF_CLUSTER * 8 + page, &raw));
|
||||
return raw;
|
||||
}
|
||||
|
||||
static std::array<u8, 16> CalculateECC(const NANDJournal::Page& page)
|
||||
{
|
||||
return StarletMemory::CalculateNANDECC(page.data());
|
||||
}
|
||||
|
||||
void MarkSysconfSpare()
|
||||
{
|
||||
for (u32 page = 0; page < 8; ++page)
|
||||
{
|
||||
auto& raw = m_memory->m_nand_overlay.at(SYSCONF_CLUSTER * 8 + page);
|
||||
for (size_t i = 0; i < 0x30; ++i)
|
||||
raw[0x800 + i] = i == 0 ? 0xff : static_cast<u8>(0x40 + i + page);
|
||||
}
|
||||
}
|
||||
std::unique_ptr<StarletMemory> m_memory;
|
||||
std::unique_ptr<Superblock> m_superblock;
|
||||
};
|
||||
|
||||
TEST_F(StarletPairingTest, MissingShared2DoesNotPreventBootOrWriteNAND)
|
||||
{
|
||||
CheckWithoutWrites(true);
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, MissingSysDoesNotPreventBootOrWriteNAND)
|
||||
{
|
||||
AddPath(1);
|
||||
CheckWithoutWrites(true);
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, MissingSysconfDoesNotPreventBootOrWriteNAND)
|
||||
{
|
||||
AddPath(2);
|
||||
CheckWithoutWrites(true);
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, AbsentBluetoothEntryDoesNotPreventBootOrWriteNAND)
|
||||
{
|
||||
SetSysconf(false);
|
||||
CheckWithoutWrites(true);
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, UnreadableNANDRemainsAnError)
|
||||
{
|
||||
StoreSuperblock();
|
||||
RemoveFirstSuperblockPage();
|
||||
EXPECT_FALSE(Install());
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, MalformedDirectoryRemainsAnError)
|
||||
{
|
||||
m_superblock->fst[0].sub = DiscIO::NANDImporter::FSTEntryCount;
|
||||
CheckWithoutWrites(false);
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, CyclicSiblingListFailsInsteadOfHanging)
|
||||
{
|
||||
m_superblock->fst[0].sub = 1;
|
||||
m_superblock->fst[1].sib = 1;
|
||||
CheckWithoutWrites(false);
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, NewestFormattedSuperblockDoesNotRestoreOlderPairings)
|
||||
{
|
||||
SetSysconf(true);
|
||||
StoreSuperblock();
|
||||
// The formatter's newest root has no shared2, although an old copy still has it.
|
||||
m_superblock->version = 2;
|
||||
m_superblock->fst[0].sub = 0xffff;
|
||||
StoreSuperblock(1);
|
||||
// Keep the older copy and all raw data intact while selecting the newer root.
|
||||
EXPECT_TRUE(Install());
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, EmptyExistingSysconfRemainsAnError)
|
||||
{
|
||||
AddPath(3);
|
||||
CheckWithoutWrites(false);
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, FailedPairingWriteRemainsAnErrorAndDoesNotAlterOverlay)
|
||||
{
|
||||
SetSysconf(true);
|
||||
UseUnwritableJournal();
|
||||
CheckWithoutWrites(false);
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, ExistingEmptyPairingRecordIsStillPopulated)
|
||||
{
|
||||
SetSysconf(true);
|
||||
StoreSuperblock();
|
||||
EXPECT_TRUE(Install());
|
||||
// Once installed, the trusted identities are reused without another NAND write.
|
||||
CheckWithoutWrites(true);
|
||||
}
|
||||
|
||||
TEST_F(StarletPairingTest, PairingWriteUsesNativeSFFSAuthenticationLayout)
|
||||
{
|
||||
SetSysconf(true);
|
||||
m_superblock->fst[3].uid = 0x12345678;
|
||||
m_superblock->fst[3].x3 = 0x9abcdef0;
|
||||
StoreSuperblock();
|
||||
MarkSysconfSpare();
|
||||
std::array<NANDJournal::Page, 8> before;
|
||||
for (u32 page = 0; page < 8; ++page)
|
||||
before[page] = ReadSysconfPage(page);
|
||||
ASSERT_TRUE(Install());
|
||||
|
||||
std::array<NANDJournal::Page, 8> after;
|
||||
std::vector<u8> ciphertext(0x4000);
|
||||
for (u32 page = 0; page < 8; ++page)
|
||||
{
|
||||
after[page] = ReadSysconfPage(page);
|
||||
std::copy_n(after[page].begin(), 0x800, ciphertext.begin() + page * 0x800);
|
||||
}
|
||||
const std::array<u8, 16> aes_key{};
|
||||
auto aes = Common::AES::CreateContextDecrypt(aes_key.data());
|
||||
std::vector<u8> authenticated(0x4040);
|
||||
ASSERT_TRUE(aes->CryptIvZero(ciphertext.data(), authenticated.data() + 0x40, ciphertext.size()));
|
||||
const auto pairing = IOS::HLE::GetEmulatedWiimotePairingData();
|
||||
EXPECT_TRUE(
|
||||
std::ranges::equal(pairing, std::span(authenticated).subspan(0x40 + 0x1a, pairing.size())));
|
||||
|
||||
// Independent fixture salt: UID, file name, cluster ordinal, FST index and x3.
|
||||
const std::array<u8, 0x1c> salt = {0x12, 0x34, 0x56, 0x78, 'S', 'Y', 'S', 'C', 'O', 'N',
|
||||
'F', 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 3, 0x9a, 0xbc, 0xde, 0xf0};
|
||||
std::ranges::copy(salt, authenticated.begin());
|
||||
const std::array<u8, 20> hmac_key{};
|
||||
std::array<u8, 20> hmac{};
|
||||
ASSERT_TRUE(Common::HMAC::HMACWithSHA1(hmac_key, authenticated, hmac.data()));
|
||||
|
||||
// Native SFFS puts the full HMAC at spare byte 1 in page 6. The second
|
||||
// copy is split: 12 bytes at page 6 byte 21, then 8 at page 7 byte 1.
|
||||
auto expected = before;
|
||||
std::ranges::copy(hmac, expected[6].begin() + 0x801);
|
||||
std::copy_n(hmac.begin(), 12, expected[6].begin() + 0x815);
|
||||
std::copy_n(hmac.begin() + 12, 8, expected[7].begin() + 0x801);
|
||||
for (u32 page = 0; page < 8; ++page)
|
||||
{
|
||||
SCOPED_TRACE(page);
|
||||
EXPECT_TRUE(std::ranges::equal(std::span(after[page]).subspan(0x800, 0x30),
|
||||
std::span(expected[page]).subspan(0x800, 0x30)));
|
||||
EXPECT_TRUE(
|
||||
std::ranges::equal(CalculateECC(after[page]), std::span(after[page]).subspan(0x830, 16)));
|
||||
}
|
||||
}
|
||||
} // namespace IOS::LLE
|
||||
@@ -0,0 +1,441 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Common/Swap.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/WII_IPC.h"
|
||||
#include "Core/IOS/Starlet/ARMCore.h"
|
||||
#include "Core/IOS/Starlet/StarletMemory.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
namespace IOS::LLE
|
||||
{
|
||||
#if defined(_M_X86_64)
|
||||
namespace
|
||||
{
|
||||
// Only code and page tables use synthetic RAM. Data reads exercise the actual SRAM/boot0 bus,
|
||||
// without initializing any firmware, NAND image, profile, or live peripheral scheduler.
|
||||
class SRAMReadBus final : public ARMBus
|
||||
{
|
||||
public:
|
||||
SRAMReadBus() : memory(Core::System::GetInstance())
|
||||
{
|
||||
memory.Reset();
|
||||
for (u32 i = 0; i < 0x18000; ++i)
|
||||
memory.GetFastmemSRAMBase()[i] = static_cast<u8>((i * 13) ^ (i >> 8) ^ 0xa5);
|
||||
}
|
||||
|
||||
u8 Read8(u32 address) override
|
||||
{
|
||||
if (address < ram.size())
|
||||
return ram[address];
|
||||
++data_reads;
|
||||
return memory.Read8(address);
|
||||
}
|
||||
u16 Read16(u32 address) override
|
||||
{
|
||||
if (address < ram.size())
|
||||
return ARMBus::Read16(address);
|
||||
++data_reads;
|
||||
return memory.Read16(address);
|
||||
}
|
||||
u32 Read32(u32 address) override
|
||||
{
|
||||
if (address < ram.size())
|
||||
return ARMBus::Read32(address);
|
||||
++data_reads;
|
||||
return memory.Read32(address);
|
||||
}
|
||||
void Write8(u32 address, u8 value) override
|
||||
{
|
||||
if (address < ram.size())
|
||||
ram[address] = value;
|
||||
else
|
||||
memory.Write8(address, value);
|
||||
}
|
||||
void Write16(u32 address, u16 value) override
|
||||
{
|
||||
if (address < ram.size())
|
||||
ARMBus::Write16(address, value);
|
||||
else
|
||||
memory.Write16(address, value);
|
||||
}
|
||||
void Write32(u32 address, u32 value) override
|
||||
{
|
||||
if (address < ram.size())
|
||||
ARMBus::Write32(address, value);
|
||||
else
|
||||
memory.Write32(address, value);
|
||||
}
|
||||
u8* GetFastmemBase() const override { return const_cast<u8*>(ram.data()); }
|
||||
u8* GetFastmemSRAMBase() const override { return memory.GetFastmemSRAMBase(); }
|
||||
const bool* GetFastmemBoot0Mapped() const override { return memory.GetFastmemBoot0Mapped(); }
|
||||
const bool* GetFastmemSRAMSplitMode() const override { return memory.GetFastmemSRAMSplitMode(); }
|
||||
const u8* GetDirectMemoryPointer(u32 address, u32 size) const override
|
||||
{
|
||||
if (address < ram.size() && size <= ram.size() - address)
|
||||
return ram.data() + address;
|
||||
return memory.GetDirectMemoryPointer(address, size);
|
||||
}
|
||||
|
||||
void SetMapping(bool split, bool boot0)
|
||||
{
|
||||
memory.Write32(0x0d800060, split ? 0x20 : 0); // HW_SRNPROT
|
||||
memory.Write32(0x0d80018c, boot0 ? 0 : 0x1000); // HW_BOOT0
|
||||
}
|
||||
|
||||
void InstallReads(bool thumb, bool big_endian)
|
||||
{
|
||||
if (thumb)
|
||||
{
|
||||
constexpr std::array<u16, 6> code = {
|
||||
0x6830, // ldr r0, [r6]
|
||||
0x7831, // ldrb r1, [r6]
|
||||
0x8832, // ldrh r2, [r6]
|
||||
0x57f3, // ldrsb r3, [r6, r7]
|
||||
0x5ff4, // ldrsh r4, [r6, r7]
|
||||
0xe7fe, // b .
|
||||
};
|
||||
for (u32 i = 0; i < code.size(); ++i)
|
||||
Write16(i * 2, big_endian ? code[i] : Common::swap16(code[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
constexpr std::array<u32, 6> code = {
|
||||
0xe5960000, // ldr r0, [r6]
|
||||
0xe5d61000, // ldrb r1, [r6]
|
||||
0xe1d620b0, // ldrh r2, [r6]
|
||||
0xe1d630d0, // ldrsb r3, [r6]
|
||||
0xe1d640f0, // ldrsh r4, [r6]
|
||||
0xeafffffe, // b .
|
||||
};
|
||||
for (u32 i = 0; i < code.size(); ++i)
|
||||
Write32(i * 4, big_endian ? code[i] : Common::swap32(code[i]));
|
||||
}
|
||||
}
|
||||
|
||||
void InstallStores(bool thumb, bool big_endian)
|
||||
{
|
||||
if (thumb)
|
||||
{
|
||||
constexpr std::array<u16, 4> code = {
|
||||
0x6030, // str r0, [r6]
|
||||
0x7131, // strb r1, [r6, #4]
|
||||
0x80f2, // strh r2, [r6, #6]
|
||||
0xe7fe, // b .
|
||||
};
|
||||
for (u32 i = 0; i < code.size(); ++i)
|
||||
Write16(i * 2, big_endian ? code[i] : Common::swap16(code[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
constexpr std::array<u32, 4> code = {
|
||||
0xe5860000, // str r0, [r6]
|
||||
0xe5c61004, // strb r1, [r6, #4]
|
||||
0xe1c620b6, // strh r2, [r6, #6]
|
||||
0xeafffffe, // b .
|
||||
};
|
||||
for (u32 i = 0; i < code.size(); ++i)
|
||||
Write32(i * 4, big_endian ? code[i] : Common::swap32(code[i]));
|
||||
}
|
||||
}
|
||||
|
||||
StarletMemory memory;
|
||||
std::array<u8, 0x8000> ram{};
|
||||
u64 data_reads = 0;
|
||||
};
|
||||
|
||||
class StarletSRAMRead : public testing::TestWithParam<bool>
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
Core::DeclareAsCPUThread();
|
||||
Core::System::GetInstance().GetWiiIPC().Reset();
|
||||
}
|
||||
|
||||
static void Prepare(ARMCore& core, bool thumb, bool big_endian, u32 address)
|
||||
{
|
||||
if (core.IsBigEndian() != big_endian)
|
||||
core.SetBigEndian(big_endian);
|
||||
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | (thumb ? ARMCore::CPSR_T : 0));
|
||||
for (u32 i = 0; i < 16; ++i)
|
||||
core.SetRegister(i, 0);
|
||||
core.SetRegister(6, address);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_P(StarletSRAMRead, NewlyProfiledPagesAvoidDataBusCalls)
|
||||
{
|
||||
for (const bool thumb : {false, true})
|
||||
{
|
||||
for (const bool big_endian : {false, true})
|
||||
{
|
||||
auto bus = std::make_unique<SRAMReadBus>();
|
||||
bus->SetMapping(true, false);
|
||||
bus->InstallReads(thumb, big_endian);
|
||||
ARMCore core(*bus, GetParam());
|
||||
core.SetJitEnabled(true);
|
||||
for (const u32 base : {0x0d400000u, 0xfff00000u, 0xfffe0000u})
|
||||
{
|
||||
for (const u32 page : {0x01u, 0x02u, 0x04u, 0x13u, 0x15u, 0x16u, 0x1au, 0x1cu, 0x1du})
|
||||
{
|
||||
const u32 address = base + page * 0x1000;
|
||||
SCOPED_TRACE(testing::Message() << "thumb=" << thumb << " be=" << big_endian
|
||||
<< " address=" << std::hex << address);
|
||||
Prepare(core, thumb, big_endian, address);
|
||||
bus->data_reads = 0;
|
||||
ASSERT_EQ(core.RunCycles(6), 6u);
|
||||
ASSERT_EQ(core.GetLastUndefinedInstruction(), 0u);
|
||||
ASSERT_EQ(bus->data_reads, 0u);
|
||||
EXPECT_EQ(core.GetRegister(0), big_endian ? bus->memory.Read32(address) :
|
||||
Common::swap32(bus->memory.Read32(address)));
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(core.GetJitSlowReadCount(), 0u);
|
||||
EXPECT_EQ(core.GetJitFallbackInstructionCount(), 0u);
|
||||
EXPECT_EQ(core.GetJitCompiledBlockCount(), 1u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(StarletSRAMRead, SizedNativeStoresMatchInterpreterAcrossMappings)
|
||||
{
|
||||
for (const bool thumb : {false, true})
|
||||
{
|
||||
for (const bool big_endian : {false, true})
|
||||
{
|
||||
auto reference_bus = std::make_unique<SRAMReadBus>();
|
||||
auto native_bus = std::make_unique<SRAMReadBus>();
|
||||
reference_bus->InstallStores(thumb, big_endian);
|
||||
native_bus->InstallStores(thumb, big_endian);
|
||||
ARMCore reference(*reference_bus);
|
||||
ARMCore native(*native_bus, GetParam());
|
||||
native.SetJitEnabled(true);
|
||||
for (const bool split : {false, true, false})
|
||||
{
|
||||
for (const bool boot0 : {false, true, false})
|
||||
{
|
||||
reference_bus->SetMapping(split, boot0);
|
||||
native_bus->SetMapping(split, boot0);
|
||||
for (const u32 base : {0x0d400000U, 0xfff00000U, 0xfffe0000U})
|
||||
{
|
||||
for (u32 page = 0; page < 32; ++page)
|
||||
{
|
||||
for (const u32 offset : {0U, 1U, 2U, 3U, 0xff8U, 0xffcU, 0xffeU, 0xfffU})
|
||||
{
|
||||
const u32 address = base + page * 0x1000 + offset;
|
||||
if (address > 0xfffffff7U)
|
||||
continue; // Do not wrap a test data store into synthetic guest code.
|
||||
SCOPED_TRACE(testing::Message()
|
||||
<< "thumb=" << thumb << " be=" << big_endian << " split=" << split
|
||||
<< " boot0=" << boot0 << " address=" << std::hex << address);
|
||||
for (ARMCore* core : {&reference, &native})
|
||||
{
|
||||
Prepare(*core, thumb, big_endian, address);
|
||||
core->SetRegister(0, 0x12345678);
|
||||
core->SetRegister(1, 0x9abcdef0);
|
||||
core->SetRegister(2, 0x7e3dac12);
|
||||
ASSERT_EQ(core->RunCycles(4), 4U);
|
||||
ASSERT_EQ(core->GetLastUndefinedInstruction(), 0U);
|
||||
}
|
||||
for (u32 reg = 0; reg < 16; ++reg)
|
||||
ASSERT_EQ(native.GetRegister(reg), reference.GetRegister(reg));
|
||||
ASSERT_EQ(native.GetCPSR(), reference.GetCPSR());
|
||||
ASSERT_TRUE(std::equal(reference_bus->memory.GetFastmemSRAMBase(),
|
||||
reference_bus->memory.GetFastmemSRAMBase() + 0x18000,
|
||||
native_bus->memory.GetFastmemSRAMBase()));
|
||||
for (u32 byte = 0; byte < 8; ++byte)
|
||||
ASSERT_EQ(native_bus->Read8(address + byte),
|
||||
reference_bus->Read8(address + byte));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(StarletSRAMRead, MatchesInterpreterAcrossLiveMappingsAliasesAndBoundaries)
|
||||
{
|
||||
for (const bool thumb : {false, true})
|
||||
{
|
||||
for (const bool big_endian : {false, true})
|
||||
{
|
||||
auto interpreter_bus = std::make_unique<SRAMReadBus>();
|
||||
auto jit_bus = std::make_unique<SRAMReadBus>();
|
||||
interpreter_bus->InstallReads(thumb, big_endian);
|
||||
jit_bus->InstallReads(thumb, big_endian);
|
||||
ARMCore interpreter(*interpreter_bus);
|
||||
ARMCore jit(*jit_bus, GetParam());
|
||||
jit.SetJitEnabled(true);
|
||||
std::array<u8, 0x18000> original;
|
||||
std::copy_n(jit_bus->GetFastmemSRAMBase(), original.size(), original.begin());
|
||||
std::optional<u64> compiled_blocks;
|
||||
// Toggle the hardware state while reusing the same compiled code: guards must consult
|
||||
// live boot0/split latches, not their values at compilation time.
|
||||
for (const bool split : {false, true, false})
|
||||
{
|
||||
for (const bool boot0 : {false, true, false})
|
||||
{
|
||||
interpreter_bus->SetMapping(split, boot0);
|
||||
jit_bus->SetMapping(split, boot0);
|
||||
for (const u32 base : {0x0d400000u, 0xfff00000u, 0xfffe0000u})
|
||||
{
|
||||
for (u32 page = 0; page < 32; ++page)
|
||||
{
|
||||
for (const u32 offset :
|
||||
{0u, 1u, 2u, 3u, 0x3fdu, 0x3feu, 0x3ffu, 0xffcu, 0xffdu, 0xffeu, 0xfffu})
|
||||
{
|
||||
const u32 address = base + page * 0x1000 + offset;
|
||||
if (address > 0xfffffffcu)
|
||||
continue; // Exclude address-space wrap into the synthetic code buffer.
|
||||
SCOPED_TRACE(testing::Message()
|
||||
<< "thumb=" << thumb << " be=" << big_endian << " split=" << split
|
||||
<< " boot0=" << boot0 << " address=" << std::hex << address);
|
||||
Prepare(interpreter, thumb, big_endian, address);
|
||||
Prepare(jit, thumb, big_endian, address);
|
||||
ASSERT_EQ(interpreter.RunCycles(6), 6u);
|
||||
ASSERT_EQ(jit.RunCycles(6), 6u);
|
||||
ASSERT_EQ(jit.GetLastUndefinedInstruction(), 0u);
|
||||
for (u32 reg = 0; reg < 16; ++reg)
|
||||
ASSERT_EQ(jit.GetRegister(reg), interpreter.GetRegister(reg)) << "r" << reg;
|
||||
ASSERT_EQ(jit.GetCPSR(), interpreter.GetCPSR());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unaligned fallbacks also compile continuations. Once the first boundary sweep has
|
||||
// warmed them, changing the hardware mapping must reuse those blocks, not rebuild them.
|
||||
if (!compiled_blocks)
|
||||
compiled_blocks = jit.GetJitLifetimeCompiledBlockCount();
|
||||
else
|
||||
EXPECT_EQ(jit.GetJitLifetimeCompiledBlockCount(), *compiled_blocks);
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(std::equal(original.begin(), original.end(), jit_bus->GetFastmemSRAMBase()));
|
||||
EXPECT_TRUE(
|
||||
std::equal(original.begin(), original.end(), interpreter_bus->GetFastmemSRAMBase()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(StarletSRAMRead, PreservedBlockUsesRemappedSRAMAfterGuestTLBInvalidation)
|
||||
{
|
||||
auto bus = std::make_unique<SRAMReadBus>();
|
||||
bus->SetMapping(true, false);
|
||||
bus->Write32(0, 0xe5960000); // ldr r0, [r6]
|
||||
bus->Write32(4, 0xeafffffe); // b .
|
||||
bus->Write32(0x40, 0xee080f17); // mcr p15, 0, r0, c8, c7, 0 (invalidate unified TLB)
|
||||
bus->Write32(0x4000, 0x00000c02); // Identity section for code/page tables.
|
||||
bus->Write32(0x4c00, 0x00005001); // VA 0x30000000 -> coarse page table at 0x5000.
|
||||
bus->Write32(0x5000, 0x0d404ff2); // Small page -> SRAM B, aperture page 04.
|
||||
bus->memory.Write32(0x0d404000, 0x12345678);
|
||||
bus->memory.Write32(0xffff6000, 0x87654321);
|
||||
ARMCore core(*bus, GetParam());
|
||||
core.GetCP15State().translation_table_base = 0x4000;
|
||||
core.GetCP15State().domain_access_control = 3;
|
||||
core.GetCP15State().control |= 1;
|
||||
core.SetRegister(6, 0x30000000);
|
||||
core.SetJitEnabled(true);
|
||||
ASSERT_EQ(core.RunCycles(2), 2u);
|
||||
ASSERT_EQ(core.GetRegister(0), 0x12345678u);
|
||||
const auto old_blocks = core.GetJitCompiledBlockCount();
|
||||
bus->Write32(0x5000, 0xffff6ff2); // Remap to SRAM A, aperture page 16.
|
||||
core.SetRegister(15, 0x40);
|
||||
ASSERT_EQ(core.RunCycles(1), 1u);
|
||||
core.SetRegister(15, 0);
|
||||
ASSERT_EQ(core.RunCycles(2), 2u);
|
||||
EXPECT_EQ(core.GetRegister(0), 0x87654321u);
|
||||
EXPECT_EQ(core.GetJitCompiledBlockCount(), old_blocks + 1);
|
||||
EXPECT_EQ(core.GetJitSlowReadCount(), 0u);
|
||||
}
|
||||
TEST_P(StarletSRAMRead, OptionalProfilingPreservesBusCountsResultsAndReset)
|
||||
{
|
||||
for (const bool thumb : {false, true})
|
||||
{
|
||||
for (const bool big_endian : {false, true})
|
||||
{
|
||||
for (const u32 address : {0xfff1f000U, 0x0d800214U})
|
||||
{
|
||||
auto bus = std::make_unique<SRAMReadBus>();
|
||||
auto reference_bus = std::make_unique<SRAMReadBus>();
|
||||
for (SRAMReadBus* target : {bus.get(), reference_bus.get()})
|
||||
{
|
||||
target->SetMapping(true, false);
|
||||
target->memory.Write32(address, 0x89abcdef);
|
||||
if (thumb)
|
||||
{
|
||||
target->Write16(0, big_endian ? 0x6830 : Common::swap16(0x6830)); // ldr r0,[r6]
|
||||
target->Write16(2, big_endian ? 0x6030 : Common::swap16(0x6030)); // str r0,[r6]
|
||||
target->Write16(4, big_endian ? 0xe7fc : Common::swap16(0xe7fc)); // b 0
|
||||
}
|
||||
else
|
||||
{
|
||||
target->Write32(0, big_endian ? 0xe5960000 : Common::swap32(0xe5960000));
|
||||
target->Write32(4, big_endian ? 0xe5860000 : Common::swap32(0xe5860000));
|
||||
target->Write32(8, big_endian ? 0xeafffffc : Common::swap32(0xeafffffc));
|
||||
}
|
||||
}
|
||||
ARMCore core(*bus, GetParam());
|
||||
ARMCore reference(*reference_bus);
|
||||
EXPECT_FALSE(reference.IsJitMemoryProfilingEnabled());
|
||||
core.SetJitEnabled(true);
|
||||
// Recompile after reset and endian changes with the same host-only profiling choice.
|
||||
for (int reset = 0; reset < 2; ++reset)
|
||||
{
|
||||
core.Reset();
|
||||
reference.Reset();
|
||||
EXPECT_EQ(core.IsJitMemoryProfilingEnabled(), GetParam());
|
||||
const u64 reads_before = core.GetJitSlowReadCount();
|
||||
const u64 writes_before = core.GetJitSlowWriteCount();
|
||||
for (ARMCore* cpu : {&core, &reference})
|
||||
{
|
||||
Prepare(*cpu, thumb, big_endian, address);
|
||||
ASSERT_EQ(cpu->RunCycles(768), 768U);
|
||||
}
|
||||
EXPECT_EQ(core.GetJitSlowReadCount() - reads_before, 256U);
|
||||
EXPECT_EQ(core.GetJitSlowWriteCount() - writes_before, 256U);
|
||||
for (u32 reg = 0; reg < 16; ++reg)
|
||||
EXPECT_EQ(core.GetRegister(reg), reference.GetRegister(reg));
|
||||
EXPECT_EQ(core.GetCPSR(), reference.GetCPSR());
|
||||
EXPECT_EQ(bus->memory.Read32(address), reference_bus->memory.Read32(address));
|
||||
EXPECT_EQ(bus->memory.Read32(address), 0x89abcdefU);
|
||||
const auto samples = core.TakeHotJitSlowMemorySamples(8);
|
||||
if (GetParam())
|
||||
{
|
||||
ASSERT_EQ(samples.size(), 2U);
|
||||
for (const auto& [key, count] : samples)
|
||||
{
|
||||
EXPECT_EQ(static_cast<u32>(key), address);
|
||||
EXPECT_EQ(count, 1U);
|
||||
}
|
||||
EXPECT_GT(address == 0xfff1f000 ? core.GetJitSlowSRAMAccessCount() :
|
||||
core.GetJitSlowMMIOAccessCount(),
|
||||
0U);
|
||||
}
|
||||
else
|
||||
{
|
||||
EXPECT_TRUE(samples.empty());
|
||||
EXPECT_EQ(core.GetJitSlowSRAMAccessCount(), 0U);
|
||||
EXPECT_EQ(core.GetJitSlowMMIOAccessCount(), 0U);
|
||||
EXPECT_EQ(core.GetJitSlowSRAMPageAccessCount(0x1f), 0U);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(MemoryProfiling, StarletSRAMRead, testing::Bool());
|
||||
} // namespace
|
||||
#endif
|
||||
} // namespace IOS::LLE
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/WII_IPC.h"
|
||||
#include "Core/IOS/Starlet/StarletMemory.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
namespace IOS::LLE
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Deliberately branch-based reference, independent of the optimized aperture-bit swap.
|
||||
std::optional<u32> MapSRAMByte(u32 address, bool split, bool boot0)
|
||||
{
|
||||
if (!(address >= 0x0d400000 && address < 0x0d420000) && address < 0xfff00000)
|
||||
return std::nullopt;
|
||||
const u32 window = address & 0x1ffff;
|
||||
if (boot0 && address >= 0xfffe0000 && (split ? window < 0x10000 : window >= 0x10000))
|
||||
return std::nullopt;
|
||||
if (!split)
|
||||
return window < 0x18000 ? std::optional(window) : std::nullopt;
|
||||
if (window < 0x8000)
|
||||
return 0x10000 + window;
|
||||
if (window >= 0x10000)
|
||||
return window - 0x10000;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
class StarletSRAMWrite : public testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
Core::DeclareAsCPUThread();
|
||||
Core::System::GetInstance().GetWiiIPC().Reset();
|
||||
}
|
||||
static void SetMapping(StarletMemory& memory, bool split, bool boot0)
|
||||
{
|
||||
memory.Write32(0x0d800060, split ? 0x20 : 0);
|
||||
memory.Write32(0x0d80018c, boot0 ? 0 : 0x1000);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(StarletSRAMWrite, EveryApertureByteMatchesIndependentMapping)
|
||||
{
|
||||
auto memory = std::make_unique<StarletMemory>(Core::System::GetInstance());
|
||||
memory->Reset();
|
||||
u8* const sram = memory->GetFastmemSRAMBase();
|
||||
for (u32 i = 0; i < StarletMemory::SRAM_SIZE; ++i)
|
||||
sram[i] = static_cast<u8>((i * 13) ^ (i >> 8) ^ 0xa5);
|
||||
for (const bool split : {false, true})
|
||||
{
|
||||
for (const bool boot0 : {false, true})
|
||||
{
|
||||
SetMapping(*memory, split, boot0);
|
||||
for (const u32 base : {0x0d400000u, 0xfff00000u, 0xfffe0000u})
|
||||
{
|
||||
for (u32 i = 0; i < 0x20000; ++i)
|
||||
{
|
||||
const u32 address = base + i;
|
||||
const auto offset = MapSRAMByte(address, split, boot0);
|
||||
// Reset leaves the synthetic boot ROM zero-filled. Holes also read zero.
|
||||
ASSERT_EQ(memory->Read8(address), offset ? sram[*offset] : 0)
|
||||
<< "address=" << std::hex << address << " split=" << split << " boot0=" << boot0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(StarletSRAMWrite, WideStoresMatchByteOracleAcrossLiveMappingsAndAllMirrors)
|
||||
{
|
||||
auto memory = std::make_unique<StarletMemory>(Core::System::GetInstance());
|
||||
memory->Reset();
|
||||
std::array<u8, StarletMemory::SRAM_SIZE> expected;
|
||||
for (u32 i = 0; i < expected.size(); ++i)
|
||||
expected[i] = static_cast<u8>((i * 13) ^ (i >> 8) ^ 0xa5);
|
||||
std::copy(expected.begin(), expected.end(), memory->GetFastmemSRAMBase());
|
||||
// Toggle back, too: no cached host pointer may survive changes to SM/boot0.
|
||||
for (const bool split : {false, true, false})
|
||||
{
|
||||
for (const bool boot0 : {false, true, false})
|
||||
{
|
||||
SetMapping(*memory, split, boot0);
|
||||
for (const u32 base : {0x0d400000u, 0xfff00000u, 0xfff20000u, 0xfff40000u, 0xfff60000u,
|
||||
0xfff80000u, 0xfffa0000u, 0xfffc0000u, 0xfffe0000u})
|
||||
{
|
||||
for (u32 page = 0; page < 32; ++page)
|
||||
{
|
||||
for (const u32 in_page : {0u, 1u, 2u, 3u, 0xffcu, 0xffdu, 0xffeu, 0xfffu})
|
||||
{
|
||||
const u32 address = base + page * 0x1000 + in_page;
|
||||
for (const u32 width : {2u, 4u})
|
||||
{
|
||||
if (address > 0xffffffffu - (width - 1))
|
||||
continue; // Do not wrap into uninitialized MEM1.
|
||||
const u32 value = (address * 0x12345u) ^ (width == 2 ? 0x89abcdefu : 0x76543210u);
|
||||
for (u32 byte = 0; byte < width; ++byte)
|
||||
{
|
||||
if (const auto offset = MapSRAMByte(address + byte, split, boot0))
|
||||
expected[*offset] = static_cast<u8>(value >> ((width - 1 - byte) * 8));
|
||||
}
|
||||
if (width == 2)
|
||||
memory->Write16(address, static_cast<u16>(value));
|
||||
else
|
||||
memory->Write32(address, value);
|
||||
ASSERT_TRUE(
|
||||
std::equal(expected.begin(), expected.end(), memory->GetFastmemSRAMBase()))
|
||||
<< "address=" << std::hex << address << " width=" << width << " split=" << split
|
||||
<< " boot0=" << boot0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
} // namespace IOS::LLE
|
||||
Reference in new issue
Block a user