Glamourer issues with Penumbra
Issue: Running Glamourer causes initially slow loading / Inconsistent crashes on load in Windows 11 & Wine-XIV 10.4.1 & hard CTD on Proton-XIV 9.26 everytime
Error:
Fatal error. 0xC0000005
at Penumbra.Interop.Hooks.PostProcessing.ShaderReplacementFixer.PrepareColorTableDetour(FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.MaterialResourceHandle*, Byte, Byte)
at Glamourer.Interop.Material.PrepareColorSet.Detour(FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.MaterialResourceHandle*, Penumbra.GameData.Structs.StainId, Penumbra.GameData.Structs.StainId)
at Dalamud.Game.Framework.HandleFrameworkUpdate(FFXIVClientStructs.FFXIV.Client.System.Framework.Framework*)
Attempted Workaround: Removing Glamourer resolves crashing issues on Proton-XIV & obscenely long loading times on Wine-XIV. Note: Have yet to test on Windows proper but I was having issues with that as well. [I dual boot and am lazy/haven't fully moved over to linux yet]
Additional Information: Reinstalling Glamourer causes immediate CTD prior to Square Enix logo on Proton-XIV, and crashes upon reinstall/renable once in game
After uninstalling Glamourer, the game is loading consistently with no errors, so something seems wrong with Penumbra + Glamourer. (Have not tested with Penumbra disabled and Glamourer enabled though)
I will add additional logs if I can reproduce on Windows or Wine-XIV again, but I wanted to create this while I was working on my issues in Proton-XIV before I forgot and as I reproduce steps.
Exact same issue present on NixOS, successful launch with Penumbra or Glamourer enabled feels like it happens only once in every 20 launches, though hard crashing instead of simply freezing before the Square Enix logo is a new failure case for me as of today. Dalamud in general has more issues than it used to prior to 7.2, but these two plugins are an extreme case.
This issue is present under Fedora 41 & 42 from my view it seems to point to an issue with Penumbra and Galmourer but debugging wine on Linux appears to be a royal pain
Issue also present on EndeavourOS using the default Wine-XIV included in xivlauncher-git. I'm getting the same error and CTD in PrepareColorTableDetour when attempting to enter the character select screen. Occasionally, I can get in without any problems, but it feels like for every time it doesn't crash, there are 5 times where it does. I've narrowed it down to most likely being specifically a Penumbra issue, as I was able to get in fairly consistently with just Glamourer, but beyond that I'm stumped.
This is a really peculiar bug:
- the
AsmHookon one of the PapStackAccess patches (LoadAlwaysResidentMotionPacks) targets a 7-byte instruction (the4C 8D 05 80 14 E0 01, in 2025.04.16.0000.0000) - this should be fine, since AsmHook will never generate a jump over 7 bytes!
- except.. on Wine,
Reloaded.Memory.Buffersreturns a target buffer at e.g.0xFFFF0A24instead of0x7FFF0000on Windows - fasm will therefore generate a
0x6732-bit addressing mode prefix to zero-extend the address - and the output sequence (
67 FF 24 25 24 0A FF FF) is 8 bytes, whileReloaded.Hooksonly saved a 7-byte sequence of the originallea r8, .. - so once this code is hit, it'll return to the last
FFfrom the patch, and chaos ensues...
I managed to locally fix this by replacing two instances of UInt32.MaxValue with Int32.MaxValue in goatcorp.Reloaded.Hooks, though I'm not sure if that's the right way to go to solve this, or if this should have better logic in e.g. the Reloaded.Memory.Buffers fork or even further down the line.
Some hopefully-relevant context: The definition of FindOrCreateBufferInRange defaults to int.MaxValue:
public static MemoryBuffer FindOrCreateBufferInRange(int size, nuint minimumAddress = 1, nuint maximumAddress = int.MaxValue, int alignment = 4)
Two upstream Reloaded changes switched these callsites to passing uint.MaxValue nearly three years ago: https://github.com/goatcorp/goatcorp.Reloaded.Hooks/commit/57ce273c78e03f907c2e7ee7291096d017e45694 "Expand GetAbsoluteJumpMnemonics to 4GB Address Range" and https://github.com/goatcorp/goatcorp.Reloaded.Hooks/commit/f880d0eb056b678150e3f2d07c026ae61c33912a "Expand: GetAbsoluteCallMnemonics to 4GB Address Range"
I presume these upstream changes were made to reduce failure rates in 32-bit applications, but sadly there doesn't seem to be any annotation as to the underlying reason in the commit message there.
Good call on the default value, though.
Around 3 years ago, I was trying to migrate the library to be tolerant of the Large Address Aware flag in Windows, i.e. making it work in the upper 2GB of a 32-bit process. I'm guessing that based on the above, that caused a regression somewhere in a 64-bit code path. I was not aware of the sign extension problem.
My main concern at the time was people throwing massive texture packs at 32-bit games, and running out of address space; since in practice there's only ~1.6GB usable address space, once you factor in memory fragmentation, etc. Externally injected code further shrinks our available memory beyond that.
The way I tested at the time was by setting the Large Address Aware flag on the executable that provides the test runner (dotnet.exe is Large Address Aware, but the binaries used for tests, e.g. JetBrains' Test Runner might not be!!) and then running the LAA tests (disabled by default, opt in)
In any case, chances are the Rust rewrite Reloaded.Hooks-rs, doesn't suffer from this issue, as I wrote the JIT assembler there from scratch (for optimal performance & code size). [There's also advanced tests there, including wrapping around address space boundaries, etc.]
However, despite being ~80% done, that's been on the backburner for around 15 months now. I imagine I'll get back to it at the literal end of the year. I was originally hoping Q3, but with texture compression shenanigans and an archive format taking way, way longer to complete, that doesn't seem quite feasible anymore.