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..6302032bfa 100644 --- a/Core/HW/MeMMIO.cpp +++ b/Core/HW/MeMMIO.cpp @@ -15,7 +15,16 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. +#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" #include "Core/MemMap.h" @@ -162,6 +171,265 @@ 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)) { + RunCSC(d); + } 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; + } + + // 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. + 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 +442,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 0973268a93..eca735f946 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 { @@ -93,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; @@ -166,6 +169,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); @@ -193,6 +200,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 +209,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 +344,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..147cb4d9f1 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,21 @@ 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 + + // 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; + // 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..d13b62f890 100644 --- a/unittest/JitHarness.cpp +++ b/unittest/JitHarness.cpp @@ -918,6 +918,155 @@ 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); +} + +// 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); + + // 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(); + 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 2c6efce620..b7fbaab167 100644 --- a/unittest/JitHarness.h +++ b/unittest/JitHarness.h @@ -25,6 +25,8 @@ bool TestMMIO(); 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 ef503994b7..f823701498 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -3738,6 +3738,8 @@ TestItem availableTests[] = { TEST_ITEM(MESpace), TEST_ITEM(MeCore), TEST_ITEM(MeInterrupts), + TEST_ITEM(MeWait), + TEST_ITEM(MeDmacplus), TEST_ITEM(CPUEquivalence), TEST_ITEM(CPURoundingMode), TEST_ITEM(VFPUDotJit),