From caed1bc5f2b540db803f12df2b87f33e07f62a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 20 Oct 2026 18:55:48 +0200 Subject: [PATCH 1/3] ME: Skip the rest of a slice the ME spends polling for the main CPU Most homebrew ME code waits in a loop like lw t0, flag; beq t0, zero, loop; nop: Daedalus/MElib/NooDS's mediaengine.prx, snes9xTYL, PMP Mod, Yabause, FBA, the Touhou ports (with a few nops in it). Such a loop stores nothing, has only plain loads, integer ALU ops, nops and branches, and writes every register it reads before reading it in a pass, so every pass goes the same way while memory stays the same - which it does while the ME runs, since the main CPU doesn't. At the top of one, between parts of a slice, the ME interprets a pass, and if it comes back to the top, waits out the rest of the slice. Loops that also store (a heartbeat counter) or span several blocks aren't recognized, nor are delay loops counting down a register. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/MIPS/MeCore.cpp | 122 +++++++++++++++++++++++++++++++++++++++- Core/MIPS/MeCore.h | 8 +++ unittest/JitHarness.cpp | 77 +++++++++++++++++++++++++ unittest/JitHarness.h | 1 + unittest/UnitTest.cpp | 1 + 5 files changed, 208 insertions(+), 1 deletion(-) diff --git a/Core/MIPS/MeCore.cpp b/Core/MIPS/MeCore.cpp index 0973268a93..ba67c50cf7 100644 --- a/Core/MIPS/MeCore.cpp +++ b/Core/MIPS/MeCore.cpp @@ -28,6 +28,7 @@ #include "Core/MIPS/MeCore.h" #include "Core/MIPS/Interpreter.h" #include "Core/MIPS/JitCommon/JitCommon.h" +#include "Core/MIPS/MIPSAnalyst.h" #include "Core/MIPS/MIPSTables.h" namespace MeCore { @@ -193,6 +194,7 @@ static void CheckInterrupts(MEState *me) { if ((status & (STATUS_IE | STATUS_EXL | STATUS_ERL)) != STATUS_IE) { return; } + me->waiting = false; // Interrupts go straight to EBase (COP0 25), with ExcCode 0. cpu.cop0[14] = cpu.pc; cpu.cop0[13] &= ~CAUSE_EXCCODE_BD; @@ -201,6 +203,117 @@ static void CheckInterrupts(MEState *me) { cpu.llBit = 0; } +// Most ME code that waits for the main CPU polls in a loop like lw t0, flag; beq t0, zero, loop; nop. +// Such a loop stores nothing and reads only what it wrote itself earlier in the same pass (or what +// it doesn't write), so every pass goes the same way while memory stays the same - and it does +// while the ME runs, since the main CPU doesn't. One that loops back once will loop until the end +// of the slice. Sony's firmware waits with halt instead, and homebrew loops that also store (a +// heartbeat) or span several blocks aren't recognized. +static const int MAX_WAIT_LOOP_INSTRUCTIONS = 16; + +static bool IsPlainOp(MIPSOpcode op, MIPSInfo info) { + const u64 notPlain = BAD_INSTRUCTION | OUT_MEM | OUT_OTHER | IN_OTHER | IS_SYSCALL | IS_FPU | IS_VFPU | + IN_FPUFLAG | OUT_FPUFLAG | IN_VFPU_CC | OUT_VFPU_CC | IN_LO | IN_HI | OUT_LO | OUT_HI | OUT_RA | + IN_FS | IN_FT | OUT_FD | OUT_FS | OUT_FT | IN_VS | IN_VT | OUT_VD; + if (info & notPlain) + return false; + switch (op >> 26) { + case 0x10: // cop0, eret + case 0x1C: // halt, mfic, mtic + case 0x22: // lwl + case 0x26: // lwr + case 0x2F: // cache + case 0x30: // ll + return false; + default: + return true; + } +} + +// Where a conditional branch or j goes, or 0 if it isn't one (or is a jr, jal or similar). +static u32 BranchTarget(u32 pc, MIPSOpcode op) { + const u32 code = op >> 26; + const bool regimm = code == 1 && ((op >> 16) & 0x1C) == 0; // bltz, bgez and their likely forms + if ((code >= 4 && code <= 7) || (code >= 0x14 && code <= 0x17) || regimm) + return pc + 4 + ((s32)(s16)(op & 0xFFFF) << 2); + if (code == 2) + return ((pc + 4) & 0xF0000000) | ((op & 0x03FFFFFF) << 2); + return 0; +} + +// The end (after the delay slot) of a wait loop starting at start, or 0 if it isn't one. +static u32 FindWaitLoop(const Memory::AddressSpace &space, u32 start) { + MIPSOpcode ops[MAX_WAIT_LOOP_INSTRUCTIONS + 1]; + int count = 0; + bool loops = false; + for (u32 pc = start; count < MAX_WAIT_LOOP_INSTRUCTIONS; pc += 4) { + if (!space.IsValid4AlignedAddress(pc)) + return 0; + const MIPSOpcode op(space.ReadUnchecked_U32(pc)); + const MIPSInfo info = MIPSGetInfo(op); + ops[count++] = op; + if (loops) { + // The back edge's delay slot. + if ((info & (IS_CONDBRANCH | IS_JUMP)) || !IsPlainOp(op, info)) + return 0; + break; + } + if (info & (IS_CONDBRANCH | IS_JUMP)) { + const u32 target = BranchTarget(pc, op); + // An exit is fine, but likely branches skip their delay slot when not taken. + if (target == 0 || ((info & LIKELY) && target != start)) + return 0; + loops = target == start; + } else if (!IsPlainOp(op, info)) { + return 0; + } + } + if (!loops) + return 0; + + // Every register the loop writes has to be written before it's read in a pass. + u32 written = 0; + for (int i = 0; i < count; ++i) { + const MIPSGPReg out = MIPSAnalyst::GetOutGPReg(ops[i]); + if (out != MIPS_REG_INVALID && out != MIPS_REG_ZERO) + written |= 1U << out; + } + u32 writtenSoFar = 0; + for (int i = 0; i < count; ++i) { + for (int r = 1; r < 32; ++r) { + if ((written & (1U << r)) && !(writtenSoFar & (1U << r)) && MIPSAnalyst::ReadsFromGPReg(ops[i], (MIPSGPReg)r)) + return 0; + } + const MIPSGPReg out = MIPSAnalyst::GetOutGPReg(ops[i]); + if (out != MIPS_REG_INVALID && out != MIPS_REG_ZERO) + writtenSoFar |= 1U << out; + } + return start + count * 4; +} + +// At the top of a wait loop, runs a pass of it. If that comes back to the top, it would only go on +// doing so: true, and the ME waits out the rest of the slice. +static bool WaitsForMainCPU(MEState *me) { + MIPSState &cpu = me->cpu; + const u32 start = cpu.pc; + auto it = me->waitLoopEnds.find(start); + if (it == me->waitLoopEnds.end()) + it = me->waitLoopEnds.emplace(start, FindWaitLoop(*cpu.space, start)).first; + const u32 end = it->second; + if (end == 0 || cpu.inDelaySlot) + return false; + for (int i = 0; i <= MAX_WAIT_LOOP_INSTRUCTIONS; ++i) { + cpu.downcount -= MIPS_InterpretSingleStep(&cpu); + if (cpu.inDelaySlot) + continue; + if (cpu.pc == start) + return true; + if (cpu.pc < start || cpu.pc >= end) + return false; + } + return false; +} + static void RunCPU(MIPSState &cpu) { if (!cpu.jit) { MIPSInterpret_RunDowncount(&cpu); @@ -225,9 +338,16 @@ void RunFor(int cycles) { } MIPSState &cpu = me->cpu; cpu.downcount += cycles; + if (!cpu.pendingInvalidates_.empty() || cpu.invalidateAll_) { + me->waitLoopEnds.clear(); + } + // The main CPU may have given a waiting loop what it waits for. + me->waiting = false; while (cpu.downcount >= 0 && coreState == CORE_RUNNING_CPU) { CheckInterrupts(me); - if (me->halted) { + if (!me->halted && !me->waiting) + me->waiting = WaitsForMainCPU(me); + if (me->halted || me->waiting) { cpu.downcount = -1; } else { // Run a part, with the rest set aside so the clock stays right. diff --git a/Core/MIPS/MeCore.h b/Core/MIPS/MeCore.h index 537951cee9..e89053d4a2 100644 --- a/Core/MIPS/MeCore.h +++ b/Core/MIPS/MeCore.h @@ -22,6 +22,8 @@ #pragma once +#include + #include "Common/CommonTypes.h" #include "Core/MIPS/MIPS.h" @@ -56,6 +58,12 @@ struct MEState { u32 ramConfig = 0; // 0x40. Bits 0-1: RAM size, 16 << n MB. u32 mutex = 0; // 0x48. Bit 0 is the main CPU's, bit 1 the ME's. u32 memProtect[20]{}; // 0xBC000000-0xBC00004C + + // Not saved: whether the code at an address is a wait loop (see IsWaitLoop), and where it ends. + // 0 for not one. Cleared when the ME's code changes. + std::unordered_map waitLoopEnds; + // Waiting for the main CPU, for the rest of this RunFor. + bool waiting = false; }; // Null when the ME isn't emulated. diff --git a/unittest/JitHarness.cpp b/unittest/JitHarness.cpp index 7e71fb15b4..560e1b5882 100644 --- a/unittest/JitHarness.cpp +++ b/unittest/JitHarness.cpp @@ -918,6 +918,83 @@ bool TestMeInterrupts() { return TestMeInterruptsOn(CPUCore::INTERPRETER) && TestMeInterruptsOn(CPUCore::IR_INTERPRETER) && TestMeInterruptsOn(CPUCore::JIT); } +// The ME skips the rest of a slice spent polling for the main CPU, and only then. +static bool TestMeWaitOn(CPUCore core) { + Memory::g_PSPModel = PSP_MODEL_FAT; + SetupJitHarness(); + PSP_CoreParameter().cpuCore = core; + MMIO::Init(); + bool success = true; + + if (!MeCore::Init()) { + printf("No Media Engine on this platform, skipping\n"); + MMIO::Shutdown(); + DestroyJitHarness(); + return true; + } + MeCore::MEState *me = MeCore::g_meState; + + const u32 code = 0x08900000; + const u32 flag = code + 0x200; + const u32 marker = code + 0x204; + const u32 heartbeat = code + 0x208; + const char *lines[] = { + "lui t0, 0x0890", + "lw t1, 0x200(t0)", // 0x08900004: waits for the flag + "beq t1, zero, 0x08900004", + "nop", + "ori t2, zero, 0x1234", + "sw t2, 0x204(t0)", + "lui t3, 0x0010", // then counts down, which isn't waiting + "addiu t3, t3, -1", // 0x0890001C + "bne t3, zero, 0x0890001C", + "nop", + "lw t4, 0x208(t0)", // 0x08900028: and beats, which isn't either + "addiu t4, t4, 1", + "sw t4, 0x208(t0)", + "j 0x08900028", + "nop", + }; + u32 addr = code; + for (const char *line : lines) { + std::string error; + if (!MipsAssembleOpcode(line, currentDebugMIPS, addr, &error)) { + printf("ERROR: %s: %s\n", line, error.c_str()); + success = false; + } + addr += 4; + } + const u32 jump[] = { MIPS_MAKE_LUI(MIPS_REG_T0, 0x0890), 0x01000008 /* jr t0 */, MIPS_MAKE_NOP() }; + memcpy(Memory::GetBootPage() + 0x40, jump, sizeof(jump)); + + MeCore::SetReset(false); + MeCore::RunFor(100000); + EXPECT_TRUE(me->waiting); + EXPECT_EQ_HEX(me->cpu.pc, code + 4); + EXPECT_TRUE(MeCore::GetTicks() >= 100000 && MeCore::GetTicks() < 100010); + + Memory::WriteUnchecked_U32(1, flag); + MeCore::RunFor(100000); + EXPECT_FALSE(me->waiting); + EXPECT_EQ_HEX(Memory::ReadUnchecked_U32(marker), 0x1234); + const u32 left = me->cpu.r[MIPS_REG_T3]; + EXPECT_TRUE(left > 0 && left < 0x100000 - 1000); + + MeCore::RunFor(10000000); + EXPECT_EQ_INT(me->cpu.r[MIPS_REG_T3], 0); + EXPECT_FALSE(me->waiting); + EXPECT_TRUE(Memory::ReadUnchecked_U32(heartbeat) > 1000); + + MeCore::Shutdown(); + MMIO::Shutdown(); + DestroyJitHarness(); + return success; +} + +bool TestMeWait() { + return TestMeWaitOn(CPUCore::INTERPRETER) && TestMeWaitOn(CPUCore::IR_INTERPRETER) && TestMeWaitOn(CPUCore::JIT); +} + // Differential fuzzing of the CPU cores. Random straight-line code (ALU, FPU, VFPU, loads and // stores) runs from a random register state on each core, and every core has to end up with the // interpreter's state, NaN payloads aside. diff --git a/unittest/JitHarness.h b/unittest/JitHarness.h index 2c6efce620..723cc31911 100644 --- a/unittest/JitHarness.h +++ b/unittest/JitHarness.h @@ -25,6 +25,7 @@ bool TestMMIO(); bool TestMESpace(); bool TestMeCore(); bool TestMeInterrupts(); +bool TestMeWait(); bool TestCPUEquivalence(); bool TestCPURoundingMode(); bool TestVFPUDotJit(); diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index ef503994b7..ba112a64f9 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -3738,6 +3738,7 @@ TestItem availableTests[] = { TEST_ITEM(MESpace), TEST_ITEM(MeCore), TEST_ITEM(MeInterrupts), + TEST_ITEM(MeWait), TEST_ITEM(CPUEquivalence), TEST_ITEM(CPURoundingMode), TEST_ITEM(VFPUDotJit), From 307ed26927b2e58f1b96bdbd2834d158635897ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 20 Oct 2026 18:59:08 +0200 Subject: [PATCH 2/3] ME: DMACplus transfers and LCD registers A PL080-like controller: channels 4-7 at 0xBC800180, run at once when enabled, following linked list items of five words (source, destination, next, control, configuration). Addresses are physical, and on the ME's bus (a master bit in the control word) the low ones are its eDRAM. Transfer size is in units of the source width. A finished channel with its interrupt bit set shows in 0x04, cleared through 0x08. 0xBC800100-0x10C set the LCD's framebuffer, which goes to sceDisplay. Pixel format 0 is 8888 (mcidclan's samples); others are logged. The colour space conversion registers (0x120-0x160) are only kept so far. Like the other ME devices, this answers only while the ME is emulated. mcidclan's dmacplus-me2sc and sc2me-then-me2sc samples show their transferred eDRAM. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HW/MMIO.cpp | 1 + Core/HW/MeMMIO.cpp | 122 ++++++++++++++++++++++++++++++++++++++++ Core/HW/MeMMIO.h | 2 + Core/MIPS/MeCore.cpp | 4 ++ Core/MIPS/MeCore.h | 9 +++ unittest/JitHarness.cpp | 52 +++++++++++++++++ unittest/JitHarness.h | 1 + unittest/UnitTest.cpp | 1 + 8 files changed, 192 insertions(+) diff --git a/Core/HW/MMIO.cpp b/Core/HW/MMIO.cpp index e6ba500927..cad553324f 100644 --- a/Core/HW/MMIO.cpp +++ b/Core/HW/MMIO.cpp @@ -97,6 +97,7 @@ void Init() { MeMMIO::SysregDevice(), MeMMIO::InterruptDevice(), MeMMIO::MemProtectDevice(), + MeMMIO::DmacplusDevice(), }; for (MMIODevice *device : g_devices) { device->Reset(); diff --git a/Core/HW/MeMMIO.cpp b/Core/HW/MeMMIO.cpp index ff41fe6f87..ed525786ff 100644 --- a/Core/HW/MeMMIO.cpp +++ b/Core/HW/MeMMIO.cpp @@ -15,7 +15,13 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. +#include +#include + #include "Common/Log.h" +#include "Core/Debugger/MemBlockInfo.h" +#include "Core/HLE/sceDisplay.h" +#include "GPU/ge_constants.h" #include "Core/HW/MMIO.h" #include "Core/HW/MeMMIO.h" #include "Core/MemMap.h" @@ -162,6 +168,118 @@ public: void DoState(PointerWrap &p) override {} }; +// A PL080-like DMA controller with the LCD and colour space conversion registers below its +// channels (4-7, at 0x180). Transfers run at once, when a channel is enabled. Addresses are physical; +// on the ME's bus (a master bit in the control word), the low ones are its eDRAM. Each linked list +// item is five words (source, destination, next, control, configuration). +class MeDmacplusDevice : public MMIODevice { +public: + MeDmacplusDevice() : MMIODevice("DMACplus", 0xBC800000, 0x200) {} + + bool Read(MIPSState *mips, u32 offset, int size, u32 *value) override { + u32 *reg = Register(offset); + if (!reg) + return false; + *value = *reg; + return true; + } + + bool Write(MIPSState *mips, u32 offset, int size, u32 value) override { + if (!g_meState || (offset & 3)) + return false; + MeCore::MEState::Dmacplus &d = g_meState->dmacplus; + if (offset == 0x08) { + d.intTCStatus &= ~value; + return true; + } + if (offset == 0x10) { + // No errors to clear. + return true; + } + u32 *reg = Register(offset); + if (!reg) + return false; + *reg = value; + if (offset == 0x100) { + SetLCD(d); + } else if (offset == 0x160 && (value & 1)) { + WARN_LOG(Log::ME, "DMACplus: colour space conversion not implemented"); + } else if (offset >= 0x180 && (offset & 0x1F) == 0x10 && (value & 1)) { + RunChannel(d, (offset - 0x180) / 0x20); + } + return true; + } + + void Reset() override {} + void DoState(PointerWrap &p) override {} + +private: + static u32 *Register(u32 offset) { + if (!g_meState || (offset & 3)) + return nullptr; + MeCore::MEState::Dmacplus &d = g_meState->dmacplus; + if (offset == 0x00 || offset == 0x04 || offset == 0x14) + return &d.intTCStatus; + if (offset >= 0x100 && offset < 0x110) + return &d.lcd[(offset - 0x100) / 4]; + if (offset >= 0x120 && offset <= 0x160) + return &d.csc[(offset - 0x120) / 4]; + if (offset >= 0x180 && (offset & 0x1F) < 0x14) + return &d.channels[(offset - 0x180) / 0x20][(offset & 0x1F) / 4]; + return nullptr; + } + + // Pixel format 0 is 8888 (mcidclan's samples). The others aren't known. + static void SetLCD(const MeCore::MEState::Dmacplus &d) { + if (d.lcd[1] != 0) { + WARN_LOG(Log::ME, "DMACplus: LCD pixel format %d", d.lcd[1]); + } + __DisplaySetFramebuf(d.lcd[0], d.lcd[3], GE_FORMAT_8888, 1); + } + + static u8 *PhysicalPointer(u32 addr, u32 size, bool meBus) { + addr &= 0x1FFFFFFF; + const Memory::AddressSpace *space = g_meState->cpu.space; + if (meBus && addr + size <= space->edramSize) + return space->GetPointerUnchecked(addr); + if (Memory::IsValidRange(addr, size)) + return Memory::GetPointerWriteUnchecked(addr); + return nullptr; + } + + static void RunChannel(MeCore::MEState::Dmacplus &d, int channel) { + u32 *regs = d.channels[channel]; + // Bounded, in case of a list that loops. + for (int item = 0; item < 0x10000; ++item) { + const u32 control = regs[3]; + const u32 width = 1 << std::min((control >> 18) & 7, 4U); + const u32 bytes = (control & 0xFFF) * width; + u8 *src = PhysicalPointer(regs[0], bytes, (control >> 24) & 1); + u8 *dst = PhysicalPointer(regs[1], bytes, (control >> 25) & 1); + if (src && dst) { + memmove(dst, src, bytes); + NotifyMemInfo(MemBlockFlags::WRITE, regs[1] & 0x1FFFFFFF, bytes, "DMACplus"); + } else { + ERROR_LOG(Log::ME, "DMACplus: bad transfer of %d bytes from %08x to %08x", bytes, regs[0], regs[1]); + } + if (control & 0x80000000) + d.intTCStatus |= 1 << (channel + 4); + const u32 next = regs[2]; + if (next == 0) + break; + const u8 *lli = PhysicalPointer(next & ~3, 20, false); + if (!lli) { + ERROR_LOG(Log::ME, "DMACplus: bad linked list item at %08x", next); + break; + } + for (int i = 0; i < 5; ++i) + regs[i] = ((const u32_le *)lli)[i]; + } + regs[4] &= ~1; + } +}; + +static MeDmacplusDevice g_dmacplus; static MeSysregDevice g_sysreg; static MeInterruptDevice g_interrupts; static MeMemProtectDevice g_memProtect; @@ -174,6 +292,10 @@ MMIODevice *InterruptDevice() { return &g_interrupts; } +MMIODevice *DmacplusDevice() { + return &g_dmacplus; +} + MMIODevice *MemProtectDevice() { return &g_memProtect; } diff --git a/Core/HW/MeMMIO.h b/Core/HW/MeMMIO.h index 3f837fedad..55e49f4659 100644 --- a/Core/HW/MeMMIO.h +++ b/Core/HW/MeMMIO.h @@ -28,6 +28,8 @@ namespace MeMMIO { MMIODevice *SysregDevice(); // The ME's interrupt controller (0xBC300000). The main CPU has its own there. MMIODevice *InterruptDevice(); +// DMACplus (0xBC800000): DMA between the CPUs' memories, and the LCD. +MMIODevice *DmacplusDevice(); // Memory protection (0xBC000000), which the ME's startup code unlocks. MMIODevice *MemProtectDevice(); diff --git a/Core/MIPS/MeCore.cpp b/Core/MIPS/MeCore.cpp index ba67c50cf7..7e9088dded 100644 --- a/Core/MIPS/MeCore.cpp +++ b/Core/MIPS/MeCore.cpp @@ -167,6 +167,10 @@ void DoState(PointerWrap &p) { Do(p, me->ramConfig); Do(p, me->mutex); DoArray(p, me->memProtect, ARRAY_SIZE(me->memProtect)); + Do(p, me->dmacplus.intTCStatus); + DoArray(p, me->dmacplus.lcd, ARRAY_SIZE(me->dmacplus.lcd)); + DoArray(p, me->dmacplus.csc, ARRAY_SIZE(me->dmacplus.csc)); + DoArray(p, &me->dmacplus.channels[0][0], 4 * 5); DoArray(p, me->cpu.space->GetPointerUnchecked(0), me->cpu.space->edramSize); DoArray(p, me->cpu.space->GetPointerUnchecked(0x04000000), me->cpu.space->localSize); Do(p, meIntrEvent); diff --git a/Core/MIPS/MeCore.h b/Core/MIPS/MeCore.h index e89053d4a2..147cb4d9f1 100644 --- a/Core/MIPS/MeCore.h +++ b/Core/MIPS/MeCore.h @@ -59,6 +59,15 @@ struct MEState { u32 mutex = 0; // 0x48. Bit 0 is the main CPU's, bit 1 the ME's. u32 memProtect[20]{}; // 0xBC000000-0xBC00004C + // DMACplus (0xBC800000), which moves data between the CPUs' memories, and drives the LCD and the + // colour space conversion. Only what homebrew's ME code uses. + struct Dmacplus { + u32 intTCStatus = 0; // 0x04, channels that finished with their interrupt bit set + u32 lcd[4]{}; // 0x100: framebuffer, pixel format, width, stride + u32 csc[0x11]{}; // 0x120-0x160: colour space conversion + u32 channels[4][5]{}; // 0x180-0x1FF: source, destination, next LLI, control, configuration + } dmacplus; + // Not saved: whether the code at an address is a wait loop (see IsWaitLoop), and where it ends. // 0 for not one. Cleared when the ME's code changes. std::unordered_map waitLoopEnds; diff --git a/unittest/JitHarness.cpp b/unittest/JitHarness.cpp index 560e1b5882..47f79b4b8b 100644 --- a/unittest/JitHarness.cpp +++ b/unittest/JitHarness.cpp @@ -995,6 +995,58 @@ bool TestMeWait() { return TestMeWaitOn(CPUCore::INTERPRETER) && TestMeWaitOn(CPUCore::IR_INTERPRETER) && TestMeWaitOn(CPUCore::JIT); } +// DMACplus moves data between the ME's eDRAM and main memory, following linked list items. +bool TestMeDmacplus() { + Memory::g_PSPModel = PSP_MODEL_FAT; + SetupJitHarness(); + MMIO::Init(); + bool success = true; + + if (!MeCore::Init()) { + printf("No Media Engine on this platform, skipping\n"); + MMIO::Shutdown(); + DestroyJitHarness(); + return true; + } + const Memory::AddressSpace *space = MeCore::g_meState->cpu.space; + for (u32 i = 0; i < 0x2000; i += 4) + space->WriteUnchecked_U32(0xA0000000 | i, 0x80000000 + i); + + // ME to main CPU (channel 5): 0x1000 bytes in 16-byte units, then 0x1000 more from the list. + auto control = [](bool srcME, bool dstME, u32 widthBit, u32 count) { + return (dstME ? 1U << 25 : 0) | (srcME ? 1U << 24 : 0) | (widthBit << 21) | (widthBit << 18) | (1U << 15) | (1U << 12) | count; + }; + const u32 lli = 0x08900000; + const u32 item[5] = { 0x1000, 0x08A01000, 0, control(true, false, 2, 0x400), 1 }; + for (int i = 0; i < 5; ++i) + Memory::WriteUnchecked_U32(item[i], lli + i * 4); + MMIO::Write(&mipsr4k, 0xBC8001B0, 4, 0); + MMIO::Write(&mipsr4k, 0xBC8001A0, 4, 0); + MMIO::Write(&mipsr4k, 0xBC8001A4, 4, 0x48A00000); + MMIO::Write(&mipsr4k, 0xBC8001A8, 4, lli); + MMIO::Write(&mipsr4k, 0xBC8001AC, 4, control(true, false, 4, 0x100) | 0x80000000); + MMIO::Write(&mipsr4k, 0xBC8001B0, 4, 1); + EXPECT_EQ_HEX(MMIO::Read(&mipsr4k, 0xBC8001B0, 4), 0); + EXPECT_EQ_HEX(Memory::ReadUnchecked_U32(0x08A00000), 0xA0000000); + EXPECT_EQ_HEX(Memory::ReadUnchecked_U32(0x08A01FFC), 0xA0001FFC); + EXPECT_EQ_HEX(MMIO::Read(&mipsr4k, 0xBC800004, 4), 1 << 5); + MMIO::Write(&mipsr4k, 0xBC800008, 4, 1 << 5); + EXPECT_EQ_HEX(MMIO::Read(&mipsr4k, 0xBC800004, 4), 0); + + // And back (channel 4), to further into the eDRAM. + MMIO::Write(&mipsr4k, 0xBC800180, 4, 0x08A00000); + MMIO::Write(&mipsr4k, 0xBC800184, 4, 0x10000); + MMIO::Write(&mipsr4k, 0xBC800188, 4, 0); + MMIO::Write(&mipsr4k, 0xBC80018C, 4, control(false, true, 2, 0x800)); + MMIO::Write(&mipsr4k, 0xBC800190, 4, 1); + EXPECT_EQ_HEX(space->ReadUnchecked_U32(0x80011FFC), 0xA0001FFC); + + MeCore::Shutdown(); + MMIO::Shutdown(); + DestroyJitHarness(); + return success; +} + // Differential fuzzing of the CPU cores. Random straight-line code (ALU, FPU, VFPU, loads and // stores) runs from a random register state on each core, and every core has to end up with the // interpreter's state, NaN payloads aside. diff --git a/unittest/JitHarness.h b/unittest/JitHarness.h index 723cc31911..b7fbaab167 100644 --- a/unittest/JitHarness.h +++ b/unittest/JitHarness.h @@ -26,6 +26,7 @@ bool TestMESpace(); bool TestMeCore(); bool TestMeInterrupts(); bool TestMeWait(); +bool TestMeDmacplus(); bool TestCPUEquivalence(); bool TestCPURoundingMode(); bool TestVFPUDotJit(); diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index ba112a64f9..f823701498 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -3739,6 +3739,7 @@ TestItem availableTests[] = { TEST_ITEM(MeCore), TEST_ITEM(MeInterrupts), TEST_ITEM(MeWait), + TEST_ITEM(MeDmacplus), TEST_ITEM(CPUEquivalence), TEST_ITEM(CPURoundingMode), TEST_ITEM(VFPUDotJit), From c6760fa461aec3a64c5c1a72fd118d09a0238a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 20 Oct 2026 19:13:45 +0200 Subject: [PATCH 3/3] ME: DMACplus colour space conversion As the firmware's DMACplus driver programs it (lowio.prx 6.61, sceDmacplus_driver_d126494b for AVC and 9c492b9b for VME, the same in 1.50's dmacplus.prx): eight source buffers at 0x120-0x13C (two half height sets), size in macroblocks and the mode at 0x140, two destinations at 0x144/0x148, stride and output format (0 8888, 1 565, 2 5551, 3 4444) at 0x14C, a Q3.7 matrix at 0x150-0x158 and the luma offset at 0x15C, whose default (0x110, 16) is the firmware's. Writing 0x160 runs it, and it finishes at once, with 0x04 bit 1 set. AVC sources are in the decoder's tiled layout (UntileYCbCr): interleaved into one destination they're the frame's even and odd rows, and going to two, each set is a picture of its own with its own chroma, as mcidclan's me-avc-csc-hd lays out its data. VME sources are planar. Not checked on hardware: 0x140 bits 1 and 2 (taken as chroma line replication and leaving out the second set, from mcidclan's samples) and 0x15C bit 8 (taken as offsetting chroma by 128). mcidclan's vme-hardware-csc, avc-csc-hd and vme-csc-hd-992x992 show their pictures. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HW/MeMMIO.cpp | 152 +++++++++++++++++++++++++++++++++++++++- Core/MIPS/MeCore.cpp | 2 + unittest/JitHarness.cpp | 20 ++++++ 3 files changed, 173 insertions(+), 1 deletion(-) diff --git a/Core/HW/MeMMIO.cpp b/Core/HW/MeMMIO.cpp index ed525786ff..6302032bfa 100644 --- a/Core/HW/MeMMIO.cpp +++ b/Core/HW/MeMMIO.cpp @@ -17,10 +17,13 @@ #include #include +#include #include "Common/Log.h" #include "Core/Debugger/MemBlockInfo.h" #include "Core/HLE/sceDisplay.h" +#include "Core/HLE/sceMpegbase.h" +#include "GPU/GPUCommon.h" #include "GPU/ge_constants.h" #include "Core/HW/MMIO.h" #include "Core/HW/MeMMIO.h" @@ -203,7 +206,7 @@ public: if (offset == 0x100) { SetLCD(d); } else if (offset == 0x160 && (value & 1)) { - WARN_LOG(Log::ME, "DMACplus: colour space conversion not implemented"); + RunCSC(d); } else if (offset >= 0x180 && (offset & 0x1F) == 0x10 && (value & 1)) { RunChannel(d, (offset - 0x180) / 0x20); } @@ -247,6 +250,153 @@ private: return nullptr; } + // The colour space conversion, as the firmware's DMACplus driver programs it (lowio.prx 6.61, + // sceDmacplus_driver_d126494b and 9c492b9b; dmacplus.prx 1.50 the same). The frame is two sets of + // half its height: buffers 0, 1, 4 and 5, and 2, 3, 6 and 7. In AVC mode they're the even and odd + // rows of a frame in the decoder's tiled layout (see UntileYCbCr); in VME mode, planar Y at 0x120 + // (0x128), Cb at 0x130 (0x138) and Cr at 0x134 (0x13C). Each set goes to its own destination, or + // both interleave by rows into the first. Not checked on hardware: what 0x140 bits 1 and 2 do + // (taken as chroma line replication and leaving out the second set, from mcidclan's samples), + // and 0x15C bit 8 (taken as chroma being offset by 128). + static void RunCSC(MeCore::MEState::Dmacplus &d) { + const u32 *r = d.csc; + auto reg = [&](u32 offset) { return r[(offset - 0x120) / 4]; }; + const u32 config = reg(0x140); + const bool vme = (config & 1) != 0; + const bool replicateChroma = (config & 2) != 0; + const bool onlyFirst = (config & 4) != 0; + const int width = ((config >> 8) & 0x3F) * 16; + const int height = ((config >> 16) & 0x3F) * 16; + const u32 output = reg(0x14C); + const int stride = output >> 8; + const int format = (output >> 1) & 3; + const bool separate = (output & 1) != 0; + d.csc[(0x160 - 0x120) / 4] &= ~1; + d.intTCStatus |= 2; + if (width == 0 || height == 0 || stride < width) { + ERROR_LOG(Log::ME, "DMACplus: bad colour space conversion %dx%d, stride %d", width, height, stride); + return; + } + + const int setHeight = height / 2; + const int width2 = width / 2; + // Each set's planes, as rows: luma, and chroma at half width. + const u8 *luma[2]{}, *cb[2]{}, *cr[2]{}; + int lumaStride = width, chromaStride = width2, chromaRowShift = 0; + std::vector planes; + if (vme) { + const int chromaRows = replicateChroma ? setHeight / 2 : setHeight; + chromaRowShift = replicateChroma ? 1 : 0; + for (int s = 0; s < 2; ++s) { + luma[s] = PhysicalPointer(reg(0x120 + s * 8), width * setHeight, true); + cb[s] = PhysicalPointer(reg(0x130 + s * 8), width2 * chromaRows, true); + cr[s] = PhysicalPointer(reg(0x134 + s * 8), width2 * chromaRows, true); + } + } else { + const int blocks = (width / 16) * (height / 16); + const int sizes[8] = { 64 * blocks, 64 * blocks, 64 * blocks, 64 * blocks, 32 * blocks, 32 * blocks, 32 * blocks, 32 * blocks }; + const u8 *src[8]; + for (int i = 0; i < 8; ++i) + src[i] = PhysicalPointer(reg(0x120 + i * 4), sizes[i], true); + planes.resize(width * height + 2 * width2 * (height / 2)); + u8 *frameY = planes.data(); + u8 *frameCb = frameY + width * height; + u8 *frameCr = frameCb + width2 * (height / 2); + if (!separate) { + // The sets are the frame's even and odd rows, both with the frame's chroma rows. + UntileYCbCr(frameY, frameCb, frameCr, src, sizes, width, height); + lumaStride = width * 2; + for (int s = 0; s < 2; ++s) { + luma[s] = frameY + s * width; + cb[s] = frameCb; + cr[s] = frameCr; + } + } else { + // Each set is a picture of its own, with its own chroma at half its height (as + // mcidclan's me-avc-csc-hd lays out its data, n12ToHw.py). + std::vector tall(width * height + 2 * width2 * (height / 2)); + for (int s = 0; s < 2; ++s) { + const u8 *setSrc[8] = { src[s * 2], src[s * 2 + 1], nullptr, nullptr, src[4 + s * 2], src[5 + s * 2], nullptr, nullptr }; + // Untiled as the even rows of a picture twice as tall. + UntileYCbCr(tall.data(), tall.data() + width * height, tall.data() + width * height + width2 * (height / 2), setSrc, sizes, width, height); + u8 *setY = frameY + s * width * setHeight; + for (int y = 0; y < setHeight; ++y) + memcpy(setY + y * width, tall.data() + 2 * y * width, width); + u8 *cbOut = frameCb + s * width2 * (setHeight / 2); + u8 *crOut = frameCr + s * width2 * (setHeight / 2); + const u8 *tallCb = tall.data() + width * height; + const u8 *tallCr = tallCb + width2 * (height / 2); + for (int y = 0; y < setHeight / 2; ++y) { + memcpy(cbOut + y * width2, tallCb + 2 * y * width2, width2); + memcpy(crOut + y * width2, tallCr + 2 * y * width2, width2); + } + luma[s] = setY; + cb[s] = cbOut; + cr[s] = crOut; + } + chromaRowShift = 1; + } + } + + auto coefficient = [](u32 value, int shift) { + int c = (value >> shift) & 0x3FF; + return c >= 0x200 ? c - 0x400 : c; + }; + int coef[3][3]; + for (int c = 0; c < 3; ++c) { + const u32 value = reg(0x150 + c * 4); + coef[c][0] = coefficient(value, 0); + coef[c][1] = coefficient(value, 10); + coef[c][2] = coefficient(value, 20); + } + const int lumaOffset = reg(0x15C) & 0xFF; + const int chromaOffset = (reg(0x15C) & 0x100) ? 128 : 0; + + static const GEBufferFormat formats[4] = { GE_FORMAT_8888, GE_FORMAT_565, GE_FORMAT_5551, GE_FORMAT_4444 }; + const int bpp = format == 0 ? 4 : 2; + const u32 rows = separate ? setHeight : height; + const u32 dstAddr[2] = { reg(0x144), reg(0x148) }; + for (int s = 0; s < 2; ++s) { + if (separate && s == 1 && onlyFirst) + break; + if (!luma[s] || !cb[s] || !cr[s]) { + ERROR_LOG(Log::ME, "DMACplus: colour space conversion source %d not readable", s); + continue; + } + const u32 dst = separate ? dstAddr[s] : dstAddr[0]; + u8 *out = PhysicalPointer(dst, stride * rows * bpp, false); + if (!out) { + ERROR_LOG(Log::ME, "DMACplus: colour space conversion destination %08x not writable", dst); + continue; + } + for (int y = 0; y < setHeight; ++y) { + const u8 *rowY = luma[s] + y * lumaStride; + const u8 *rowCb = cb[s] + (y >> chromaRowShift) * chromaStride; + const u8 *rowCr = cr[s] + (y >> chromaRowShift) * chromaStride; + const int outRow = separate ? y : y * 2 + s; + u8 *outPixels = out + outRow * stride * bpp; + for (int x = 0; x < width; ++x) { + const int Y = rowY[x] - lumaOffset; + const int Cb = rowCb[x >> 1] - chromaOffset; + const int Cr = rowCr[x >> 1] - chromaOffset; + int rgb[3]; + for (int c = 0; c < 3; ++c) + rgb[c] = std::clamp((coef[c][0] * Y + coef[c][1] * Cb + coef[c][2] * Cr + 64) >> 7, 0, 255); + switch (format) { + case 0: ((u32_le *)outPixels)[x] = rgb[0] | (rgb[1] << 8) | (rgb[2] << 16); break; + case 1: ((u16_le *)outPixels)[x] = (rgb[0] >> 3) | ((rgb[1] >> 2) << 5) | ((rgb[2] >> 3) << 11); break; + case 2: ((u16_le *)outPixels)[x] = (rgb[0] >> 3) | ((rgb[1] >> 3) << 5) | ((rgb[2] >> 3) << 10); break; + default: ((u16_le *)outPixels)[x] = (rgb[0] >> 4) | ((rgb[1] >> 4) << 4) | ((rgb[2] >> 4) << 8); break; + } + } + } + const u32 size = stride * rows * bpp; + NotifyMemInfo(MemBlockFlags::WRITE, dst & 0x1FFFFFFF, size, "DMACplusCsc"); + if (gpu) + gpu->PerformWriteFormattedFromMemory(dst & 0x1FFFFFFF, size, stride, formats[format], width); + } + } + static void RunChannel(MeCore::MEState::Dmacplus &d, int channel) { u32 *regs = d.channels[channel]; // Bounded, in case of a list that loops. diff --git a/Core/MIPS/MeCore.cpp b/Core/MIPS/MeCore.cpp index 7e9088dded..eca735f946 100644 --- a/Core/MIPS/MeCore.cpp +++ b/Core/MIPS/MeCore.cpp @@ -94,6 +94,8 @@ static void PowerOn(MEState *me) { cpu.cop0[22] = ME_CPU_ID; cpu.r[MIPS_REG_SP] = 0x80000000 + cpu.space->edramSize - 0x20; me->ramConfig = 0x20000002; + // The luma offset (16) and chroma offset of the firmware's default conversion (avcodec.prx). + me->dmacplus.csc[(0x15C - 0x120) / 4] = 0x110; me->busClock = 0xD61D; // Held in reset, as the register says: a read-modify-write of it mustn't release the ME. me->resetFlags = 4; diff --git a/unittest/JitHarness.cpp b/unittest/JitHarness.cpp index 47f79b4b8b..d13b62f890 100644 --- a/unittest/JitHarness.cpp +++ b/unittest/JitHarness.cpp @@ -1041,6 +1041,26 @@ bool TestMeDmacplus() { MMIO::Write(&mipsr4k, 0xBC800190, 4, 1); EXPECT_EQ_HEX(space->ReadUnchecked_U32(0x80011FFC), 0xA0001FFC); + // Colour space conversion: a planar (VME) 32x32 grey frame to 8888, with the default matrix. + for (u32 i = 0; i < 32 * 32; ++i) + space->WriteUnchecked_U8(128, 0x80020000 + i); + for (u32 i = 0; i < 16 * 16 * 2; ++i) + space->WriteUnchecked_U8(128, 0x80030000 + i); + const u32 csc[] = { + 0x120, 0x20000, 0x128, 0x20000 + 32 * 16, 0x130, 0x30000, 0x134, 0x30000 + 16 * 8, + 0x138, 0x30000 + 16 * 16, 0x13C, 0x30000 + 16 * 24, + 0x140, (2 << 16) | (2 << 8) | (1 << 1) | 1, + 0x144, 0x48B00000, 0x148, 0x48B00000 + 64 * 16 * 4, + 0x14C, (64 << 8) | 1, + 0x150, (0x0CC << 20) | 0x095, 0x154, (0x398 << 20) | (0x3CE << 10) | 0x095, 0x158, (0x102 << 10) | 0x095, + }; + for (size_t i = 0; i < ARRAY_SIZE(csc); i += 2) + MMIO::Write(&mipsr4k, 0xBC800000 + csc[i], 4, csc[i + 1]); + MMIO::Write(&mipsr4k, 0xBC800160, 4, 1); + EXPECT_EQ_HEX(MMIO::Read(&mipsr4k, 0xBC800160, 4) & 1, 0); + EXPECT_EQ_HEX(Memory::ReadUnchecked_U32(0x08B00000), 0x828282); + EXPECT_EQ_HEX(Memory::ReadUnchecked_U32(0x08B00000 + 64 * 31 * 4 + 31 * 4), 0x828282); + MeCore::Shutdown(); MMIO::Shutdown(); DestroyJitHarness();