From 12fb5d3b55c2609d7e9e4124245cf3eae8af82db Mon Sep 17 00:00:00 2001 From: Jacob Babor Date: Sat, 27 Jun 2026 02:42:21 -0500 Subject: [PATCH] Add disabled GPS momentum penalty prototype --- docs/compaction-handoff.md | 34 +++++ docs/gps-routing-research.md | 33 +++-- red4ext/EdgeWeightGPS/src/Main.cpp | 221 +++++++++++++++++++++++++++++ 3 files changed, 279 insertions(+), 9 deletions(-) diff --git a/docs/compaction-handoff.md b/docs/compaction-handoff.md index c1c9257..e180748 100644 --- a/docs/compaction-handoff.md +++ b/docs/compaction-handoff.md @@ -2,6 +2,40 @@ Current date/time context: 2026-06-27, local timezone America/Chicago. +## 2026-06-27 Disabled Momentum Penalty Prototype + +- Static disassembly of the full async solver relaxation cluster is updated. + `0x40ba58` has exactly four direct callers: + `0x40b175`, `0x40b4ac`, `0x40b6d5`, and `0x40bca7`. +- The ordinary adjacency walkers are still the clean first patch surface: + `0x40b304` reaches relax at `0x40b4ac`, and `0x40b540` reaches relax at + `0x40b6d5`. Both compute vanilla tentative `g` in `xmm6`, copy it to + `xmm3`, write `f = g + h` at `[rsp+0x20]`, and then call `0x40ba58`. +- Patch starts are `0x40b49a` for list A and `0x40b6c3` for list B. Each + replaces 23 bytes ending at the vanilla `0x40ba58` call and jumps to a + private executable cave. The cave reproduces the overwritten vanilla math, + adds a fixed nonnegative cost to `xmm6`, recomputes `f` from the penalized + `g`, calls vanilla `0x40ba58`, and jumps back after the overwritten block. +- Added a disabled-by-default RED4ext prototype in + `red4ext/EdgeWeightGPS/src/Main.cpp`: + `kEnableGpsMomentumPenaltyInlinePatch = false`, + `kGpsMomentumFixedEdgePenalty = 8.0f`. +- This first prototype is only a short-segment/churn lever. It adds a fixed + per-edge penalty on ordinary neighbor expansion, so routes that stair-step + through many small pieces become more expensive without making highways + cheaper. It does not yet compute turn angle, ramp transition cost, or + predecessor/current/neighbor geometry. +- The first prototype intentionally leaves the two non-ordinary relax paths + vanilla: + `0x40b175` appears to be a target/terminal candidate path, and `0x40bca7` + appears to be a special continuation/end-candidate path from `0x40bbbc`. + Patch them only after the ordinary expansion test proves stable. +- Build verification passed in toolbox: + `toolbox run -c 2077 ./tools/build_red4ext_shim.sh`. +- The prototype was not installed into the live game directory. To test later, + flip `kEnableGpsMomentumPenaltyInlinePatch` to `true`, rebuild, install only + after the game is cold, and compare against the fixed El Coyote route set. + ## 2026-06-27 Routing Literature And Engine Research - Added `docs/gps-routing-research.md` as the durable summary of the GPS diff --git a/docs/gps-routing-research.md b/docs/gps-routing-research.md index ea45e2f..aab31dd 100644 --- a/docs/gps-routing-research.md +++ b/docs/gps-routing-research.md @@ -403,18 +403,33 @@ Add tests targeted at the new model: ## Practical Next Step -Before coding the full patch, do one static pass to finish the field map around -`0x40b304`, `0x40b540`, and `0x40ba58`: +The static pass around `0x40b304`, `0x40b540`, and `0x40ba58` is complete +enough for the first prototype: + +- `0x40ba58` has four direct callers: `0x40b175`, `0x40b4ac`, `0x40b6d5`, + and `0x40bca7`. +- The ordinary adjacency walkers are the two clean first patch sites: + `0x40b4ac` from `0x40b304`, and `0x40b6d5` from `0x40b540`. +- At both ordinary sites, the live context contains solver, current state, + neighbor state, current node, neighbor node, current progress, neighbor + progress, vanilla `g`, and vanilla `f`. +- A disabled-by-default RED4ext prototype now patches the two ordinary sites by + adding a fixed nonnegative per-edge penalty before calling vanilla + `0x40ba58`. + +Remaining static work before the full momentum model: - Confirm candidate vector layout at `0x40b8f0`/state allocation. -- Confirm predecessor state pointer/index path from `state+0x1e`. - Confirm route-mode values at `solver+0xc4` for manual driving vs AutoDrive. - Identify a cheap way to detect road-class transition from current/neighbor node flags. -- Decide whether the first prototype patches only the two main relax callsites - (`0x40b4ac`, `0x40b6d5`) or also the special/helper relax paths - (`0x40b175`, `0x40bca7`). +- Decode predecessor geometry from `state+0x1e` well enough to compute + predecessor/current/neighbor turn angle. +- Decide whether the later turn/ramp model should also patch the special/helper + relax paths (`0x40b175`, `0x40bca7`) or keep them vanilla. -After that, implement the smallest live-tunable prototype with additive -penalties only. Keep highway multiplier support available as a diagnostic knob, -but do not use it as the primary default behavior. +Next implementation step is to enable and test the smallest additive prototype +with route-result comparisons. If the fixed per-edge penalty moves paths in the +right direction and remains stable, extend it into a live-tunable cost file and +then add turn/continuity penalties. Keep highway multiplier support available as +a diagnostic knob, but do not use it as the primary default behavior. diff --git a/red4ext/EdgeWeightGPS/src/Main.cpp b/red4ext/EdgeWeightGPS/src/Main.cpp index f89ee46..f9cd92f 100644 --- a/red4ext/EdgeWeightGPS/src/Main.cpp +++ b/red4ext/EdgeWeightGPS/src/Main.cpp @@ -103,6 +103,7 @@ constexpr bool kEnableGpsMultiplierHooks = false; constexpr bool kEnableGpsAuxMultiplierHook = false; constexpr bool kEnableGpsMultiplierSignatureTrace = false; constexpr bool kEnableGpsNodeMultiplierInlinePatch = true; +constexpr bool kEnableGpsMomentumPenaltyInlinePatch = false; constexpr bool kEnableGpsCostTablePatch = false; constexpr bool kEnableGpsProviderClassPatch = false; constexpr bool kEnableGpsProviderClassPatchTrace = false; @@ -197,6 +198,9 @@ constexpr uintptr_t kGpsFilteredCostProviderVtableRva = 0x2AE60F8; constexpr uintptr_t kGpsAuxMultiplierRva = 0x040BB00; constexpr uintptr_t kGpsNodeMultiplierRva = 0x040BB40; constexpr uintptr_t kGpsNodeMultiplierRoadTailRva = 0x040BB98; +constexpr uintptr_t kGpsRelaxUpdateRva = 0x040BA58; +constexpr uintptr_t kGpsExpandListARelaxPatchRva = 0x040B49A; +constexpr uintptr_t kGpsExpandListBRelaxPatchRva = 0x040B6C3; constexpr uintptr_t kGpsCostMultiplierTableRva = 0x3154D28; constexpr uint64_t kGpsResolveTraceWindowMs = 1200; constexpr uint32_t kGpsResolveSelectedRetLogLimit = 24; @@ -217,6 +221,9 @@ constexpr float kGpsSolverHighwayMultiplierDefault = 0.80f; constexpr float kGpsSolverGpsOnlyMultiplier = 1.10f; constexpr float kGpsSolverPavementMultiplier = 1.25f; constexpr size_t kGpsNodeMultiplierRoadTailPatchSize = 14; +constexpr size_t kGpsMomentumRelaxPatchSize = 23; +constexpr size_t kGpsMomentumPenaltyCaveSize = 96; +constexpr float kGpsMomentumFixedEdgePenalty = 8.0f; constexpr std::array kGpsSolverNonRoadMultipliers = { 10.0f, 6.0f, 2.0f, 0.0f, 4.0f, 2.5f, 1.5f, 0.0f, }; @@ -245,6 +252,16 @@ constexpr std::array kGpsProviderClassOverrides = { constexpr std::array kGpsProviderClassProbeIds = {1, 3, 4, 5, 14, 15}; +struct GpsMomentumPenaltyPatchSite +{ + const char* name; + uintptr_t targetRva; + std::array original{}; + void* cave = nullptr; + uint8_t* penaltyConstant = nullptr; + bool applied = false; +}; + HMODULE gModule = nullptr; std::mutex gLogMutex; RED4ext::v1::PluginHandle gHandle = nullptr; @@ -330,6 +347,10 @@ std::array gGpsNodeMultiplierRoadT void* gGpsNodeMultiplierRoadTailCave = nullptr; uint8_t* gGpsNodeMultiplierRoadTailHighwayConstant = nullptr; bool gGpsNodeMultiplierRoadTailPatchApplied = false; +std::array gGpsMomentumPenaltyPatchSites = {{ + {"expand-list-a", kGpsExpandListARelaxPatchRva}, + {"expand-list-b", kGpsExpandListBRelaxPatchRva}, +}}; std::mutex gGpsCostLogMutex; std::mutex gGpsAsyncLogMutex; std::mutex gGpsCostTablePatchMutex; @@ -687,6 +708,43 @@ void LogGpsSolverHighwayMultiplier(std::string_view aPrefix, float aValue) LogRed4ext(line.str()); } +void EmitAbsoluteRaxCall(uint8_t* aCave, size_t& aOffset, uintptr_t aTarget) +{ + aCave[aOffset++] = 0x48; // mov rax, imm64 + aCave[aOffset++] = 0xB8; + std::memcpy(aCave + aOffset, &aTarget, sizeof(aTarget)); + aOffset += sizeof(aTarget); + aCave[aOffset++] = 0xFF; // call rax + aCave[aOffset++] = 0xD0; +} + +void EmitAbsoluteRaxJump(uint8_t* aCave, size_t& aOffset, uintptr_t aTarget) +{ + aCave[aOffset++] = 0x48; // mov rax, imm64 + aCave[aOffset++] = 0xB8; + std::memcpy(aCave + aOffset, &aTarget, sizeof(aTarget)); + aOffset += sizeof(aTarget); + aCave[aOffset++] = 0xFF; // jmp rax + aCave[aOffset++] = 0xE0; +} + +void BuildAbsoluteJumpPatch(uint8_t* aPatch, size_t aPatchSize, uintptr_t aTarget) +{ + size_t offset = 0; + aPatch[offset++] = 0xFF; // jmp qword ptr [rip+0] + aPatch[offset++] = 0x25; + aPatch[offset++] = 0x00; + aPatch[offset++] = 0x00; + aPatch[offset++] = 0x00; + aPatch[offset++] = 0x00; + std::memcpy(aPatch + offset, &aTarget, sizeof(aTarget)); + offset += sizeof(aTarget); + while (offset < aPatchSize) + { + aPatch[offset++] = 0x90; + } +} + void MaybeReloadGpsSolverWeights(bool aForce = false) { if (!kEnableGpsNodeMultiplierInlinePatch) @@ -761,6 +819,159 @@ void MaybeReloadGpsSolverWeights(bool aForce = false) LogRed4ext(line.str()); } +void RemoveGpsMomentumPenaltyInlinePatch(); + +bool InstallGpsMomentumPenaltyPatchSite(GpsMomentumPenaltyPatchSite& aSite, + std::initializer_list aAddCurrentCostInstruction) +{ + if (aSite.applied) + { + return true; + } + + auto* cave = static_cast( + VirtualAlloc(nullptr, kGpsMomentumPenaltyCaveSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); + if (!cave) + { + std::ostringstream line; + line << "gps-momentum-penalty-inline-patch skipped site=" << aSite.name << " reason=alloc-failed"; + LogRed4ext(line.str()); + return false; + } + + size_t offset = 0; + const auto emit = [&](std::initializer_list aBytes) { + for (auto byte : aBytes) + { + cave[offset++] = byte; + } + }; + + // Replaces the final g/f assembly before 0x40ba58. The overwritten + // vanilla block differs only in which XMM register stores current.g. + emit(aAddCurrentCostInstruction); + + const auto addPenaltyOffset = offset; + emit({0xF3, 0x0F, 0x58, 0x35, 0x00, 0x00, 0x00, 0x00}); // addss xmm6, [rip+disp32] + + emit({0xF3, 0x0F, 0x58, 0xC6}); // addss xmm0, xmm6 + emit({0x0F, 0x28, 0xDE}); // movaps xmm3, xmm6 + emit({0xF3, 0x0F, 0x11, 0x44, 0x24, 0x20}); // movss [rsp+0x20], xmm0 + EmitAbsoluteRaxCall(cave, offset, GetImageBase() + kGpsRelaxUpdateRva); + EmitAbsoluteRaxJump(cave, offset, GetImageBase() + aSite.targetRva + kGpsMomentumRelaxPatchSize); + + while ((offset % alignof(float)) != 0) + { + cave[offset++] = 0x90; + } + + const auto penaltyConstantOffset = offset; + WriteFloat(cave + offset, kGpsMomentumFixedEdgePenalty); + offset += sizeof(float); + + const auto caveAddress = reinterpret_cast(cave); + WriteInt32(cave + addPenaltyOffset + 4, + static_cast((caveAddress + penaltyConstantOffset) - (caveAddress + addPenaltyOffset + 8))); + + auto* target = reinterpret_cast(GetImageBase() + aSite.targetRva); + std::memcpy(aSite.original.data(), target, aSite.original.size()); + + std::array patch{}; + BuildAbsoluteJumpPatch(patch.data(), patch.size(), caveAddress); + + DWORD oldProtect = 0; + if (!VirtualProtect(target, patch.size(), PAGE_EXECUTE_READWRITE, &oldProtect)) + { + VirtualFree(cave, 0, MEM_RELEASE); + std::ostringstream line; + line << "gps-momentum-penalty-inline-patch skipped site=" << aSite.name << " reason=protect-failed"; + LogRed4ext(line.str()); + return false; + } + + std::memcpy(target, patch.data(), patch.size()); + FlushInstructionCache(GetCurrentProcess(), target, patch.size()); + + DWORD ignoredProtect = 0; + VirtualProtect(target, patch.size(), oldProtect, &ignoredProtect); + + aSite.cave = cave; + aSite.penaltyConstant = cave + penaltyConstantOffset; + aSite.applied = true; + + std::ostringstream line; + line << "gps-momentum-penalty-inline-patch applied site=" << aSite.name << " target_rva=0x" << std::hex + << aSite.targetRva << " cave=" << static_cast(cave) << std::dec + << " fixedEdgePenalty=" << kGpsMomentumFixedEdgePenalty; + LogRed4ext(line.str()); + return true; +} + +bool InstallGpsMomentumPenaltyInlinePatch() +{ + if (!kEnableGpsMomentumPenaltyInlinePatch) + { + return true; + } + + const bool firstOk = + InstallGpsMomentumPenaltyPatchSite(gGpsMomentumPenaltyPatchSites[0], + {0xF3, 0x41, 0x0F, 0x58, 0xF6}); // addss xmm6, xmm14 + const bool secondOk = + InstallGpsMomentumPenaltyPatchSite(gGpsMomentumPenaltyPatchSites[1], + {0xF3, 0x41, 0x0F, 0x58, 0xF4}); // addss xmm6, xmm12 + + if (!firstOk || !secondOk) + { + RemoveGpsMomentumPenaltyInlinePatch(); + return false; + } + + return true; +} + +void RemoveGpsMomentumPenaltyInlinePatch() +{ + for (auto& site : gGpsMomentumPenaltyPatchSites) + { + if (!site.applied) + { + continue; + } + + auto* target = reinterpret_cast(GetImageBase() + site.targetRva); + DWORD oldProtect = 0; + if (VirtualProtect(target, site.original.size(), PAGE_EXECUTE_READWRITE, &oldProtect)) + { + std::memcpy(target, site.original.data(), site.original.size()); + FlushInstructionCache(GetCurrentProcess(), target, site.original.size()); + + DWORD ignoredProtect = 0; + VirtualProtect(target, site.original.size(), oldProtect, &ignoredProtect); + + std::ostringstream line; + line << "gps-momentum-penalty-inline-patch restored site=" << site.name; + LogRed4ext(line.str()); + } + else + { + std::ostringstream line; + line << "gps-momentum-penalty-inline-patch restore-failed site=" << site.name + << " reason=protect-failed"; + LogRed4ext(line.str()); + } + + if (site.cave) + { + VirtualFree(site.cave, 0, MEM_RELEASE); + site.cave = nullptr; + } + + site.penaltyConstant = nullptr; + site.applied = false; + } +} + bool InstallGpsNodeMultiplierInlinePatch() { if (!kEnableGpsNodeMultiplierInlinePatch || gGpsNodeMultiplierRoadTailPatchApplied) @@ -5550,7 +5761,16 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e gGpsSolverHighwayMultiplier.load(std::memory_order_relaxed)); } + if (kEnableGpsMomentumPenaltyInlinePatch) + { + std::ostringstream line; + line << "momentum fixed-edge penalty active value=" << kGpsMomentumFixedEdgePenalty + << " sites=ordinary-expansion"; + LogRed4ext(line.str()); + } + InstallGpsNodeMultiplierInlinePatch(); + InstallGpsMomentumPenaltyInlinePatch(); if (kEnableGpsQueryLifecycleHooks) { @@ -5781,6 +6001,7 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e if (aReason == RED4ext::v1::EMainReason::Unload) { RemoveGpsNodeMultiplierInlinePatch(); + RemoveGpsMomentumPenaltyInlinePatch(); if (kEnableGpsQueryLifecycleHooks) {