Add live GPS momentum weights
This commit is contained in:
@@ -2,6 +2,32 @@
|
||||
|
||||
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
|
||||
|
||||
- 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.
@@ -224,7 +224,8 @@ 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 float kGpsMomentumFixedEdgePenaltyDefault = 8.0f;
|
||||
constexpr float kGpsMomentumFixedEdgePenaltyMax = 1000.0f;
|
||||
constexpr std::array<float, 8> kGpsSolverNonRoadMultipliers = {
|
||||
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 uint64_t kGpsSolverWeightsReloadIntervalMs = 500;
|
||||
constexpr uint64_t kGpsSpatialWeightsReloadIntervalMs = 500;
|
||||
constexpr uint64_t kGpsMomentumWeightsReloadIntervalMs = 500;
|
||||
|
||||
struct GpsProviderClassOverride
|
||||
{
|
||||
@@ -601,13 +603,18 @@ std::array<std::atomic<float>, 5> gGpsSpatialMultipliers = {
|
||||
kGpsSpatialUnknownMultiplier,
|
||||
};
|
||||
std::atomic<float> gGpsSolverHighwayMultiplier{kGpsSolverHighwayMultiplierDefault};
|
||||
std::atomic<float> gGpsMomentumFixedEdgePenalty{kGpsMomentumFixedEdgePenaltyDefault};
|
||||
std::mutex gGpsSpatialWeightsMutex;
|
||||
std::mutex gGpsMomentumWeightsMutex;
|
||||
std::atomic<uint64_t> gGpsSolverWeightsNextReloadCheckMs = 0;
|
||||
std::atomic<uint64_t> gGpsSpatialWeightsNextReloadCheckMs = 0;
|
||||
std::atomic<uint64_t> gGpsMomentumWeightsNextReloadCheckMs = 0;
|
||||
std::filesystem::file_time_type gGpsSolverWeightsLastWriteTime{};
|
||||
std::filesystem::file_time_type gGpsSpatialWeightsLastWriteTime{};
|
||||
std::filesystem::file_time_type gGpsMomentumWeightsLastWriteTime{};
|
||||
bool gGpsSolverWeightsFileKnown = false;
|
||||
bool gGpsSpatialWeightsFileKnown = false;
|
||||
bool gGpsMomentumWeightsFileKnown = false;
|
||||
|
||||
uintptr_t GetImageBase();
|
||||
|
||||
@@ -638,6 +645,11 @@ std::filesystem::path GetSolverWeightsPath()
|
||||
return GetPluginDirectory() / L"solver_weights.bin";
|
||||
}
|
||||
|
||||
std::filesystem::path GetMomentumWeightsPath()
|
||||
{
|
||||
return GetPluginDirectory() / L"momentum_weights.bin";
|
||||
}
|
||||
|
||||
uint64_t GetElapsedMs()
|
||||
{
|
||||
if (!gLogStartTick || !gLogFrequency)
|
||||
@@ -715,6 +727,34 @@ void LogGpsSolverHighwayMultiplier(std::string_view aPrefix, float aValue)
|
||||
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)
|
||||
{
|
||||
aCave[aOffset++] = 0x48; // mov rax, imm64
|
||||
@@ -838,6 +878,82 @@ void MaybeReloadGpsSolverWeights(bool aForce = false)
|
||||
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();
|
||||
|
||||
bool InstallGpsMomentumPenaltyPatchSite(GpsMomentumPenaltyPatchSite& aSite,
|
||||
@@ -889,7 +1005,7 @@ bool InstallGpsMomentumPenaltyPatchSite(GpsMomentumPenaltyPatchSite& aSite,
|
||||
}
|
||||
|
||||
const auto penaltyConstantOffset = offset;
|
||||
WriteFloat(cave + offset, kGpsMomentumFixedEdgePenalty);
|
||||
WriteFloat(cave + offset, gGpsMomentumFixedEdgePenalty.load(std::memory_order_relaxed));
|
||||
offset += sizeof(float);
|
||||
|
||||
const auto caveAddress = reinterpret_cast<uintptr_t>(cave);
|
||||
@@ -925,7 +1041,7 @@ bool InstallGpsMomentumPenaltyPatchSite(GpsMomentumPenaltyPatchSite& aSite,
|
||||
std::ostringstream line;
|
||||
line << "gps-momentum-penalty-inline-patch applied site=" << aSite.name << " target_rva=0x" << std::hex
|
||||
<< aSite.targetRva << " cave=" << static_cast<void*>(cave) << std::dec
|
||||
<< " fixedEdgePenalty=" << kGpsMomentumFixedEdgePenalty;
|
||||
<< " fixedEdgePenalty=" << gGpsMomentumFixedEdgePenalty.load(std::memory_order_relaxed);
|
||||
LogRed4ext(line.str());
|
||||
return true;
|
||||
}
|
||||
@@ -2604,6 +2720,14 @@ void AppendGpsResultObjectLightCounts(std::ostringstream& aLine, const char* aPr
|
||||
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)
|
||||
{
|
||||
uint32_t capacity = 0;
|
||||
@@ -4555,7 +4679,9 @@ bool DetourGpsQueryResultFetch(void* aManager, uint32_t aQueryId, void* aOutPath
|
||||
(!isTracked && callCount < kMaxUntrackedSamples && aQueryId < 2))
|
||||
: (result && isMapRouteQuery);
|
||||
|
||||
if (result && isMapRouteQuery)
|
||||
uint32_t gpsResultRouteCount = 0;
|
||||
const auto hasGpsResultRouteCount = TryReadGpsResultRouteCount(aOutPath, gpsResultRouteCount);
|
||||
if (result && isMapRouteQuery && hasGpsResultRouteCount && gpsResultRouteCount > 0)
|
||||
{
|
||||
TrackedGpsQuery summaryQuery{};
|
||||
uint64_t callsA = 0;
|
||||
@@ -4566,14 +4692,15 @@ bool DetourGpsQueryResultFetch(void* aManager, uint32_t aQueryId, void* aOutPath
|
||||
globalCallsB))
|
||||
{
|
||||
const auto totalCalls = callsA + callsB;
|
||||
const auto fixedPenalty = kGpsMomentumFixedEdgePenalty;
|
||||
const auto fixedPenalty = gGpsMomentumFixedEdgePenalty.load(std::memory_order_relaxed);
|
||||
std::ostringstream line;
|
||||
line << "gps-momentum-summary queryId=" << aQueryId << " resultFetchCall=" << perQueryCall;
|
||||
AppendTrackedGpsQuery(line, summaryQuery);
|
||||
line << " callsA=" << callsA << " callsB=" << callsB << " callsTotal=" << totalCalls
|
||||
<< " fixedPenalty=" << fixedPenalty
|
||||
<< " penaltyTotal=" << static_cast<double>(totalCalls) * static_cast<double>(fixedPenalty)
|
||||
<< " globalCallsA=" << globalCallsA << " globalCallsB=" << globalCallsB;
|
||||
<< " globalCallsA=" << globalCallsA << " globalCallsB=" << globalCallsB
|
||||
<< " gpsResultRouteCount=" << gpsResultRouteCount;
|
||||
AppendGpsResultObjectLightCounts(line, "gpsResult", aOutPath);
|
||||
LogRed4ext(line.str());
|
||||
}
|
||||
@@ -5787,6 +5914,7 @@ bool OnRunningUpdate(void* aApp)
|
||||
}
|
||||
|
||||
MaybeReloadGpsSolverWeights();
|
||||
MaybeReloadGpsMomentumWeights();
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -5869,9 +5997,11 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
|
||||
if (kEnableGpsMomentumPenaltyInlinePatch)
|
||||
{
|
||||
std::ostringstream line;
|
||||
line << "momentum fixed-edge penalty active value=" << kGpsMomentumFixedEdgePenalty
|
||||
<< " sites=ordinary-expansion";
|
||||
line << "momentum weights path=" << GetMomentumWeightsPath().string();
|
||||
LogRed4ext(line.str());
|
||||
MaybeReloadGpsMomentumWeights(true);
|
||||
LogGpsMomentumFixedEdgePenalty("momentum fixed-edge penalty active",
|
||||
gGpsMomentumFixedEdgePenalty.load(std::memory_order_relaxed));
|
||||
}
|
||||
|
||||
InstallGpsNodeMultiplierInlinePatch();
|
||||
|
||||
Executable
+60
@@ -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())
|
||||
Reference in New Issue
Block a user