fix unlock fps

This commit is contained in:
Lightczx
2023-05-26 16:20:12 +08:00
parent e44a20f202
commit 4178f1b89b
8 changed files with 89 additions and 64 deletions

View File

@@ -28,6 +28,20 @@ internal static class ThrowHelper
throw new OperationCanceledException(message, inner); throw new OperationCanceledException(message, inner);
} }
/// <summary>
/// 无效操作
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <returns>nothing</returns>
/// <exception cref="InvalidOperationException">无效操作异常</exception>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static InvalidOperationException InvalidOperation(string message, Exception? inner)
{
throw new InvalidOperationException(message, inner);
}
/// <summary> /// <summary>
/// 游戏文件操作失败 /// 游戏文件操作失败
/// </summary> /// </summary>

View File

@@ -1,6 +1,5 @@
{ {
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json", "$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
"allowMarshaling": true, "allowMarshaling": true,
"public": true,
"useSafeHandles": false "useSafeHandles": false
} }

View File

@@ -51,6 +51,7 @@ GetForegroundWindow
GetWindowPlacement GetWindowPlacement
GetWindowThreadProcessId GetWindowThreadProcessId
SetForegroundWindow SetForegroundWindow
ReleaseDC
// WinRT // WinRT
IMemoryBufferByteAccess IMemoryBufferByteAccess

View File

@@ -58,12 +58,8 @@ internal static class ProcessInterop
public static Task UnlockFpsAsync(IServiceProvider serviceProvider, Process game) public static Task UnlockFpsAsync(IServiceProvider serviceProvider, Process game)
{ {
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game); IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
UnlockTimingOptions options = new(100, 10000, 3000);
TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100); return unlocker.UnlockAsync(options);
TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000);
TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(3000);
return unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay);
} }
/// <summary> /// <summary>

View File

