From 3ac184a2decc0504c4747f01926d419cd6bffc48 Mon Sep 17 00:00:00 2001 From: Jacob Babor Date: Sat, 27 Jun 2026 05:04:13 -0500 Subject: [PATCH] Document native GPS patch trampoline --- red4ext/EdgeWeightGPS/src/Main.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/red4ext/EdgeWeightGPS/src/Main.cpp b/red4ext/EdgeWeightGPS/src/Main.cpp index 7381b27..67e646c 100644 --- a/red4ext/EdgeWeightGPS/src/Main.cpp +++ b/red4ext/EdgeWeightGPS/src/Main.cpp @@ -15,6 +15,9 @@ #include #include +// The public SDK headers used by this MinGW build provide the exported plugin +// types, but not every callback table layout consumed here. Keep these minimal +// ABI mirrors in field order and only use the members this plugin touches. namespace RED4ext::v1 { struct Logger @@ -82,9 +85,15 @@ struct Sdk namespace { +// Executable RVAs in the tested Cyberpunk 2077 build. The two expansion sites +// are ordinary road-neighbor relax callsites; both feed the native relax/update +// helper that commits a candidate route node when the new score wins. constexpr uintptr_t kGpsRelaxUpdateRva = 0x040BA58; constexpr uintptr_t kGpsExpandListARelaxPatchRva = 0x040B49A; constexpr uintptr_t kGpsExpandListBRelaxPatchRva = 0x040B6C3; +// Size of the overwritten instruction window at each expansion site. The +// trampoline must return after these whole instructions, never into the middle +// of native code. constexpr size_t kRelaxPatchSize = 23; constexpr size_t kPatchCaveSize = 80; constexpr float kFixedEdgePenalty = 80.0f; @@ -104,6 +113,9 @@ RED4ext::v1::PluginHandle gHandle = nullptr; const RED4ext::v1::Sdk* gSdk = nullptr; std::array gPatchSites{{ + // The two sites share the same relax/update tail but arrive with the + // current node cost in different XMM registers. Each expected instruction + // is replayed first so the cave preserves vanilla's edge accumulation. { "expand-list-a", kGpsExpandListARelaxPatchRva, @@ -238,6 +250,13 @@ bool InstallPatchSite(PatchSite& aSite) cave[offset++] = byte; } + // Register contract for the native relax/update call: + // - xmm6 holds this edge's accumulated g-score. + // - xmm0 is the candidate f-score passed through [rsp+0x20]. + // - xmm3 mirrors xmm6 for the helper call. + // The cave replays vanilla's first add into xmm6, adds our fixed segment + // penalty, rebuilds the helper arguments, then jumps back after the + // overwritten instruction window. const auto addPenaltyOffset = offset; EmitBytes(cave, offset, {0xF3, 0x0F, 0x58, 0x35, 0x00, 0x00, 0x00, 0x00}); // addss xmm6, [rip+disp32] EmitBytes(cave, offset, {0xF3, 0x0F, 0x58, 0xC6}); // addss xmm0, xmm6 @@ -246,6 +265,8 @@ bool InstallPatchSite(PatchSite& aSite) EmitAbsoluteRaxCall(cave, offset, GameImageBase() + kGpsRelaxUpdateRva); EmitAbsoluteRaxJump(cave, offset, GameImageBase() + aSite.targetRva + kRelaxPatchSize); + // Keep the embedded float naturally aligned. The RIP-relative addss above + // is patched after the final cave layout is known. while ((offset % alignof(float)) != 0) { cave[offset++] = 0x90; @@ -340,6 +361,8 @@ bool InstallPatchSites() if (!ok) { + // Route scoring should be all-or-nothing: one patched expansion site + // and one vanilla site would produce inconsistent route costs. RemovePatchSites(); }