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);
}
/// <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>

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,11 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Win32;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Windows.Win32.Foundation;
@@ -15,8 +18,6 @@ namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary>
/// 游戏帧率解锁器
/// Credit to https://github.com/34736384/genshin-fps-unlock
///
/// TODO: Save memory alloc on GameModuleEntryInfo
/// </summary>
[HighQuality]
internal sealed class GameFpsUnlocker : IGameFpsUnlocker
@@ -46,12 +47,11 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
}
/// <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");
GameModuleEntryInfo moduleEntryInfo = await FindModuleAsync(findModuleDelay, findModuleLimit).ConfigureAwait(false);
GameModuleEntryInfo moduleEntryInfo = await FindModuleAsync(options.FindModuleDelay, options.FindModuleLimit).ConfigureAwait(false);
Must.Argument(moduleEntryInfo.HasValue, "读取游戏内存失败");
// Read UnityPlayer.dll
@@ -59,10 +59,10 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
// When player switch between scenes, we have to re adjust the fps
// 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 userAssembly = moduleEntryInfo.UserAssembly;
@@ -73,17 +73,22 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
&& 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)
@@ -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
int second = 0;
@@ -130,10 +135,8 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
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 userAssembly = UnsafeFindModule(processId, "UserAssembly.dll"u8);
@@ -147,8 +150,6 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
private async Task<GameModuleEntryInfo> FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit)
{
logger.LogInformation("FindModuleAsync called");
ValueStopwatch watch = ValueStopwatch.StartNew();
using (PeriodicTimer timer = new(findModuleDelay))
{
@@ -172,15 +173,13 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
private async Task LoopAdjustFpsAsync(TimeSpan adjustFpsDelay)
{
logger.LogInformation("LoopAdjustFpsAsync called");
using (PeriodicTimer timer = new(adjustFpsDelay))
{
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
{
if (!gameProcess.HasExited && fpsAddress != 0)
{
UnsafeWriteModuleMemory(gameProcess, fpsAddress, launchOptions.TargetFps);
UnsafeWriteProcessMemory(gameProcess, fpsAddress, launchOptions.TargetFps);
}
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);
Verify.Operation(readOk, "读取内存失败");
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字节");
byte* pLocalMemory = (byte*)localMemory.Pointer;
MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer;
MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly;
logger.LogInformation("Pattern: {bytes}", BitConverter.ToString(localMemory.GetBuffer().Slice((int)(offset + unityPlayer.modBaseSize), 16).ToArray())); //
nuint localMemoryUnityPlayerAddress = (nuint)pLocalMemory;
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;
rip += *(uint*)(rip + 1) + 5;
rip += *(uint*)(rip + 3) + 7;
nuint address = (nuint)userAssembly.modBaseAddr + (rip - localMemoryUserAssemblyAddress);
logger.LogInformation("Game Process handle: {handle}", gameProcess.Handle);
nuint ptr = 0;
nuint address = rip - localMemoryUserAssemblyAddress + (nuint)userAssembly.modBaseAddr;
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");
}
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0);
Thread.Sleep(100);
}
logger.LogInformation("ptr {addr}", ptr);
logger.LogInformation("UnsafeReadProcessMemory succeed {addr:x8}", ptr);
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)
{
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 actualOffset = localMemoryActualAddress - localMemoryUnityPlayerAddress;
fpsAddress = (nuint)(unityPlayer.modBaseAddr + actualOffset);
logger.LogInformation("UnsafeTryReadModuleMemoryFindFpsAddress finished");
}
}

View File

@@ -12,9 +12,7 @@ internal interface IGameFpsUnlocker
/// <summary>
/// 异步的解锁帧数限制
/// </summary>
/// <param name="findModuleDelay">每次查找UnityPlayer的延时,推荐100毫秒</param>
/// <param name="findModuleLimit">查找UnityPlayer的最大阈值,推荐10000毫秒</param>
/// <param name="adjustFpsDelay">每次循环调整的间隔时间推荐2000毫秒</param>
/// <param name="options">选项</param>
/// <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>
</PackageReference>
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>