@@ -2,8 +2,11 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Win32; using Snap.Hutao.Win32;
using System.Buffers.Binary;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
@@ -15,8 +18,6 @@ namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary> /// <summary>
/// 游戏帧率解锁器 /// 游戏帧率解锁器
/// Credit to https://github.com/34736384/genshin-fps-unlock /// Credit to https://github.com/34736384/genshin-fps-unlock
///
/// TODO: Save memory alloc on GameModuleEntryInfo
/// </summary> /// </summary>
[HighQuality] [HighQuality]
internal sealed class GameFpsUnlocker : IGameFpsUnlocker internal sealed class GameFpsUnlocker : IGameFpsUnlocker
@@ -46,12 +47,11 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task UnlockAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit, TimeSpan adjustFpsDelay) public async Task UnlockAsync(UnlockTimingOptions options)
{ {
logger.LogInformation("UnlockAsync called");
Verify.Operation(isValid, "This Unlocker is invalid"); Verify.Operation(isValid, "This Unlocker is invalid");
GameModuleEntryInfo moduleEntryInfo = await FindModuleAsync(findModuleDelay, findModuleLimit).ConfigureAwait(false); GameModuleEntryInfo moduleEntryInfo = await FindModuleAsync(options.FindModuleDelay, options.FindModuleLimit).ConfigureAwait(false);
Must.Argument(moduleEntryInfo.HasValue, "读取游戏内存失败"); Must.Argument(moduleEntryInfo.HasValue, "读取游戏内存失败");
// Read UnityPlayer.dll // Read UnityPlayer.dll
@@ -59,10 +59,10 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
// When player switch between scenes, we have to re adjust the fps // When player switch between scenes, we have to re adjust the fps
// So we keep a loop here // So we keep a loop here
await LoopAdjustFpsAsync(adjustFpsDelay).ConfigureAwait(false); await LoopAdjustFpsAsync(options.AdjustFpsDelay).ConfigureAwait(false);
} }
private static unsafe bool UnsafeReadModulesMemory(Process process, GameModuleEntryInfo moduleEntryInfo, out VirtualMemory memory) private static unsafe bool UnsafeReadModulesMemory(Process process, in GameModuleEntryInfo moduleEntryInfo, out VirtualMemory memory)
{ {
MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer; MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer;
MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly; MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly;
@@ -73,17 +73,22 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
&& ReadProcessMemory((HANDLE)process.Handle, userAssembly.modBaseAddr, lpBuffer + unityPlayer.modBaseSize, userAssembly.modBaseSize, default); && ReadProcessMemory((HANDLE)process.Handle, userAssembly.modBaseAddr, lpBuffer + unityPlayer.modBaseSize, userAssembly.modBaseSize, default);
} }
private static unsafe bool UnsafeReadProcessMemory(Process process, nuint offset, out nuint value) private static unsafe bool UnsafeReadProcessMemory(Process process, nuint baseAddress, out nuint value)
{ {
fixed (nuint* pValue = &value) ulong temp = 0;
bool result = ReadProcessMemory((HANDLE)process.Handle, (void*)baseAddress, (byte*)&temp, 8, default);
if (!result)
{ {
return ReadProcessMemory((HANDLE)process.Handle, (void*)offset, &pValue, unchecked((uint)sizeof(nuint))); ThrowHelper.InvalidOperation("读取进程内存失败", null);
} }
value = (nuint)temp;
return result;
} }
private static unsafe bool UnsafeWriteModuleMemory(Process process, nuint baseAddress, int write) private static unsafe bool UnsafeWriteProcessMemory(Process process, nuint baseAddress, int write)
{ {
return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, &write, sizeof(int), null); return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, &write, sizeof(int), default);
} }
private static unsafe MODULEENTRY32 UnsafeFindModule(int processId, in ReadOnlySpan<byte> moduleName) private static unsafe MODULEENTRY32 UnsafeFindModule(int processId, in ReadOnlySpan<byte> moduleName)
@@ -109,7 +114,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
} }
} }
private static int FindPatternOffsetImplmentation(ReadOnlySpan<byte> memory) private static int IndexOfPattern(in ReadOnlySpan<byte> memory)
{ {
// E8 ?? ?? ?? ?? 85 C0 7E 07 E8 ?? ?? ?? ?? EB 05 // E8 ?? ?? ?? ?? 85 C0 7E 07 E8 ?? ?? ?? ?? EB 05
int second = 0; int second = 0;
@@ -130,10 +135,8 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
return -1; return -1;
} }
private unsafe GameModuleEntryInfo UnsafeGetGameModuleEntryInfo(int processId) private static unsafe GameModuleEntryInfo UnsafeGetGameModuleEntryInfo(int processId)
{ {
logger.LogInformation("UnsafeGetGameModuleEntryInfo called");
MODULEENTRY32 unityPlayer = UnsafeFindModule(processId, "UnityPlayer.dll"u8); MODULEENTRY32 unityPlayer = UnsafeFindModule(processId, "UnityPlayer.dll"u8);
MODULEENTRY32 userAssembly = UnsafeFindModule(processId, "UserAssembly.dll"u8); MODULEENTRY32 userAssembly = UnsafeFindModule(processId, "UserAssembly.dll"u8);
@@ -147,8 +150,6 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
private async Task<GameModuleEntryInfo> FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit) private async Task<GameModuleEntryInfo> FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit)
{ {
logger.LogInformation("FindModuleAsync called");
ValueStopwatch watch = ValueStopwatch.StartNew(); ValueStopwatch watch = ValueStopwatch.StartNew();
using (PeriodicTimer timer = new(findModuleDelay)) using (PeriodicTimer timer = new(findModuleDelay))
{ {
@@ -172,15 +173,13 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
private async Task LoopAdjustFpsAsync(TimeSpan adjustFpsDelay) private async Task LoopAdjustFpsAsync(TimeSpan adjustFpsDelay)
{ {
logger.LogInformation("LoopAdjustFpsAsync called");
using (PeriodicTimer timer = new(adjustFpsDelay)) using (PeriodicTimer timer = new(adjustFpsDelay))
{ {
while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
{ {
if (!gameProcess.HasExited && fpsAddress != 0) if (!gameProcess.HasExited && fpsAddress != 0)
{ {
UnsafeWriteModuleMemory(gameProcess, fpsAddress, launchOptions.TargetFps); UnsafeWriteProcessMemory(gameProcess, fpsAddress, launchOptions.TargetFps);
} }
else else
{ {
@@ -192,66 +191,46 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
} }
} }
private unsafe void UnsafeTryReadModuleMemoryFindFpsAddress(GameModuleEntryInfo moduleEntryInfo) private unsafe void UnsafeTryReadModuleMemoryFindFpsAddress(in GameModuleEntryInfo moduleEntryInfo)
{ {
logger.LogInformation("UnsafeTryReadModuleMemoryFindFpsAddress called");
bool readOk = UnsafeReadModulesMemory(gameProcess, moduleEntryInfo, out VirtualMemory localMemory); bool readOk = UnsafeReadModulesMemory(gameProcess, moduleEntryInfo, out VirtualMemory localMemory);
Verify.Operation(readOk, "读取内存失败"); Verify.Operation(readOk, "读取内存失败");
using (localMemory) using (localMemory)
{ {
int offset = FindPatternOffsetImplmentation(localMemory.GetBuffer().Slice(unchecked((int)moduleEntryInfo.UnityPlayer.modBaseSize))); int offset = IndexOfPattern(localMemory.GetBuffer()[(int)moduleEntryInfo.UnityPlayer.modBaseSize..]);
Must.Range(offset > 0, "未匹配到FPS字节"); Must.Range(offset > 0, "未匹配到FPS字节");
byte* pLocalMemory = (byte*)localMemory.Pointer; byte* pLocalMemory = (byte*)localMemory.Pointer;
MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer; MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer;
MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly; MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly;
logger.LogInformation("Pattern: {bytes}", BitConverter.ToString(localMemory.GetBuffer().Slice((int)(offset + unityPlayer.modBaseSize), 16).ToArray())); //
nuint localMemoryUnityPlayerAddress = (nuint)pLocalMemory; nuint localMemoryUnityPlayerAddress = (nuint)pLocalMemory;
nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.modBaseSize; nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.modBaseSize;
{
logger.LogInformation("localMemoryUnityPlayerAddress {addr:X8}", localMemoryUnityPlayerAddress);
logger.LogInformation("localMemoryUserAssemblyAddress {addr:X8}", localMemoryUserAssemblyAddress);
logger.LogInformation("memory end at {addr:X8}", localMemoryUserAssemblyAddress + userAssembly.modBaseSize);
}
nuint rip = localMemoryUserAssemblyAddress + (uint)offset; nuint rip = localMemoryUserAssemblyAddress + (uint)offset;
rip += *(uint*)(rip + 1) + 5; rip += *(uint*)(rip + 1) + 5;
rip += *(uint*)(rip + 3) + 7; rip += *(uint*)(rip + 3) + 7;
nuint address = (nuint)userAssembly.modBaseAddr + (rip - localMemoryUserAssemblyAddress);
logger.LogInformation("Game Process handle: {handle}", gameProcess.Handle);
nuint ptr = 0; nuint ptr = 0;
nuint address = rip - localMemoryUserAssemblyAddress + (nuint)userAssembly.modBaseAddr; SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0);
logger.LogInformation("UnsafeReadModuleMemory at {addr:x8}|{uaAddr:x8}", address, (nuint)userAssembly.modBaseAddr);
while (ptr == 0)
{
// Critial: The pointer here is always returning 0
// Make this a dead loop.
if (UnsafeReadProcessMemory(gameProcess, address, out ptr))
{
logger.LogInformation("UnsafeReadProcessMemory succeed {addr:x8}", ptr);
}
else
{
logger.LogInformation("UnsafeReadProcessMemory failed");
}
Thread.Sleep(100); logger.LogInformation("UnsafeReadProcessMemory succeed {addr:x8}", ptr);
}
logger.LogInformation("ptr {addr}", ptr);
rip = ptr - (nuint)unityPlayer.modBaseAddr + localMemoryUnityPlayerAddress; rip = ptr - (nuint)unityPlayer.modBaseAddr + localMemoryUnityPlayerAddress;
logger.LogInformation("UnityPlayer addr: {up:x8}, rip addr: {rip:x8}", localMemoryUnityPlayerAddress, rip);
while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9) while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9)
{ {
rip += *(uint*)(rip + 1) + 5; rip += (nuint)(*(int*)(rip + 1) + 5);
logger.LogInformation("rip ? addr: {rip:x8}", rip);
} }
nuint localMemoryActualAddress = rip + *(uint*)(rip + 2) + 6; nuint localMemoryActualAddress = rip + *(uint*)(rip + 2) + 6;
nuint actualOffset = localMemoryActualAddress - localMemoryUnityPlayerAddress; nuint actualOffset = localMemoryActualAddress - localMemoryUnityPlayerAddress;
fpsAddress = (nuint)(unityPlayer.modBaseAddr + actualOffset); fpsAddress = (nuint)(unityPlayer.modBaseAddr + actualOffset);
logger.LogInformation("UnsafeTryReadModuleMemoryFindFpsAddress finished");
} }
} }

View File

@@ -12,9 +12,7 @@ internal interface IGameFpsUnlocker
/// <summary> /// <summary>
/// 异步的解锁帧数限制 /// 异步的解锁帧数限制
/// </summary> /// </summary>
/// <param name="findModuleDelay">每次查找UnityPlayer的延时,推荐100毫秒</param> /// <param name="options">选项</param>
/// <param name="findModuleLimit">查找UnityPlayer的最大阈值,推荐10000毫秒</param>
/// <param name="adjustFpsDelay">每次循环调整的间隔时间推荐2000毫秒</param>
/// <returns>解锁的结果</returns> /// <returns>解锁的结果</returns>
Task UnlockAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit, TimeSpan adjustFpsDelay); Task UnlockAsync(UnlockTimingOptions options);
} }

View File

@@ -0,0 +1,38 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary>
/// 解锁时机选项
/// </summary>
internal readonly struct UnlockTimingOptions
{
/// <summary>
/// 每次查找 Module 的延时
/// </summary>
public readonly TimeSpan FindModuleDelay;
/// <summary>
/// 查找 Module 的最大时间阈值
/// </summary>
public readonly TimeSpan FindModuleLimit;
/// <summary>
/// 每次循环调整的间隔时间
/// </summary>
public readonly TimeSpan AdjustFpsDelay;
/// <summary>
/// 构造一个新的解锁器选项
/// </summary>
/// <param name="findModuleDelayMilliseconds">每次查找UnityPlayer的延时,推荐100毫秒</param>
/// <param name="findModuleLimitMilliseconds">查找UnityPlayer的最大阈值,推荐10000毫秒</param>
/// <param name="adjustFpsDelayMilliseconds">每次循环调整的间隔时间推荐2000毫秒</param>
public UnlockTimingOptions(int findModuleDelayMilliseconds, int findModuleLimitMilliseconds, int adjustFpsDelayMilliseconds)
{
FindModuleDelay = TimeSpan.FromMilliseconds(findModuleDelayMilliseconds);
FindModuleLimit = TimeSpan.FromMilliseconds(findModuleLimitMilliseconds);
AdjustFpsDelay = TimeSpan.FromMilliseconds(adjustFpsDelayMilliseconds);
}
}

View File

@@ -258,7 +258,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.6.11" /> <PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.6.11" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.221-beta"> <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.252-beta">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>