I installed VMWare player to take a look into this issue today
I'm pretty confident this is just a new bug in their texture streaming code and not an intentional anti-VM change
What seems to be happening is various texture loading is now failing on VMWare, and the client is now waiting for those textures to be fully loaded first. That never happens and the end result is an infinite loading screen
It's hard to say the exact issue, but it could be:
- faulty multithreaded code that is breaking on low performance VMs
- an issue with the texture loading code in the VM environment now
- some sort of texture streaming cache corruption issue
I can clear the number of pending resources to be loaded in a debugger, and the client sort of works again, but of course the textures are the super low resolution ones. Obviously, that's not an issue for anything but pixel bots, but the bigger issue is how can you make the client not wait on interface texture loading again. The system being used is also used for things like knowing when the instance info pointer is valid, so if you just clear the value too soon, the client crashes because that pointer is not valid yet. There may be even more other instability issues trying to work around the issue simply by clearing the value I did.
Image: Imgur: The magic of the Internet
On the title screen, it seems the language flags at the top left are the reason why some people see the lockups when starting up the game (there's a few others, but those are the obvious culprits)
At first, I thought it was only interface textures that had an issue, but I've also noticed partially invisible mobs as well. Dying and returning seemed to "reload" some of the broken textures (not all though) so that's why I think there could also be some sort of texture cache corruption happening. I've also noticed that on the title screen where sometimes the language flags load if you delete production_config.txt and let the client recreate it.
I came up with a very dirty hack to try and work around the issue, but it's not worth the time trying to develop, support, or maintain. However, I'll share it for anyone that wants to waste the time on it. This is a x64 .net 4.8 program and you need to use nuget to add the "System.Runtime.CompilerServices.Unsafe" package.
The idea was pretty simple, keep track of the value and force clear it after it's remained unchanged for 30s. However, there's an issue with this in town, since the client can keep loading new textures as people zone in and out. You could add in logic to track the total time spent and then force clear, or work in game offset specific logic to check game pointers to figure out if that. However, due to poor VM performance varying between systems, 30s might not be enough, so that's something else that might have to change. If you clear the value too soon, you can crash the client. These issues are why it's not worth trying to support this type of project, but I'll at least put out this info to anyone who has the resources to do something with it.
SketchyTextureLoadingLockupFix.cs
Code:
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace SketchyTextureLoadingLockupFix
{
public static class Utility
{
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
public static extern bool ReadProcessMemory(SafeProcessHandle hProcess, UIntPtr lpBaseAddress, [Out] byte[] lpBuffer, ulong dwSize, out ulong lpNumberOfBytesRead);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
public static extern bool WriteProcessMemory(SafeProcessHandle hProcess, UIntPtr lpBaseAddress, byte[] lpBuffer, ulong nSize, out ulong lpNumberOfBytesWritten);
public static unsafe UIntPtr ToUIntPtr(this IntPtr ptr)
{
return new UIntPtr(ptr.ToPointer());
}
public static ulong ToUInt64(this IntPtr ptr)
{
return ptr.ToUIntPtr().ToUInt64();
}
public static byte[] FromStruct<T>(this T value) where T : struct
{
var typeSize = System.Runtime.CompilerServices.Unsafe.SizeOf<T>();
var bytes = new byte[typeSize];
if (bytes.Length > 0)
{
System.Runtime.CompilerServices.Unsafe.CopyBlockUnaligned(ref bytes[0],
ref System.Runtime.CompilerServices.Unsafe.As<T, byte>(ref value), (uint)bytes.Length);
}
return bytes;
}
public static T ToStruct<T>(this byte[] bytes) where T : struct
{
return ToStructArray<T>(bytes)[0];
}
public static T[] ToStructArray<T>(this byte[] bytes) where T : struct
{
var typeSize = System.Runtime.CompilerServices.Unsafe.SizeOf<T>();
if (bytes.Length % typeSize != 0)
throw new ArgumentException("Length of bytes is not a multiple of structure size");
var values = new T[bytes.Length / typeSize];
if (bytes.Length > 0)
{
System.Runtime.CompilerServices.Unsafe.CopyBlockUnaligned(
ref System.Runtime.CompilerServices.Unsafe.As<T, byte>(ref values[0]), ref bytes[0],
(uint)bytes.Length);
}
return values;
}
public static ulong BaseAddress(this Process process)
{
return process.MainModule.BaseAddress.ToUInt64();
}
public static T Read<T>(this Process process, ulong address) where T : struct
{
return process.ReadArray<T>(address, 1)[0];
}
public static T[] ReadArray<T>(this Process process, ulong address, ulong count) where T : struct
{
var buffer = process.ReadBytes(address, count * (ulong)Marshal.SizeOf<T>());
return buffer.ToStructArray<T>();
}
public static byte[] ReadBytes(this Process process, ulong address, ulong count)
{
var buffer = new byte[count];
process.ReadBytes(address, count, buffer);
return buffer;
}
public static void ReadBytes(this Process process, ulong address, ulong count, byte[] buffer)
{
if (!ReadProcessMemory(process.SafeHandle, (UIntPtr)address, buffer, count, out var readCount))
{
throw new Exception($"'ReadProcessMemory' failed with error {Marshal.GetLastWin32Error()}.");
}
if (readCount != count)
{
throw new Exception($"'ReadProcessMemory' read {readCount} / {count} bytes.");
}
}
}
class Program
{
static void Main(string[] args)
{
var procName = "PathOfExile_x64";
//var procName = "[3.14.2.2]PathOfExile_x64";
var procs = Process.GetProcessesByName(procName);
if (procs.Length != 1)
{
Console.WriteLine(
$"{procs.Length} processes were found with name '{procName}'");
return;
}
var clearBytes = new byte[8];
using (var process = procs[0])
{
var baseAddress = process.BaseAddress();
// This is an address in the .data section, so it's going to change every patch
var targetAddress = baseAddress + (0x7FF7321D1E40 - 0x7FF72FC10000);
Stopwatch lastChange = Stopwatch.StartNew();
ulong lastValue = 0;
while (!Console.KeyAvailable)
{
var curValue = process.Read<ulong>(targetAddress);
if (curValue != lastValue)
{
lastValue = curValue;
lastChange.Restart();
Console.WriteLine($"=> {curValue} pending files");
}
if (curValue != 0)
{
if (lastChange.Elapsed.TotalSeconds > 30)
{
Console.WriteLine($"{lastChange.Elapsed} has elapsed with no file load changes. Now force clearing the value...");
if (!Utility.WriteProcessMemory(process.SafeHandle, (UIntPtr)targetAddress, clearBytes, (ulong)clearBytes.Length, out var wrote))
{
throw new Exception($"WriteProcessMemory' failed with error [{Marshal.GetLastWin32Error()}]");
}
}
}
Thread.Sleep(1000);
}
}
}
}
}
One function that makes use of the address to clear starts like this, so you can make a byte sig to make some parts in the function possibly.
The asm listing (3.14.2.2) is relative to the .code section of the client , not the image base, so if your image base was 0x7000, then your code section is at 0x7000 + 0x1000, and the start of this function would be 0x7000 + 0x1000 + 0xF07070
Code:
$+F07070 | 48:8BC4 | mov rax,rsp |
$+F07073 | 56 | push rsi |
$+F07074 | 57 | push rdi |
$+F07075 | 41:56 | push r14 |
$+F07077 | 48:83EC 60 | sub rsp,0x60 |
$+F0707B | 48:C740 A8 FEFFFFFF | mov qword ptr [rax-0x58],0xFFFFFFFFFFFFFFFE |
$+F07083 | 48:8958 08 | mov qword ptr [rax+0x8],rbx |
$+F07087 | 48:8968 10 | mov qword ptr [rax+0x10],rbp |
$+F0708B | 0FB6DA | movzx ebx,dl |
$+F0708E | 48:8BF9 | mov rdi,rcx |
$+F07091 | 4C:8D35 98125A01 | lea r14,qword ptr [0x7FF7320B9330] |
$+F07098 | 4C:8970 20 | mov qword ptr [rax+0x20],r14 |
$+F0709C | 49:8BCE | mov rcx,r14 |
$+F0709F | FF15 A371B400 | call qword ptr [<&RtlAcquireSRWLockExclusive>] |
$+F070A5 | 90 | nop |
$+F070A6 | 48:8B05 939D6B01 | mov rax,qword ptr [0x7FF7321D1E40] | // This is the address you want to clear
$+F070AD | 48:85C0 | test rax,rax |
$+F070B0 | 75 10 | jne 0x7FF730B180C2 |
$+F070B2 | 0FB605 3DEF4701 | movzx eax,byte ptr [0x7FF731F96FF6] |
$+F070B9 | 84C0 | test al,al |
$+F070BB | 75 05 | jne 0x7FF730B180C2 |
$+F070BD | 40:32ED | xor bpl,bpl |
$+F070C0 | EB 03 | jmp 0x7FF730B180C5 |
$+F070C2 | 40:B5 01 | mov bpl,0x1 |
$+F070C5 | 84DB | test bl,bl |
$+F070C7 | 74 10 | je 0x7FF730B180D9 |
$+F070C9 | 0FB605 14AE4101 | movzx eax,byte ptr [0x7FF731F32EE4] |
$+F070D0 | 84C0 | test al,al |
$+F070D2 | 74 05 | je 0x7FF730B180D9 |
$+F070D4 | 40:B6 01 | mov sil,0x1 |
$+F070D7 | EB 03 | jmp 0x7FF730B180DC |
$+F070D9 | 40:32F6 | xor sil,sil |
$+F070DC | 48:8B05 55125A01 | mov rax,qword ptr [0x7FF7320B9338] |
$+F070E3 | 48:3B05 56125A01 | cmp rax,qword ptr [0x7FF7320B9340] |
$+F070EA | 0F84 B3000000 | je 0x7FF730B181A3 |
$+F070F0 | 48:C7C3 FFFFFFFF | mov rbx,0xFFFFFFFFFFFFFFFF |
$+F070F7 | 66:0F1F8400 00000000 | nop word ptr [rax+rax],ax |
$+F07100 | 0F1000 | movups xmm0,xmmword ptr [rax] |
$+F07103 | 0F114424 28 | movups xmmword ptr [rsp+0x28],xmm0 |
$+F07108 | F2:0F1048 10 | movsd xmm1,qword ptr [rax+0x10] |
$+F0710D | F2:0F114C24 38 | movsd qword ptr [rsp+0x38],xmm1 |
$+F07113 | 837C24 38 04 | cmp dword ptr [rsp+0x38],0x4 |
$+F07118 | 74 52 | je 0x7FF730B1816C |
Anyways, this issue is not really worth looking into any more, so good luck to anyone that messes with it!