diff --git a/runtime/src/hle/gx/gx_dl.cpp b/runtime/src/hle/gx/gx_dl.cpp index 19dbcf3..daceeb7 100644 --- a/runtime/src/hle/gx/gx_dl.cpp +++ b/runtime/src/hle/gx/gx_dl.cpp @@ -807,21 +807,17 @@ struct DlMayContainDrawVisitor { bool OnNop(uint8_t) { return true; } bool OnBpReg(const uint8_t*, uint32_t) { return true; } - // An array base/stride write means the list is setting up indexed geometry. - bool OnCpReg(const uint8_t*, uint8_t reg, uint32_t) { return !(reg >= 0xA0 && reg <= 0xBF); } + // CP writes must reach the HLE vertex state, and indexed XF loads need their + // array resolved, so neither may be passed through raw. + bool OnCpReg(const uint8_t*, uint8_t, uint32_t) { return false; } bool OnXfReg(const uint8_t*, uint32_t) { return true; } - bool OnIndexedXf(const uint8_t*, uint8_t, uint32_t) { return true; } + bool OnIndexedXf(const uint8_t*, uint8_t, uint32_t) { return false; } bool OnInvalidateVertexCache(uint8_t) { return true; } // Draw opcodes reach this too (kHandlesDraw is false): a draw opcode, or a // command byte this walker does not model, both mean "assume it draws". bool OnUnknownCommand(uint8_t) { return false; } - bool OnCallDisplayList(uint32_t addr, uint32_t size, int depth) { - if (addr == 0 || size == 0) return true; - const uint8_t* nested = static_cast(GuestToHostPtr(addr, size)); - if (!nested) return false; - DlMayContainDrawVisitor child; - return WalkDisplayList(nested, size, child, depth + 1); - } + // Aurora skips nested calls, so the child has to be flattened. + bool OnCallDisplayList(uint32_t addr, uint32_t size, int) { return addr == 0 || size == 0; } }; static bool DisplayListMayContainDraw(const uint8_t* data, uint32_t nbytes) { @@ -1142,8 +1138,7 @@ static void ApplyAuroraVtxDesc() { // GXGetVtxDesc only decodes GX_VA_PNMTXIDX..GX_VA_TEX7; the XF array // pseudo-attributes above GX_VA_TEX7 have no CP descriptor bits, so // probing them would always report GX_NONE and republish a no-op. - if (!needsPublish && !IsMatrixIndexAttr(gxAttr) && gxAttr <= GX_VA_TEX7 && - (type == GX_INDEX8 || type == GX_INDEX16)) { + if (!needsPublish && gxAttr <= GX_VA_TEX7) { needsPublish = !AuroraVtxDescMatchesLive(gxAttr, type); } if (needsPublish) { diff --git a/runtime/src/hle/gx/gx_fifo.cpp b/runtime/src/hle/gx/gx_fifo.cpp index 08282d7..108e4fd 100644 --- a/runtime/src/hle/gx/gx_fifo.cpp +++ b/runtime/src/hle/gx/gx_fifo.cpp @@ -490,9 +490,16 @@ void HleFifoWrite(u32 val, uint32_t sizeBytes) { const uint16_t vtxCount = ReadBE16(data + 1); const GXVtxFmt vtxFmt = static_cast(cmd & GX_VAT_MASK_CMD); const GXPrimitive prim = OpcodeToGXPrimitive(cmd); + if (vtxCount == 0) { + if (!consumeBytes(3, sink)) break; + continue; + } uint32_t rawVertexSize = 0; - if (TryGetRawDirectFifoVertexSize(vtxFmt, rawVertexSize)) { - const uint32_t packetBytes = 3u + static_cast(vtxCount) * rawVertexSize; + const uint32_t packetBytes = TryGetRawDirectFifoVertexSize(vtxFmt, rawVertexSize) + ? 3u + static_cast(vtxCount) * rawVertexSize + : 0u; + // A packet that cannot fit the buffer is streamed per vertex instead. + if (packetBytes != 0 && packetBytes <= g_hleGxState.fifoBytes.size()) { if (g_hleGxState.fifoByteCount < packetBytes) { break; } diff --git a/runtime/src/hle/gx/gx_texture.cpp b/runtime/src/hle/gx/gx_texture.cpp index c3c6746..0f79533 100644 --- a/runtime/src/hle/gx/gx_texture.cpp +++ b/runtime/src/hle/gx/gx_texture.cpp @@ -703,8 +703,11 @@ extern "C" void GX__LoadTexObj_80170f2c(uint32_t oa, uint32_t tid) { } PPC_NATIVE_OVERRIDE_VOID(80170f2c, GX__LoadTexObj_80170f2c, (uint32_t oa, uint32_t tid), (oa, tid)); -extern "C" void GX__LoadTexObjPreLoaded_80170dc8(uint32_t oa, uint32_t tid) { GX__LoadTexObj_80170f2c(oa, tid); } -PPC_NATIVE_OVERRIDE_VOID(80170dc8, GX__LoadTexObjPreLoaded_80170dc8, (uint32_t oa, uint32_t tid), (oa, tid)); +extern "C" void GX__LoadTexObjPreLoaded_80170dc8(uint32_t oa, uint32_t region, uint32_t tid) { + (void)region; + GX__LoadTexObj_80170f2c(oa, tid); +} +PPC_NATIVE_OVERRIDE_VOID(80170dc8, GX__LoadTexObjPreLoaded_80170dc8, (uint32_t oa, uint32_t region, uint32_t tid), (oa, region, tid)); // ============================================================================ // Texture Object Getters diff --git a/runtime/src/hle/gx/gx_vertex.cpp b/runtime/src/hle/gx/gx_vertex.cpp index 6f87268..3a8c43b 100644 --- a/runtime/src/hle/gx/gx_vertex.cpp +++ b/runtime/src/hle/gx/gx_vertex.cpp @@ -150,7 +150,7 @@ extern "C" void GX__SetVtxAttrFmtv_8016de08(uint32_t vf, uint32_t la) { while(true){ uint32_t a=Memory::Read32(p); if(a==0xFFu) break; - list.push_back({a, Memory::Read32(p+4), Memory::Read32(p+8), Memory::Read32(p+12)&0xFFu}); + list.push_back({a, Memory::Read32(p+4), Memory::Read32(p+8), Memory::Read8(p+12)}); p+=16; } GxWorker::Run([vf, list = std::move(list)] { @@ -326,7 +326,7 @@ extern "C" void GX__Begin_8016f0f0(uint32_t t, uint32_t vf, uint32_t nv) { static void GxBeginImpl(uint32_t t, uint32_t vf, uint32_t nv) { ApplyAuroraVtxStateForBegin(static_cast(vf)); g_hleGxState.currentVtxFmt=(GXVtxFmt)vf; g_hleGxState.currentPrim=(GXPrimitive)t; - g_hleGxState.vertsRemaining=nv; g_hleGxState.inBegin=true; g_hleGxState.auroraBeginCalled=false; + g_hleGxState.vertsRemaining=nv; g_hleGxState.inBegin=nv!=0; g_hleGxState.auroraBeginCalled=false; g_hleGxState.fifoReadOffset = 0; g_hleGxState.fifoByteCount=0; g_hleGxState.ResetVertex(); } diff --git a/runtime/src/hle/storage/dvd.cpp b/runtime/src/hle/storage/dvd.cpp index 6171fa7..091dab6 100644 --- a/runtime/src/hle/storage/dvd.cpp +++ b/runtime/src/hle/storage/dvd.cpp @@ -1031,16 +1031,17 @@ extern "C" int32_t DVDLowInit_80164848() { } PPC_NATIVE_OVERRIDE(80164848, DVDLowInit_80164848, int32_t, (), ()); -extern "C" int32_t DVDLowInquiry_80165A30(uint32_t cmdBlockPtr, uint32_t callback) +// DVDLowInquiry(DVDDriveInfo* info, DVDLowCallback cb); reports a retail drive. +extern "C" int32_t DVDLowInquiry_80165A30(uint32_t driveInfoPtr, uint32_t callback) { - // HLE: acknowledge the drive is present. Must mark the command block complete - // (State = 0) or callers polling this address hang waiting for "Busy" to clear. - if (cmdBlockPtr) { - Memory::Write32(cmdBlockPtr + DVD_CB_OFFSET_STATE, DVD_STATE_END); + if (driveInfoPtr && Memory::Contains(driveInfoPtr, 0x20)) { + Memory::Write32(driveInfoPtr + 0x0, 0x00000002); // revision level, device code + Memory::Write32(driveInfoPtr + 0x4, 0x20060526); // release date + Memory::Write32(driveInfoPtr + 0x8, 0x41000000); } + InvokeDvdLowCallback(callback, DvdReadContract::kInterruptTransferComplete); CompleteDvdCancelState(); - - return 1; // Return 1 to indicate the command was successfully issued. + return 1; } PPC_NATIVE_OVERRIDE(80165A30, DVDLowInquiry_80165A30, int32_t, (uint32_t b, uint32_t c), (b, c)); @@ -1051,11 +1052,21 @@ extern "C" int32_t DVDLowReadDiskID_80164AAC(uint32_t diskIdPtr, uint32_t callba Memory::Write16(diskIdPtr + 0x04, 0x3031); Memory::Write8 (diskIdPtr + 0x06, 0x01); } + InvokeDvdLowCallback(callback, DvdReadContract::kInterruptTransferComplete); CompleteDvdCancelState(); return 1; // Success } PPC_NATIVE_OVERRIDE(80164AAC, DVDLowReadDiskID_80164AAC, int32_t, (uint32_t p, uint32_t c), (p, c)); +// Host files need no head positioning. +extern "C" int32_t DVDLowSeek_801664E0(uint32_t offset, uint32_t callback) { + (void)offset; + InvokeDvdLowCallback(callback, DvdReadContract::kInterruptTransferComplete); + CompleteDvdCancelState(); + return 1; +} +PPC_NATIVE_OVERRIDE(801664E0, DVDLowSeek_801664E0, int32_t, (uint32_t o, uint32_t c), (o, c)); + // 0x80166330 -> DVDLowRead (And 0x80165708 UnencryptedRead) // The game calls this to read the Disk Header (offset 0) or raw data. extern "C" int32_t DVDLowRead_80166330(uint32_t buffer, uint32_t length, uint32_t offset, uint32_t callback) diff --git a/runtime/src/hle/storage/nand_isfs.cpp b/runtime/src/hle/storage/nand_isfs.cpp index b077605..d91dd89 100644 --- a/runtime/src/hle/storage/nand_isfs.cpp +++ b/runtime/src/hle/storage/nand_isfs.cpp @@ -964,7 +964,7 @@ static int32_t HandleIsfsReadDir(uint32_t numIn, uint32_t numOut, uint32_t vecto } const std::filesystem::path hostPath = TranslateNandPath(wiiPath.c_str()); if (!IsDirectory(hostPath)) { - return ISFS_ENOENT; + return PathExists(hostPath) ? ISFS_EINVAL : ISFS_ENOENT; } // NAND names are at most 12 characters; longer host names cannot exist on diff --git a/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.Locals.cs b/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.Locals.cs index 6aac3df..abf54e5 100644 --- a/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.Locals.cs +++ b/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.Locals.cs @@ -253,6 +253,12 @@ public sealed partial class CxxLinearCodeGenerator ? index : -1; + private static uint ClearPairedBit(uint mask, string destination) + { + var bit = PairedFloatRegisterBit(destination); + return bit >= 0 ? mask & ~(1u << bit) : mask; + } + private static HashSet MaterializePairedFloatSet(uint mask, StringComparer comparer) { var set = new HashSet(comparer); @@ -454,11 +460,21 @@ public sealed partial class CxxLinearCodeGenerator break; } case IrLoad load: + newOut = ClearPairedBit(newOut, load.Destination); + break; + case IrResolvedLoad load: + newOut = ClearPairedBit(newOut, load.Destination); + break; + case IrResolvedLoadPair pair: + newOut = ClearPairedBit(newOut, pair.FirstDestination); + newOut = ClearPairedBit(newOut, pair.SecondDestination); + break; + case IrResolvedPsqLoad load: { var destBit = PairedFloatRegisterBit(load.Destination); if (destBit >= 0) { - newOut &= ~(1u << destBit); + newOut |= 1u << destBit; } break; } diff --git a/translator/src/Translator.Core/Disassembly/JumpTableDetector.cs b/translator/src/Translator.Core/Disassembly/JumpTableDetector.cs index 73a0eb6..6c319c5 100644 --- a/translator/src/Translator.Core/Disassembly/JumpTableDetector.cs +++ b/translator/src/Translator.Core/Disassembly/JumpTableDetector.cs @@ -70,7 +70,7 @@ internal static class JumpTableDetector } entriesAreRelative = HasRelativeEntryAdd(ordered, loadIndex, mtctrIndex, targetRegister, baseReg.Name); - if (!TryFindUpperBound(ordered, loadIndex, indexReg.Name, out var upperBound) || upperBound < 0 || upperBound >= MaxEntryCount) + if (!TryFindUpperBound(ordered, loadIndex, indexReg, out var upperBound) || upperBound < 0 || upperBound >= MaxEntryCount) { return false; } @@ -381,47 +381,60 @@ internal static class JumpTableDetector return false; } - private static bool TryFindUpperBound(IReadOnlyList ordered, int startIndex, string register, out int upperBound) + private static bool TryFindUpperBound(IReadOnlyList ordered, int startIndex, PpcRegisterOperand register, out int upperBound) { - // Preferred: compare directly on the same register used for lwzx indexing. + // Preferred: the compare on the lwzx index, followed back through the copies and + // shifts that derived it. A compare older than the value's definition bounds + // something else. + var tracked = register.Number; for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--) { var ins = ordered[i]; - if (ins.Operands.Count < 2 || ins.Operands[0] is not PpcRegisterOperand reg || - !string.Equals(reg.Name, register, StringComparison.OrdinalIgnoreCase) || - ins.Operands[1] is not PpcImmediateOperand imm) + if (TryGetCompareImmediate(ins, out var compared, out var bound)) + { + if (compared == tracked) + { + upperBound = bound; + return true; + } + continue; + } + + if (ins.IsCall) + { + break; + } + + if (!DefinesGpr(ins, tracked)) { continue; } - if (ins.Mnemonic is "cmplwi" or "cmpwi") + if (IsIndexDerivation(ins) && + ins.Operands[0] is PpcRegisterOperand dest && dest.Number == tracked && + ins.Operands[1] is PpcRegisterOperand source) { - upperBound = imm.Value; - return true; + tracked = source.Number; + continue; } + + break; } // Fallback: some loops compare a logical counter (e.g. r18) distinct from the byte-offset // register (e.g. r30) that feeds lwzx, so the bound check may not target the index register. for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--) { - var ins = ordered[i]; - if (ins.Operands.Count < 2 || ins.Operands[1] is not PpcImmediateOperand imm) + if (ordered[i].Operands.Count > 0 && ordered[i].Operands[0] is PpcConditionRegisterOperand) + { + continue; + } + if (!TryGetCompareImmediate(ordered[i], out _, out var bound) || bound < 0 || bound >= MaxEntryCount) { continue; } - if (ins.Mnemonic is not ("cmplwi" or "cmpwi")) - { - continue; - } - - if (imm.Value < 0 || imm.Value >= MaxEntryCount) - { - continue; - } - - upperBound = imm.Value; + upperBound = bound; return true; } @@ -429,6 +442,70 @@ internal static class JumpTableDetector return false; } + // Copies, sign extensions and left shifts (slwi, clrlwi, clrlslwi) keep a bound on + // the source a bound on the result's entry index. + private static bool IsIndexDerivation(PpcInstruction ins) => ins.Mnemonic switch + { + "mr" or "extsb" or "extsh" => ins.Operands.Count >= 2, + "rlwinm" => ins.Operands.Count == 5 && + ins.Operands[2] is PpcImmediateOperand sh && + ins.Operands[4] is PpcImmediateOperand me && + me.Value == 31 - sh.Value, + _ => false, + }; + + // MayWriteGpr treats both register fields of an X-form as written, which also + // counts sources (or, extsb, stwx, cmpw); read the decoded destination instead. + private static bool DefinesGpr(PpcInstruction ins, int register) + { + if (ins.Word >> 26 != 31) + { + return PpcRegisterEffects.MayWriteGpr(ins.Word, register); + } + + var mnemonic = ins.Mnemonic; + if (mnemonic.EndsWith("ux", StringComparison.Ordinal) && + ins.Operands.Count > 1 && ins.Operands[1] is PpcRegisterOperand updated && updated.Number == register) + { + return true; + } + + if (mnemonic.StartsWith("st", StringComparison.Ordinal) || + mnemonic.StartsWith("cmp", StringComparison.Ordinal) || + mnemonic.StartsWith("mt", StringComparison.Ordinal) || + mnemonic.StartsWith("dcb", StringComparison.Ordinal) || + mnemonic.StartsWith("icb", StringComparison.Ordinal) || + mnemonic.StartsWith("tw", StringComparison.Ordinal)) + { + return false; + } + + return ins.Operands.Count > 0 && ins.Operands[0] is PpcRegisterOperand dest && dest.Number == register; + } + + private static bool TryGetCompareImmediate(PpcInstruction ins, out int register, out int immediate) + { + register = -1; + immediate = 0; + if (ins.Mnemonic is not ("cmplwi" or "cmpwi")) + { + return false; + } + + var operands = ins.Operands; + var first = operands.Count > 0 && operands[0] is PpcConditionRegisterOperand ? 1 : 0; + if (operands.Count < first + 2 || + operands[first] is not PpcRegisterOperand reg || + operands[first + 1] is not PpcImmediateOperand imm) + { + return false; + } + + register = reg.Number; + immediate = imm.Value; + return true; + } + private static bool TryReadWord(ProgramImage image, uint address, out uint value) { if (!image.Contains(address, sizeof(uint))) diff --git a/translator/tests/Translator.Tests/FunctionTranslatorCoverageTests.cs b/translator/tests/Translator.Tests/FunctionTranslatorCoverageTests.cs index 9f2f229..ae9c617 100644 --- a/translator/tests/Translator.Tests/FunctionTranslatorCoverageTests.cs +++ b/translator/tests/Translator.Tests/FunctionTranslatorCoverageTests.cs @@ -538,4 +538,122 @@ public class FunctionTranslatorCoverageTests Assert.Equal("// generated", result.CxxCode); Assert.Same(metrics, result.Metrics); } + + [Fact] + public void Translate_JumpTableBoundFollowsShiftedIndexPastStaleCompare() + { + const uint entry = 0x80000000; + const uint table = 0x80000100; + const uint case0 = entry + 0x20; + const uint case1 = entry + 0x28; + const uint case2 = entry + 0x30; + + var image = TranslatorCppTestHarness.CreateImage( + (entry + 0x00, 0x2C040001), // cmpwi r4, 1 + (entry + 0x04, 0x28030002), // cmplwi r3, 2 + (entry + 0x08, 0x5464103A), // slwi r4, r3, 2 + (entry + 0x0C, 0x3D808000), // lis r12, 0x8000 + (entry + 0x10, 0x398C0100), // addi r12, r12, 0x100 + (entry + 0x14, 0x7C0C202E), // lwzx r0, r12, r4 + (entry + 0x18, 0x7C0903A6), // mtctr r0 + (entry + 0x1C, 0x4E800420), // bctr + (case0 + 0x00, 0x38600007), // li r3, 7 + (case0 + 0x04, 0x4E800020), // blr + (case1 + 0x00, 0x38600009), // li r3, 9 + (case1 + 0x04, 0x4E800020), // blr + (case2 + 0x00, 0x3860000B), // li r3, 11 + (case2 + 0x04, 0x4E800020), // blr + (table + 0x00, case0), + (table + 0x04, case1), + (table + 0x08, case2)); + var result = new FunctionTranslator(image).Translate( + entry, + TranslationOptions.Default with + { + MaxBytes = 0x80, + AllowUnsupportedInstructions = true, + KnownFunctionEntryPoints = new HashSet { entry } + }); + + Assert.Contains("case 0x80000030u:", result.CxxCode, StringComparison.Ordinal); + } + + [Fact] + public void Translate_JumpTableBoundSurvivesCopyOfIndex() + { + const uint entry = 0x80000000; + const uint table = 0x80000100; + const uint case0 = entry + 0x28; + const uint case1 = entry + 0x30; + const uint case2 = entry + 0x38; + + var image = TranslatorCppTestHarness.CreateImage( + (entry + 0x00, 0x28030002), // cmplwi r3, 2 + (entry + 0x04, 0x7C7F1B78), // mr r31, r3 + (entry + 0x08, 0x2C040001), // cmpwi r4, 1 + (entry + 0x0C, 0x5460103A), // slwi r0, r3, 2 + (entry + 0x10, 0x3D808000), // lis r12, 0x8000 + (entry + 0x14, 0x398C0100), // addi r12, r12, 0x100 + (entry + 0x18, 0x7C0C002E), // lwzx r0, r12, r0 + (entry + 0x1C, 0x7C0903A6), // mtctr r0 + (entry + 0x20, 0x4E800420), // bctr + (entry + 0x24, 0x4E800020), // blr + (case0 + 0x00, 0x38600007), // li r3, 7 + (case0 + 0x04, 0x4E800020), // blr + (case1 + 0x00, 0x38600009), // li r3, 9 + (case1 + 0x04, 0x4E800020), // blr + (case2 + 0x00, 0x3860000B), // li r3, 11 + (case2 + 0x04, 0x4E800020), // blr + (table + 0x00, case0), + (table + 0x04, case1), + (table + 0x08, case2)); + var result = new FunctionTranslator(image).Translate( + entry, + TranslationOptions.Default with + { + MaxBytes = 0x80, + AllowUnsupportedInstructions = true, + KnownFunctionEntryPoints = new HashSet { entry } + }); + + Assert.Contains("case 0x80000038u:", result.CxxCode, StringComparison.Ordinal); + } + + [Fact] + public void Translate_JumpTableBoundAcceptsNonCr0Compare() + { + const uint entry = 0x80000000; + const uint table = 0x80000100; + const uint case0 = entry + 0x1C; + const uint case1 = entry + 0x24; + const uint case2 = entry + 0x2C; + + var image = TranslatorCppTestHarness.CreateImage( + (entry + 0x00, 0x28830002), // cmplwi cr1, r3, 2 + (entry + 0x04, 0x5463103A), // slwi r3, r3, 2 + (entry + 0x08, 0x3D808000), // lis r12, 0x8000 + (entry + 0x0C, 0x398C0100), // addi r12, r12, 0x100 + (entry + 0x10, 0x7C0C182E), // lwzx r0, r12, r3 + (entry + 0x14, 0x7C0903A6), // mtctr r0 + (entry + 0x18, 0x4E800420), // bctr + (case0 + 0x00, 0x38600007), // li r3, 7 + (case0 + 0x04, 0x4E800020), // blr + (case1 + 0x00, 0x38600009), // li r3, 9 + (case1 + 0x04, 0x4E800020), // blr + (case2 + 0x00, 0x3860000B), // li r3, 11 + (case2 + 0x04, 0x4E800020), // blr + (table + 0x00, case0), + (table + 0x04, case1), + (table + 0x08, case2)); + var result = new FunctionTranslator(image).Translate( + entry, + TranslationOptions.Default with + { + MaxBytes = 0x80, + AllowUnsupportedInstructions = true, + KnownFunctionEntryPoints = new HashSet { entry } + }); + + Assert.Contains("case 0x8000002Cu:", result.CxxCode, StringComparison.Ordinal); + } } diff --git a/translator/tests/Translator.Tests/PairedFlowCodeGenTests.cs b/translator/tests/Translator.Tests/PairedFlowCodeGenTests.cs index 0279a63..59e1891 100644 --- a/translator/tests/Translator.Tests/PairedFlowCodeGenTests.cs +++ b/translator/tests/Translator.Tests/PairedFlowCodeGenTests.cs @@ -152,4 +152,69 @@ public sealed class PairedFlowCodeGenTests Assert.Contains("SetCRFloatResident(cr, 0, f31.d, f1.d);", code, StringComparison.Ordinal); Assert.DoesNotContain("SetCRFloatResident(cr, 0, PPC_PsToScalarInline(f31.d)", code, StringComparison.Ordinal); } + + [Fact] + public void ResolvedScalarLoadEndsPairedStateAcrossEdge() + { + var function = new IrFunction("resolved_load_clears_paired", "entry", new[] + { + new IrBasicBlock("entry", new IrInstruction[] + { + new IrCall("f5", "PPC_PsMul", new[] { IrValue.Register("f1"), IrValue.Register("f2") }), + new IrResolveGuestMemoryRange("range", IrValue.Register("r3"), 0, 8, true, false), + new IrResolvedLoad("f5", "range", new IrAddress("r3", 0), 0, 8), + new IrJump("next") + }), + new IrBasicBlock("next", new IrInstruction[] + { + new IrSetCrField(0, IrValue.Register("f5"), IrValue.Register("f1"), false), + new IrReturn(null) + }) + }); + var types = new RepresentationEnvironment(new Dictionary + { + ["f1"] = ValueRepresentation.Float64, + ["f2"] = ValueRepresentation.Float64, + ["f5"] = ValueRepresentation.Float64, + ["r3"] = ValueRepresentation.UInt32 + }); + var signature = new FunctionAbiClassification("resolved_load_clears_paired", ValueRepresentation.Void); + + var code = new CxxLinearCodeGenerator().Emit( + 0x80006008, new SsaTransformer().Convert(function), signature, types); + + Assert.Contains("SetCRFloatResident(cr, 0, f5.d, f1.d);", code, StringComparison.Ordinal); + Assert.DoesNotContain("PPC_PsToScalarInline(f5.d)", code, StringComparison.Ordinal); + } + + [Fact] + public void ResolvedPsqLoadStaysPairedAcrossEdge() + { + var function = new IrFunction("resolved_psq_load_paired", "entry", new[] + { + new IrBasicBlock("entry", new IrInstruction[] + { + new IrResolveGuestMemoryRange("range", IrValue.Register("r3"), 0, 8, true, false), + new IrResolvedPsqLoad("f5", "range", IrValue.Register("r3"), 0, 0, 0, 0u), + new IrJump("next") + }), + new IrBasicBlock("next", new IrInstruction[] + { + new IrSetCrField(0, IrValue.Register("f5"), IrValue.Register("f1"), false), + new IrReturn(null) + }) + }); + var types = new RepresentationEnvironment(new Dictionary + { + ["f1"] = ValueRepresentation.Float64, + ["f5"] = ValueRepresentation.Float64, + ["r3"] = ValueRepresentation.UInt32 + }); + var signature = new FunctionAbiClassification("resolved_psq_load_paired", ValueRepresentation.Void); + + var code = new CxxLinearCodeGenerator().Emit( + 0x8000600C, new SsaTransformer().Convert(function), signature, types); + + Assert.Contains("SetCRFloatResident(cr, 0, PPC_PsToScalarInline(f5.d), f1.d);", code, StringComparison.Ordinal); + } }