diff --git a/Core/HLE/ReplaceTables.cpp b/Core/HLE/ReplaceTables.cpp index f75382d3de..3ba38c5716 100644 --- a/Core/HLE/ReplaceTables.cpp +++ b/Core/HLE/ReplaceTables.cpp @@ -1488,6 +1488,7 @@ static int Hook_resistance_draw_trail() { // time, so the higher the frame rate, the more of them are alive. After the angel boss in Bonn the // trails behind the debris make the display list overflow its buffer at 60fps, into the music // stream and beyond (#19755). Locking the CPU to 222MHz, which slows the frame rate, avoids it. + // resistance_update_trail now limits the node rate, so this is a backstop. // The game keeps its two list buffers and their limits next to the write pointer, but doesn't // check them here. Hooked where the node count is about to become the loop count: when the // current buffer is nearly full, skip the trail by making the count zero. @@ -1511,6 +1512,37 @@ static int Hook_resistance_draw_trail() { return 0; } +static int Hook_resistance_update_trail() { + // The trail adds a node on every update that its emitter's rate allows (one per update at most, + // and the explosion debris always allow one), and the effect's clock advances 1/60 per update + // at 60fps. Keep the nodes at least 1/30 of that clock apart, as if the game ran at 30fps, so + // running faster doesn't pack the smoke denser and multiply the trails (#19755). Hooked where the + // accumulator is about to be tested: holding it under the threshold postpones the node. + const u32 trail = currentMIPS->r[MIPS_REG_S0]; + if (!Memory::IsValid4AlignedRange(trail + 0x140, 0x3C)) { + return 0; + } + const u32 emitter = Memory::ReadUnchecked_U32(trail + 0x140); + const u32 nodes = Memory::ReadUnchecked_U32(trail + 0x144); + const s32 count = (s32)Memory::ReadUnchecked_U32(trail + 0x178); + if (count <= 0 || !Memory::IsValid4AlignedAddress(emitter + 8) || !Memory::IsValid4AlignedAddress(nodes + (count - 1) * 4)) { + return 0; + } + const u32 lastNode = Memory::ReadUnchecked_U32(nodes + (count - 1) * 4); + if (!Memory::IsValid4AlignedAddress(lastNode + 0x50)) { + return 0; + } + const float now = currentMIPS->f[20]; + const float lastNodeTime = Memory::ReadUnchecked_Float(lastNode + 0x50); + // Updates come 1/60 apart, so this lets every other one through. + if (now - lastNodeTime < 1.0f / 30.0f - 0.001f) { + const float rate = Memory::ReadUnchecked_Float(emitter + 8); + const float acc = Memory::ReadUnchecked_Float(trail + 0x170); + Memory::WriteUnchecked_Float(std::min(acc, 1.0f - rate - 0.001f), trail + 0x170); + } + return 0; +} + static int Hook_katamari_render_check() { if (!Memory::IsValidRange(currentMIPS->r[MIPS_REG_A0] + 0x3C, 8)) { return 0; @@ -1851,6 +1883,7 @@ static const ReplacementTableEntry entries[] = { { "everybodysgolf_spinner_background", &Hook_everybodysgolf_spinner_background, 0, REPFLAG_HOOKENTER, 0x138 }, { "codedarms_stencil_check", &Hook_codedarms_stencil_check, 0, REPFLAG_HOOKENTER, 0x8C }, { "resistance_draw_trail", &Hook_resistance_draw_trail, 0, REPFLAG_HOOKENTER, 0x198 }, + { "resistance_update_trail", &Hook_resistance_update_trail, 0, REPFLAG_HOOKENTER, 0x438 }, {} }; diff --git a/Core/MIPS/MIPSAnalyst.cpp b/Core/MIPS/MIPSAnalyst.cpp index 6695489065..1baa554ba9 100644 --- a/Core/MIPS/MIPSAnalyst.cpp +++ b/Core/MIPS/MIPSAnalyst.cpp @@ -377,6 +377,7 @@ static const HardHashTableEntry hardcodedHashes[] = { { 0xa85fe8abb88b1c6f, 52, "vector_sub_t", }, { 0xa9194e55cc586557, 268, "memcpy", }, { 0xa91b3d60bd75105b, 28, "vadd_t", }, + { 0xaaf1cc9399336a21, 1872, "resistance_update_trail", }, { 0xab97ec58c58a7c75, 52, "sceVfpuVector3Div", }, { 0xac84fa7571895c9a, 68, "memcpy", }, // Marvel Ultimate Alliance 2 { 0xacc2c11c3ea28320, 268, "ceilf", },