Add live GPS momentum weights

This commit is contained in:
2026-06-27 03:22:39 -05:00
parent d288b9fa6b
commit 186fc303e5
9 changed files with 224 additions and 8 deletions
+26
View File
@@ -2,6 +2,32 @@
Current date/time context: 2026-06-27, local timezone America/Chicago. Current date/time context: 2026-06-27, local timezone America/Chicago.
## 2026-06-27 Live Momentum Knob Installed
- Installed a new telemetry/control DLL at about `03:21` local while the game was
cold. Installed size: `16899519` bytes.
- Active installed config:
`solver_weights.bin = 1.0` and `momentum_weights.bin = 8.0`.
- The plugin log beside the DLL was truncated to zero after install:
`red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.log`.
- `momentum_weights.bin` is one little-endian `float32`, hot-polled every
500 ms. `0.0` disables the fixed ordinary-edge penalty; `8.0` is the current
prototype; larger values are stress tests. Presets are in
`presets/momentum/` and can be written live with
`python3 tools/write_momentum_weights.py <preset>`.
- New presets:
`vanilla=0.0`, `mild=4.0`, `default=8.0`, `strong=16.0`,
`silly=80.0`, `pathological=250.0`.
- The `gps-momentum-summary` line now waits for a nonzero result route-record
count before marking a query as summarized. It should report
`gpsResultRouteCount` directly instead of showing `0` from the staging fetch.
- Build verification passed:
`toolbox run -c 2077 ./tools/build_red4ext_shim.sh`.
- Next test should start with current `momentum=8.0`, then while the game is
open switch to `vanilla` and `silly` on one or two fixed routes to confirm
live A/B behavior. Good candidates from the last run: The Damned/Dogtown
pathological route and the SW Badlands fuel station route.
## 2026-06-27 Momentum Telemetry Test Installed ## 2026-06-27 Momentum Telemetry Test Installed
- Installed telemetry DLL at about `03:04` local while the game was cold. - Installed telemetry DLL at about `03:04` local while the game was cold.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+138 -8
View File
@@ -224,7 +224,8 @@ constexpr float kGpsSolverPavementMultiplier = 1.25f;
constexpr size_t kGpsNodeMultiplierRoadTailPatchSize = 14; constexpr size_t kGpsNodeMultiplierRoadTailPatchSize = 14;
constexpr size_t kGpsMomentumRelaxPatchSize = 23; constexpr size_t kGpsMomentumRelaxPatchSize = 23;
constexpr size_t kGpsMomentumPenaltyCaveSize = 96; constexpr size_t kGpsMomentumPenaltyCaveSize = 96;
constexpr float kGpsMomentumFixedEdgePenalty = 8.0f; constexpr float kGpsMomentumFixedEdgePenaltyDefault = 8.0f;
constexpr float kGpsMomentumFixedEdgePenaltyMax = 1000.0f;
constexpr std::array<float, 8> kGpsSolverNonRoadMultipliers = { constexpr std::array<float, 8> kGpsSolverNonRoadMultipliers = {
10.0f, 6.0f, 2.0f, 0.0f, 4.0f, 2.5f, 1.5f, 0.0f, 10.0f, 6.0f, 2.0f, 0.0f, 4.0f, 2.5f, 1.5f, 0.0f,
}; };
@@ -236,6 +237,7 @@ constexpr float kGpsSpatialPavementMultiplier = 1.35f;
constexpr float kGpsSpatialUnknownMultiplier = 1.05f; constexpr float kGpsSpatialUnknownMultiplier = 1.05f;
constexpr uint64_t kGpsSolverWeightsReloadIntervalMs = 500; constexpr uint64_t kGpsSolverWeightsReloadIntervalMs = 500;
constexpr uint64_t kGpsSpatialWeightsReloadIntervalMs = 500; constexpr uint64_t kGpsSpatialWeightsReloadIntervalMs = 500;
constexpr uint64_t kGpsMomentumWeightsReloadIntervalMs = 500;
struct GpsProviderClassOverride struct GpsProviderClassOverride
{ {
@@ -601,13 +603,18 @@ std::array<std::atomic<float>, 5> gGpsSpatialMultipliers = {
kGpsSpatialUnknownMultiplier, kGpsSpatialUnknownMultiplier,
}; };
std::atomic<float> gGpsSolverHighwayMultiplier{kGpsSolverHighwayMultiplierDefault}; std::atomic<float> gGpsSolverHighwayMultiplier{kGpsSolverHighwayMultiplierDefault};
std::atomic<float> gGpsMomentumFixedEdgePenalty{kGpsMomentumFixedEdgePenaltyDefault};
std::mutex gGpsSpatialWeightsMutex; std::mutex gGpsSpatialWeightsMutex;
std::mutex gGpsMomentumWeightsMutex;
std::atomic<uint64_t> gGpsSolverWeightsNextReloadCheckMs = 0; std::atomic<uint64_t> gGpsSolverWeightsNextReloadCheckMs = 0;
std::atomic<uint64_t> gGpsSpatialWeightsNextReloadCheckMs = 0; std::atomic<uint64_t> gGpsSpatialWeightsNextReloadCheckMs = 0;
std::atomic<uint64_t> gGpsMomentumWeightsNextReloadCheckMs = 0;
std::filesystem::file_time_type gGpsSolverWeightsLastWriteTime{}; std::filesystem::file_time_type gGpsSolverWeightsLastWriteTime{};
std::filesystem::file_time_type gGpsSpatialWeightsLastWriteTime{}; std::filesystem::file_time_type gGpsSpatialWeightsLastWriteTime{};
std::filesystem::file_time_type gGpsMomentumWeightsLastWriteTime{};
bool gGpsSolverWeightsFileKnown = false; bool gGpsSolverWeightsFileKnown = false;
bool gGpsSpatialWeightsFileKnown = false; bool gGpsSpatialWeightsFileKnown = false;
bool gGpsMomentumWeightsFileKnown = false;
uintptr_t GetImageBase(); uintptr_t GetImageBase();
@@ -638,6 +645,11 @@ std::filesystem::path GetSolverWeightsPath()
return GetPluginDirectory() / L"solver_weights.bin"; return GetPluginDirectory() / L"solver_weights.bin";
} }
std::filesystem::path GetMomentumWeightsPath()
{
return GetPluginDirectory() / L"momentum_weights.bin";
}
uint64_t GetElapsedMs() uint64_t GetElapsedMs()
{ {
if (!gLogStartTick || !gLogFrequency) if (!gLogStartTick || !gLogFrequency)
@@ -715,6 +727,34 @@ void LogGpsSolverHighwayMultiplier(std::string_view aPrefix, float aValue)
LogRed4ext(line.str()); LogRed4ext(line.str());
} }
bool IsUsableMomentumPenalty(float aValue)
{
return std::isfinite(aValue) && aValue >= 0.0f && aValue <= kGpsMomentumFixedEdgePenaltyMax;
}
void StoreGpsMomentumFixedEdgePenalty(float aValue)
{
gGpsMomentumFixedEdgePenalty.store(aValue, std::memory_order_relaxed);
for (auto& site : gGpsMomentumPenaltyPatchSites)
{
if (!site.penaltyConstant)
{
continue;
}
WriteFloat(site.penaltyConstant, aValue);
FlushInstructionCache(GetCurrentProcess(), site.penaltyConstant, sizeof(aValue));
}
}
void LogGpsMomentumFixedEdgePenalty(std::string_view aPrefix, float aValue)
{
std::ostringstream line;
line << aPrefix << " fixedEdgePenalty=" << aValue;
LogRed4ext(line.str());
}
void EmitAbsoluteRaxCall(uint8_t* aCave, size_t& aOffset, uintptr_t aTarget) void EmitAbsoluteRaxCall(uint8_t* aCave, size_t& aOffset, uintptr_t aTarget)
{ {
aCave[aOffset++] = 0x48; // mov rax, imm64 aCave[aOffset++] = 0x48; // mov rax, imm64
@@ -838,6 +878,82 @@ void MaybeReloadGpsSolverWeights(bool aForce = false)
LogRed4ext(line.str()); LogRed4ext(line.str());
} }
void MaybeReloadGpsMomentumWeights(bool aForce = false)
{
if (!kEnableGpsMomentumPenaltyInlinePatch)
{
return;
}
const auto nowMs = GetElapsedMs();
if (!aForce && nowMs < gGpsMomentumWeightsNextReloadCheckMs.load(std::memory_order_relaxed))
{
return;
}
std::lock_guard lock(gGpsMomentumWeightsMutex);
if (!aForce && nowMs < gGpsMomentumWeightsNextReloadCheckMs.load(std::memory_order_relaxed))
{
return;
}
gGpsMomentumWeightsNextReloadCheckMs.store(nowMs + kGpsMomentumWeightsReloadIntervalMs,
std::memory_order_relaxed);
const auto path = GetMomentumWeightsPath();
std::error_code error;
const auto writeTime = std::filesystem::last_write_time(path, error);
if (error)
{
if (aForce || gGpsMomentumWeightsFileKnown)
{
gGpsMomentumWeightsFileKnown = false;
std::ostringstream line;
line << "momentum weights file unavailable path=" << path.string()
<< " using fixedEdgePenalty=" << gGpsMomentumFixedEdgePenalty.load(std::memory_order_relaxed);
LogRed4ext(line.str());
}
return;
}
if (!aForce && gGpsMomentumWeightsFileKnown && writeTime == gGpsMomentumWeightsLastWriteTime)
{
return;
}
float fixedEdgePenalty = 0.0f;
std::ifstream input(path, std::ios::binary);
input.read(reinterpret_cast<char*>(&fixedEdgePenalty), static_cast<std::streamsize>(sizeof(fixedEdgePenalty)));
if (input.gcount() != static_cast<std::streamsize>(sizeof(fixedEdgePenalty)))
{
std::ostringstream line;
line << "momentum weights reload skipped path=" << path.string() << " bytes=" << input.gcount()
<< " expected=" << sizeof(fixedEdgePenalty);
LogRed4ext(line.str());
gGpsMomentumWeightsFileKnown = true;
gGpsMomentumWeightsLastWriteTime = writeTime;
return;
}
if (!IsUsableMomentumPenalty(fixedEdgePenalty))
{
std::ostringstream line;
line << "momentum weights reload skipped path=" << path.string()
<< " invalidFixedEdgePenalty=" << fixedEdgePenalty;
LogRed4ext(line.str());
gGpsMomentumWeightsFileKnown = true;
gGpsMomentumWeightsLastWriteTime = writeTime;
return;
}
StoreGpsMomentumFixedEdgePenalty(fixedEdgePenalty);
gGpsMomentumWeightsFileKnown = true;
gGpsMomentumWeightsLastWriteTime = writeTime;
std::ostringstream line;
line << "momentum weights reloaded path=" << path.string() << " fixedEdgePenalty=" << fixedEdgePenalty;
LogRed4ext(line.str());
}
void RemoveGpsMomentumPenaltyInlinePatch(); void RemoveGpsMomentumPenaltyInlinePatch();
bool InstallGpsMomentumPenaltyPatchSite(GpsMomentumPenaltyPatchSite& aSite, bool InstallGpsMomentumPenaltyPatchSite(GpsMomentumPenaltyPatchSite& aSite,
@@ -889,7 +1005,7 @@ bool InstallGpsMomentumPenaltyPatchSite(GpsMomentumPenaltyPatchSite& aSite,
} }
const auto penaltyConstantOffset = offset; const auto penaltyConstantOffset = offset;
WriteFloat(cave + offset, kGpsMomentumFixedEdgePenalty); WriteFloat(cave + offset, gGpsMomentumFixedEdgePenalty.load(std::memory_order_relaxed));
offset += sizeof(float); offset += sizeof(float);
const auto caveAddress = reinterpret_cast<uintptr_t>(cave); const auto caveAddress = reinterpret_cast<uintptr_t>(cave);
@@ -925,7 +1041,7 @@ bool InstallGpsMomentumPenaltyPatchSite(GpsMomentumPenaltyPatchSite& aSite,
std::ostringstream line; std::ostringstream line;
line << "gps-momentum-penalty-inline-patch applied site=" << aSite.name << " target_rva=0x" << std::hex line << "gps-momentum-penalty-inline-patch applied site=" << aSite.name << " target_rva=0x" << std::hex
<< aSite.targetRva << " cave=" << static_cast<void*>(cave) << std::dec << aSite.targetRva << " cave=" << static_cast<void*>(cave) << std::dec
<< " fixedEdgePenalty=" << kGpsMomentumFixedEdgePenalty; << " fixedEdgePenalty=" << gGpsMomentumFixedEdgePenalty.load(std::memory_order_relaxed);
LogRed4ext(line.str()); LogRed4ext(line.str());
return true; return true;
} }
@@ -2604,6 +2720,14 @@ void AppendGpsResultObjectLightCounts(std::ostringstream& aLine, const char* aPr
aLine << " " << aPrefix << "_pointCount=" << pointCount; aLine << " " << aPrefix << "_pointCount=" << pointCount;
} }
bool TryReadGpsResultRouteCount(void* aResultObject, uint32_t& aRouteCount)
{
aRouteCount = 0;
return aResultObject &&
TryReadMemory(reinterpret_cast<const void*>(reinterpret_cast<uintptr_t>(aResultObject) + 0x34),
aRouteCount);
}
void AppendGpsResultElementArraySummary(std::ostringstream& aLine, const char* aPrefix, void* aResultObject) void AppendGpsResultElementArraySummary(std::ostringstream& aLine, const char* aPrefix, void* aResultObject)
{ {
uint32_t capacity = 0; uint32_t capacity = 0;
@@ -4555,7 +4679,9 @@ bool DetourGpsQueryResultFetch(void* aManager, uint32_t aQueryId, void* aOutPath
(!isTracked && callCount < kMaxUntrackedSamples && aQueryId < 2)) (!isTracked && callCount < kMaxUntrackedSamples && aQueryId < 2))
: (result && isMapRouteQuery); : (result && isMapRouteQuery);
if (result && isMapRouteQuery) uint32_t gpsResultRouteCount = 0;
const auto hasGpsResultRouteCount = TryReadGpsResultRouteCount(aOutPath, gpsResultRouteCount);
if (result && isMapRouteQuery && hasGpsResultRouteCount && gpsResultRouteCount > 0)
{ {
TrackedGpsQuery summaryQuery{}; TrackedGpsQuery summaryQuery{};
uint64_t callsA = 0; uint64_t callsA = 0;
@@ -4566,14 +4692,15 @@ bool DetourGpsQueryResultFetch(void* aManager, uint32_t aQueryId, void* aOutPath
globalCallsB)) globalCallsB))
{ {
const auto totalCalls = callsA + callsB; const auto totalCalls = callsA + callsB;
const auto fixedPenalty = kGpsMomentumFixedEdgePenalty; const auto fixedPenalty = gGpsMomentumFixedEdgePenalty.load(std::memory_order_relaxed);
std::ostringstream line; std::ostringstream line;
line << "gps-momentum-summary queryId=" << aQueryId << " resultFetchCall=" << perQueryCall; line << "gps-momentum-summary queryId=" << aQueryId << " resultFetchCall=" << perQueryCall;
AppendTrackedGpsQuery(line, summaryQuery); AppendTrackedGpsQuery(line, summaryQuery);
line << " callsA=" << callsA << " callsB=" << callsB << " callsTotal=" << totalCalls line << " callsA=" << callsA << " callsB=" << callsB << " callsTotal=" << totalCalls
<< " fixedPenalty=" << fixedPenalty << " fixedPenalty=" << fixedPenalty
<< " penaltyTotal=" << static_cast<double>(totalCalls) * static_cast<double>(fixedPenalty) << " penaltyTotal=" << static_cast<double>(totalCalls) * static_cast<double>(fixedPenalty)
<< " globalCallsA=" << globalCallsA << " globalCallsB=" << globalCallsB; << " globalCallsA=" << globalCallsA << " globalCallsB=" << globalCallsB
<< " gpsResultRouteCount=" << gpsResultRouteCount;
AppendGpsResultObjectLightCounts(line, "gpsResult", aOutPath); AppendGpsResultObjectLightCounts(line, "gpsResult", aOutPath);
LogRed4ext(line.str()); LogRed4ext(line.str());
} }
@@ -5787,6 +5914,7 @@ bool OnRunningUpdate(void* aApp)
} }
MaybeReloadGpsSolverWeights(); MaybeReloadGpsSolverWeights();
MaybeReloadGpsMomentumWeights();
return false; return false;
} }
@@ -5869,9 +5997,11 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
if (kEnableGpsMomentumPenaltyInlinePatch) if (kEnableGpsMomentumPenaltyInlinePatch)
{ {
std::ostringstream line; std::ostringstream line;
line << "momentum fixed-edge penalty active value=" << kGpsMomentumFixedEdgePenalty line << "momentum weights path=" << GetMomentumWeightsPath().string();
<< " sites=ordinary-expansion";
LogRed4ext(line.str()); LogRed4ext(line.str());
MaybeReloadGpsMomentumWeights(true);
LogGpsMomentumFixedEdgePenalty("momentum fixed-edge penalty active",
gGpsMomentumFixedEdgePenalty.load(std::memory_order_relaxed));
} }
InstallGpsNodeMultiplierInlinePatch(); InstallGpsNodeMultiplierInlinePatch();
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Write EdgeWeightGPS momentum penalty presets as one raw float32 value."""
from __future__ import annotations
import argparse
import struct
from pathlib import Path
DEFAULT_INSTALL_PATH = Path(
"/var/home/salt/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/"
"Cyberpunk 2077/red4ext/plugins/EdgeWeightGPS/momentum_weights.bin"
)
PRESETS: dict[str, float] = {
"vanilla": 0.0,
"default": 8.0,
"mild": 4.0,
"strong": 16.0,
"silly": 80.0,
"pathological": 250.0,
}
def parse_penalty(text: str) -> float:
value = float(text)
if not 0.0 <= value <= 1000.0:
raise argparse.ArgumentTypeError("penalty must be between 0 and 1000")
return value
def write_weight(path: Path, value: float) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(struct.pack("<f", value))
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("preset", nargs="?", choices=sorted(PRESETS), default="default")
parser.add_argument("--value", type=parse_penalty, help="explicit fixed edge penalty")
parser.add_argument("--output", type=Path, default=DEFAULT_INSTALL_PATH)
parser.add_argument("--preset-dir", type=Path, help="write every named preset into this directory")
args = parser.parse_args()
if args.preset_dir:
for name, value in PRESETS.items():
path = args.preset_dir / f"{name}.bin"
write_weight(path, value)
print(f"{path}: {value}")
return 0
value = args.value if args.value is not None else PRESETS[args.preset]
write_weight(args.output, value)
print(f"{args.output}: {value}")
return 0
if __name__ == "__main__":
raise SystemExit(main())