diff --git a/README.md b/README.md index af5e665..7a5768b 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ Nothing is replaced, the mod's files simply sit next to the game's own. **What you need** -- F.E.A.R. **1.08**, legally installed. Only the Steam Ultimate Shooter - Edition is confirmed in-game; GOG and retail-disc copies of 1.08 install and - launch, but are untested — see [Game editions](#game-editions). Versions +- F.E.A.R. **1.08**, legally installed. The Steam Ultimate Shooter Edition and + the March 2025 GOG Preservation build are confirmed in-game; retail-disc and + other 1.08 revisions remain untested — see [Game editions](#game-editions). Versions below 1.08 are rejected, because the Public Tools modules do not match them. - The public download needs the official **Public Tools 1.08** installed locally — bundled with the Steam Ultimate Shooter Edition under `extras\`, or @@ -178,16 +178,24 @@ on disk. What differs between editions is how the game starts: |---|---|---| | Steam (Ultimate Shooter Edition 1.08) | `steam.exe -applaunch 21090` | Confirmed in-game | | Steam 1.08 + HDTextures4FEAR/XP v2.0.2 | `steam.exe -applaunch 21090` | Recognized; exact patched EXE hash confirmed | -| GOG (1.08) | `FEAR.exe` directly, same arguments | Should work, untested | +| GOG Preservation Program (1.08, 2025-03-20) | `FEAR.exe` directly, same arguments | Confirmed in-game; exact EXE/HID signatures and stable sustained play verified | | Retail disc, patched to 1.08 | `FEAR.exe` directly, same arguments | Should work, untested | +Steam CEG initially exposes encrypted bytes at the two HID patch regions, so +the earliest `dinput8.dll` check cannot match. The GameClient loader repeats +the same guarded check after CEG has unpacked the image but before DirectInput +device initialization; the `DirectInput8Create` proxy performs one final +guarded verification. A successful Steam run can therefore report +`fear_hid_fix result=already_applied`, while GOG normally reports `applied`. + The mod picks the launch mode itself: a copy under `steamapps\common` goes through Steam, anything else is started directly. An unknown `FEAR.exe` build is no longer an error. The mod records its SHA-256 with a warning and continues; on a byte mismatch the HID patch remains -off rather than touching unknown code. If you run a GOG or disc copy, that hash -plus the `fear_hid_fix` result from the log is what is needed to confirm it. +off rather than touching unknown code. If you run another GOG revision or a +disc copy, that hash plus the `fear_hid_fix` result from the log is what is +needed to confirm it. The HDTextures4FEAR/XP v2.0.2 installer replaces the Steam executable. Its patched hash and the original Steam hash are both recognized. Installing or @@ -199,7 +207,7 @@ require reinstalling the VR mod; unknown executable changes are still rejected. | Message | Cause and fix | |---|---| | `Wrong FEAR.exe version` | Not patched to 1.08, or `-RetailRoot` points at a different installation. | -| `This FEAR.exe build has not been tested` | A 1.08 build other than Steam's (GOG, disc). Installation continues; please report whether it works. | +| `This FEAR.exe build has not been tested` | An unrecognized 1.08 build, such as another GOG revision or a retail-disc executable. Installation continues; please report whether it works. | | `Public Tools 1.08 not found` | The public download could not find them—install the copy under `extras\`, pass `-PublicToolsGame`, or use a local `-PrivateBundle`. | | `Package file is missing or was modified` | The package was altered after it was built; unpack it again. | | Steam creates no `FEAR.exe` after three launch attempts | The launcher now falls back to the verified executable directly while Steam remains running, then still verifies that the matching VR bridge loaded. | diff --git a/docs/ECHOPATCH.md b/docs/ECHOPATCH.md index fb16752..103d664 100644 --- a/docs/ECHOPATCH.md +++ b/docs/ECHOPATCH.md @@ -34,6 +34,91 @@ Ereignis `fear_hid_fix` mit einem der Ergebnisse `applied`, `already_applied`, `not_fear_108`, `image_too_small`, `signature_mismatch` oder `protection_failed`. +## Steam CEG und GOG Preservation Program + +Die Steam- und die am 20. März 2025 aktualisierte GOG-Fassung sind beide +verifizierte 1.08-Builds: + +| Edition | SHA-256 von `FEAR.exe` | `SizeOfImage` | Startmodus | +|---|---|---|---| +| Steam Ultimate Shooter Edition | `D5EBC38A4F12B772C9112A2811C290ADB6C5052D3BC2F817302D38CF55BB2CBE` | `0x001F3000` | Steam App 21090 | +| GOG Preservation Program | `C1678AA4DF37E87C097F45D8EB68A7C379D99AD12D8DA2771CF6235D9493D0B2` | `0x0019D000` | `FEAR.exe` direkt | + +Beide melden Dateiversion `1.08.282.0` und PE-Zeitstempel `0x44EF6AE6`. + +### Fehlerbild und Ursache + +Vor der Korrektur startete die VR-Mod in beiden Editionen normal, aber jedes +Steam-Protokoll und das erste GOG-Protokoll meldeten +`fear_hid_fix result=signature_mismatch`. Die Bildrate war anfangs gut und nahm +anschließend während derselben Sitzung stetig ab. Das entsprach genau dem +ungepatchten HID-Fehler des Basisspiels. + +Die beiden relevanten Codeblöcke liegen in beiden Builds an denselben RVAs: + +- Der 22 Byte lange redundante HID-Block bei `0x840DD` ist bytegleich mit dem + bereits unterstützten 1.08-Build. +- Der 29 Byte lange Legacy-Input-Block bei `0x84057` verwendet im GOG-Build + auf dem Datenträger und im von CEG entpackten Steam-Prozess dieselben IAT- + Operanden: `FF 15 70 C0 54 00` und `FF 15 3C C4 54 00`. + +Beim Laden des frühen Steam-`dinput8.dll`-Proxys sind diese Steam-Codepages +noch CEG-verschlüsselt. Die erste Prüfung muss daher fehlschlagen. Nachdem CEG +das Image entpackt hat, stimmen die Live-Bytes dagegen mit der vollständig +verifizierten gemeinsamen Signatur überein. Die frühere Implementierung +ordnete diese Bytes nur der kleineren GOG-Imagegröße zu und lehnte Steam auch +nach dem Entpacken ab. Eine pauschal gelockerte Signatur wäre unsicher gewesen. + +### Abgesicherte Korrektur + +Die gemeinsame entpackte 29-Byte-Signatur wird ausschließlich für die beiden +exakten Imagegrößen `0x001F3000` (Steam) und `0x0019D000` (GOG) akzeptiert. +Danach muss weiterhin der gesamte gemeinsame 22-Byte-HID-Block passen. + +GOG kann bereits beim Laden des `dinput8.dll`-Proxys gepatcht werden. Bei +Steam wiederholt der GameClient-Loader dieselbe vollständig abgesicherte +Prüfung nach dem CEG-Entpacken, aber vor der DirectInput-Geräteinitialisierung. +Der exportierte `DirectInput8Create`-Proxy führt unmittelbar davor eine letzte +abgesicherte Wiederholungsprüfung aus. Erst wenn beide Blöcke vollständig +verifiziert oder bereits vollständig genoppt sind, werden die noch +ursprünglichen Bytes im Prozessspeicher durch NOPs ersetzt. `FEAR.exe` auf dem +Datenträger bleibt unverändert. + +Die exakten Steam- und GOG-SHA-256-Werte sind in der Entwicklungs- und +Release-Erkennung eingetragen. Eine zukünftige Revision wird dadurch nicht +automatisch akzeptiert. + +### Verifikation am 8. August 2026 + +- Vor der Korrektur: Steam und GOG jeweils + `fear_hid_fix result=signature_mismatch`. +- GOG nach der Korrektur: `fear_hid_fix result=applied`. +- Steam nach der Korrektur: `fear_hid_fix result=already_applied`; der + GameClient-Fallback hatte beide Regionen nach dem CEG-Entpacken bereits + erfolgreich gepatcht, bevor der DirectInput-Proxy sie erneut prüfte. +- Read-only-Verifikation im laufenden Steam-Prozess: alle 29 beziehungsweise + 22 Bytes der beiden Regionen waren `0x90`. +- Vollständige x86-Testsuite: 24 von 24 Tests bestanden. +- Reale GOG- und Steam-VR-Sitzungen: derselbe Spielstand über längere Zeit + stabil und ohne den zuvor beobachteten fortschreitenden Leistungseinbruch. +- Benutzerprofil und elf vorhandene Spielstände blieben beim Overlay-Update + erhalten. + +Der neueste HID-Status lässt sich so prüfen: + +```powershell +$log = Get-ChildItem \FEARVR\logs -Recurse -Filter dinput-*.log | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 +Get-Content -LiteralPath $log.FullName +``` + +Für GOG ist normalerweise `result=applied` zu erwarten. Steam meldet +normalerweise `result=already_applied`, weil der Loader nach dem CEG-Entpacken +vor dem DirectInput-Proxy zum Zug kommt. `signature_mismatch` als endgültiges +Protokollergebnis bedeutet, dass eine andere EXE-Revision oder veränderte +Prozessbytes vorliegen; in diesem Fall bleibt der Patch absichtlich aus. + ## Was ausdrücklich nicht übernommen wurde HUD-Skalierung, FOV, SSAA, Controller-/Gyro-Unterstützung, Crash-Handler, diff --git a/src/common/fear_hid_fix.cpp b/src/common/fear_hid_fix.cpp index 7698041..3dbcff5 100644 --- a/src/common/fear_hid_fix.cpp +++ b/src/common/fear_hid_fix.cpp @@ -23,6 +23,15 @@ bool IsAllNops(const std::uint8_t* bytes, std::size_t count) noexcept { [](const std::uint8_t byte) { return byte == 0x90U; }); } +const std::array& ExpectedLegacyInputHookBytes( + const std::size_t imageSize) noexcept { + if (imageSize == kGogFear108ImageSize || + imageSize == kSteamFear108ImageSize) { + return kUnpackedLegacyInputHookBytes; + } + return kLegacyInputHookBytes; +} + } // namespace bool MatchesFear108HidSignatures(const std::uint8_t* image, @@ -30,9 +39,10 @@ bool MatchesFear108HidSignatures(const std::uint8_t* image, if (image == nullptr || imageSize < kPatchEnd) { return false; } + const auto& legacyBytes = ExpectedLegacyInputHookBytes(imageSize); return std::memcmp(image + kLegacyInputHookRva, - kLegacyInputHookBytes.data(), - kLegacyInputHookBytes.size()) == 0 && + legacyBytes.data(), + legacyBytes.size()) == 0 && std::memcmp(image + kRedundantHidDeviceRva, kRedundantHidDeviceBytes.data(), kRedundantHidDeviceBytes.size()) == 0; @@ -45,10 +55,11 @@ bool HasCompatibleFear108HidPatchState( return false; } + const auto& legacyBytes = ExpectedLegacyInputHookBytes(imageSize); const bool legacyOriginal = std::memcmp(image + kLegacyInputHookRva, - kLegacyInputHookBytes.data(), - kLegacyInputHookBytes.size()) == 0; + legacyBytes.data(), + legacyBytes.size()) == 0; const bool legacyPatched = IsAllNops(image + kLegacyInputHookRva, kLegacyInputHookBytes.size()); diff --git a/src/common/fear_hid_fix.h b/src/common/fear_hid_fix.h index 5889bd9..96eeb10 100644 --- a/src/common/fear_hid_fix.h +++ b/src/common/fear_hid_fix.h @@ -12,12 +12,24 @@ namespace fearvr { // were independently verified against our unmodified local 1.08 executable; // they are checked in full before any process memory is changed. inline constexpr std::uint32_t kFear108TimeDateStamp = 0x44EF6AE6U; +inline constexpr std::size_t kSteamFear108ImageSize = 0x1F3000U; +// GOG's March 2025 Preservation build keeps the original code RVAs and PE +// timestamp, but has a smaller image. Exact image sizes isolate the shared +// unpacked signature from other 1.08 executables that have not been verified. +inline constexpr std::size_t kGogFear108ImageSize = 0x19D000U; inline constexpr std::size_t kLegacyInputHookRva = 0x84057U; inline constexpr std::array kLegacyInputHookBytes{ 0x6A, 0x00, 0x6A, 0x00, 0xFF, 0x15, 0x1C, 0x82, 0x70, 0x00, 0x50, 0x68, 0xC0, 0x0C, 0x48, 0x00, 0x6A, 0x0D, 0xFF, 0x15, 0xA4, 0x88, 0x70, 0x00, 0xA3, 0xBC, 0x5B, 0x57, 0x00}; +// GOG contains these bytes on disk. Steam exposes the same bytes after CEG +// has unpacked the process image at runtime. +inline constexpr std::array kUnpackedLegacyInputHookBytes{ + 0x6A, 0x00, 0x6A, 0x00, 0xFF, 0x15, 0x70, 0xC0, 0x54, 0x00, + 0x50, 0x68, 0xC0, 0x0C, 0x48, 0x00, 0x6A, 0x0D, 0xFF, 0x15, + 0x3C, 0xC4, 0x54, 0x00, 0xA3, 0xBC, 0x5B, 0x57, 0x00}; + inline constexpr std::size_t kRedundantHidDeviceRva = 0x840DDU; inline constexpr std::array kRedundantHidDeviceBytes{ 0x8B, 0x3F, 0x8B, 0x0F, 0x6A, 0x01, 0x8D, 0x54, 0x24, 0x0C, diff --git a/src/common/render_pacing.h b/src/common/render_pacing.h new file mode 100644 index 0000000..2c5afd4 --- /dev/null +++ b/src/common/render_pacing.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace fearvr { + +// FEAR may reach its camera render more than once before OpenXR publishes a +// new request. Only that duplicate render should wait for the next request. +// A request that already differs from the last rendered one is fresh and must +// be consumed immediately. +inline constexpr bool ShouldWaitForNewRenderRequest( + std::uint64_t lastRenderedFrameId, + std::uint64_t currentFrameId) noexcept { + return lastRenderedFrameId != 0 && currentFrameId != 0 && + currentFrameId == lastRenderedFrameId; +} + +} // namespace fearvr diff --git a/src/dinput8_proxy/dinput8_proxy.cpp b/src/dinput8_proxy/dinput8_proxy.cpp index 2910b77..0a7e503 100644 --- a/src/dinput8_proxy/dinput8_proxy.cpp +++ b/src/dinput8_proxy/dinput8_proxy.cpp @@ -199,6 +199,12 @@ extern "C" IDirect3D9* WINAPI FearVrEarlyDirect3DCreate9( extern "C" HRESULT WINAPI DirectInput8Create( HINSTANCE instance, DWORD version, REFIID interfaceId, LPVOID* output, LPUNKNOWN outer) { + // Steam CEG still exposes encrypted code when this proxy loads. The first + // DirectInput8Create call occurs after unpacking but before Retail + // enumerates redundant HID devices, so retry the fully guarded patch. + if (g_hidFixResult == fearvr::FearHidFixResult::SignatureMismatch) { + g_hidFixResult = fearvr::ApplyFear108HidFix(); + } const auto function = reinterpret_cast( SystemExport("DirectInput8Create")); const HRESULT result = function == nullptr diff --git a/src/gameclient_loader/dllmain.cpp b/src/gameclient_loader/dllmain.cpp index ab8335f..3b3b6b7 100644 --- a/src/gameclient_loader/dllmain.cpp +++ b/src/gameclient_loader/dllmain.cpp @@ -126,10 +126,10 @@ BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) { (void)reserved; if (reason == DLL_PROCESS_ATTACH) { DisableThreadLibraryCalls(instance); - // Fallback for custom launch layouts that cannot place the early - // dinput8 proxy beside FEAR.exe. The proxy normally applies this - // before DirectInput initialization; repeating the guarded operation - // here is safe and prevents later device reinitialization paths. + // Fallback for custom launch layouts and Steam CEG. Steam can load the + // early dinput8 proxy while these FEAR.exe pages are still encrypted; + // this module is loaded after unpacking but before DirectInput device + // initialization. Repeating the guarded operation is safe. fearvr::ApplyFear108HidFix(); } return TRUE; diff --git a/src/gameclient_loader/stereo_hook.cpp b/src/gameclient_loader/stereo_hook.cpp index e0750c2..5c1bec6 100644 --- a/src/gameclient_loader/stereo_hook.cpp +++ b/src/gameclient_loader/stereo_hook.cpp @@ -33,6 +33,7 @@ #include "melee_actions.h" #include "physical_duck.h" #include "protocol.h" +#include "render_pacing.h" #include "stereo_math.h" #include "vertical_camera_height.h" #include "wrist_hud_visibility.h" @@ -7063,8 +7064,13 @@ LTRESULT RenderStereo(ILTRenderer* renderer, HLOCALOBJ camera, return g_renderCameraWithOverride( renderer, camera, techniqueOverride); } + if ((request.flags & FEARVR_RF_FLATSCREEN) != 0) { + return g_renderCameraWithOverride( + renderer, camera, techniqueOverride); + } if (g_waitForNewRenderRequest != nullptr && - g_lastStereoRenderRequestId != 0) { + ShouldWaitForNewRenderRequest( + g_lastStereoRenderRequestId, request.frameId)) { g_stereoStep = "wait_for_fresh_render_request"; FearVrRenderRequest freshRequest{}; constexpr std::uint32_t kMaximumPacingWaitMilliseconds = 20; diff --git a/src/proxy32/bridge.cpp b/src/proxy32/bridge.cpp index 360d2e3..cef03c7 100644 --- a/src/proxy32/bridge.cpp +++ b/src/proxy32/bridge.cpp @@ -1126,6 +1126,10 @@ class Bridge { } InterlockedIncrement64(Atomic64(shared_->gameHeartbeat)); + if (config_.disableCapture) { + RecordBypassPresentRateLocked(); + return; + } PollPending(); UpdateHostConnection(); EnsureDeviceMetadata(device); @@ -1740,6 +1744,40 @@ class Bridge { } private: + void RecordBypassPresentRateLocked() noexcept { + const auto now = std::chrono::steady_clock::now(); + if (bypassPresentWindowStart_.time_since_epoch().count() == 0) { + bypassPresentWindowStart_ = now; + bypassPresentCount_ = 1; + logger_.Write( + "WARN", "capture_disabled", + "Frame staging and transfer are disabled; Present rate " + "is measured without bridge capture work."); + return; + } + + ++bypassPresentCount_; + const auto elapsed = std::chrono::duration_cast< + std::chrono::microseconds>( + now - bypassPresentWindowStart_); + constexpr std::int64_t kLogIntervalMicroseconds = 3000000; + if (elapsed.count() < kLogIntervalMicroseconds) { + return; + } + const double framesPerSecond = + static_cast(bypassPresentCount_) * 1000000.0 / + static_cast(elapsed.count()); + std::ostringstream message; + message << "presents=" << bypassPresentCount_ + << " window_us=" << elapsed.count() + << " fps=" << std::fixed << std::setprecision(1) + << framesPerSecond; + logger_.Write( + "INFO", "capture_bypass_present_rate", message.str()); + bypassPresentCount_ = 0; + bypassPresentWindowStart_ = now; + } + void ServiceCaptureDuringPacingWaitLocked() noexcept { // The current Latest-Frame worker owns CPU/D3D9Ex transfers. Pacing // only polls its publication query; the older PR queue is disabled. @@ -1840,7 +1878,7 @@ class Bridge { } void BeginStereoEye(std::uint32_t eye) noexcept { - if (eye >= FEARVR_EYE_COUNT) { + if (eye >= FEARVR_EYE_COUNT || config_.disableCapture) { return; } std::lock_guard lock(mutex_); @@ -1860,7 +1898,8 @@ class Bridge { } void CaptureStereoEye(std::uint32_t eye) noexcept { - if (eye >= FEARVR_EYE_COUNT || config_.sessionId == 0) { + if (eye >= FEARVR_EYE_COUNT || config_.sessionId == 0 || + config_.disableCapture) { return; } std::lock_guard lock(mutex_); @@ -1923,6 +1962,9 @@ class Bridge { void EndStereoFrame( std::uint64_t frameId, const FearVrGameCameraSample* camera) noexcept { + if (config_.disableCapture) { + return; + } std::lock_guard lock(mutex_); RestoreStereoRenderTarget(); if (!stereoAccepting_) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f7635d4..d0d3e51 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -188,6 +188,15 @@ if(MSVC) endif() add_test(NAME render_scale COMMAND test_render_scale) +add_executable(test_render_pacing test_render_pacing.cpp) +target_link_libraries( + test_render_pacing PRIVATE fearvr-common fearvr-warnings +) +if(MSVC) + target_compile_options(test_render_pacing PRIVATE /UNDEBUG) +endif() +add_test(NAME render_pacing COMMAND test_render_pacing) + if(CMAKE_SIZEOF_VOID_P EQUAL 4 AND TARGET fearvr-proxy) # Isolierter M2-Producer: Der echte System-Import bleibt d3d9.lib; erst # nach dem Link wird ausschließlich neben diese Test-EXE der Proxy kopiert. diff --git a/tests/test_hid_fix.cpp b/tests/test_hid_fix.cpp index 2a5e15f..8a0e336 100644 --- a/tests/test_hid_fix.cpp +++ b/tests/test_hid_fix.cpp @@ -46,5 +46,56 @@ int main() { image.data(), fearvr::kLegacyInputHookRva)); assert(!fearvr::HasCompatibleFear108HidPatchState( image.data(), fearvr::kLegacyInputHookRva)); + + std::vector steamImage( + fearvr::kSteamFear108ImageSize, 0); + std::copy( + fearvr::kUnpackedLegacyInputHookBytes.begin(), + fearvr::kUnpackedLegacyInputHookBytes.end(), + steamImage.begin() + fearvr::kLegacyInputHookRva); + std::copy( + fearvr::kRedundantHidDeviceBytes.begin(), + fearvr::kRedundantHidDeviceBytes.end(), + steamImage.begin() + fearvr::kRedundantHidDeviceRva); + assert(fearvr::MatchesFear108HidSignatures( + steamImage.data(), steamImage.size())); + assert(fearvr::HasCompatibleFear108HidPatchState( + steamImage.data(), steamImage.size())); + + std::vector gogImage( + fearvr::kGogFear108ImageSize, 0); + std::copy( + fearvr::kUnpackedLegacyInputHookBytes.begin(), + fearvr::kUnpackedLegacyInputHookBytes.end(), + gogImage.begin() + fearvr::kLegacyInputHookRva); + std::copy( + fearvr::kRedundantHidDeviceBytes.begin(), + fearvr::kRedundantHidDeviceBytes.end(), + gogImage.begin() + fearvr::kRedundantHidDeviceRva); + assert(fearvr::MatchesFear108HidSignatures( + gogImage.data(), gogImage.size())); + assert(fearvr::HasCompatibleFear108HidPatchState( + gogImage.data(), gogImage.size())); + + std::copy( + fearvr::kLegacyInputHookBytes.begin(), + fearvr::kLegacyInputHookBytes.end(), + gogImage.begin() + fearvr::kLegacyInputHookRva); + assert(!fearvr::MatchesFear108HidSignatures( + gogImage.data(), gogImage.size())); + assert(!fearvr::HasCompatibleFear108HidPatchState( + gogImage.data(), gogImage.size())); + + std::copy( + fearvr::kUnpackedLegacyInputHookBytes.begin(), + fearvr::kUnpackedLegacyInputHookBytes.end(), + gogImage.begin() + fearvr::kLegacyInputHookRva); + std::fill_n( + gogImage.begin() + fearvr::kRedundantHidDeviceRva, + fearvr::kRedundantHidDeviceBytes.size(), std::uint8_t{0x90U}); + assert(!fearvr::MatchesFear108HidSignatures( + gogImage.data(), gogImage.size())); + assert(fearvr::HasCompatibleFear108HidPatchState( + gogImage.data(), gogImage.size())); return 0; } diff --git a/tests/test_render_pacing.cpp b/tests/test_render_pacing.cpp new file mode 100644 index 0000000..2e44052 --- /dev/null +++ b/tests/test_render_pacing.cpp @@ -0,0 +1,26 @@ +#include + +#include "render_pacing.h" + +int main() { + using fearvr::ShouldWaitForNewRenderRequest; + + // The first stereo frame never waits. + assert(!ShouldWaitForNewRenderRequest(0, 1)); + + // Reaching the camera render again with the same OpenXR request is the + // only case that pacing should block. + assert(ShouldWaitForNewRenderRequest(41, 41)); + + // A request that advanced since the previous stereo render is already + // fresh. Waiting here caused the beta.8 half-rate regression. + assert(!ShouldWaitForNewRenderRequest(41, 42)); + + // A host restart may reset its frame counter. Treat the newly published + // request as fresh instead of waiting on an unrelated numeric ordering. + assert(!ShouldWaitForNewRenderRequest(900, 1)); + + // Invalid request IDs are never pacing candidates. + assert(!ShouldWaitForNewRenderRequest(41, 0)); + return 0; +} diff --git a/tools/_fearvr-env.ps1 b/tools/_fearvr-env.ps1 index 6bb7e62..4cb6002 100644 --- a/tools/_fearvr-env.ps1 +++ b/tools/_fearvr-env.ps1 @@ -5,13 +5,20 @@ # ============================================================================= # --- Verifizierter Ausgangszustand (§2, geprüft 2026-07-24) --- +$retailRootOverride = $env:FEARVR_RETAIL_ROOT +$retailRoot = if ([string]::IsNullOrWhiteSpace($retailRootOverride)) { + 'C:\Program Files (x86)\Steam\steamapps\common\FEAR Ultimate Shooter Edition' +} else { + [IO.Path]::GetFullPath($retailRootOverride) +} + $script:FearVr = [ordered]@{ ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path - RetailRoot = 'C:\Program Files (x86)\Steam\steamapps\common\FEAR Ultimate Shooter Edition' + RetailRoot = $retailRoot FearExeName = 'FEAR.exe' ExpectedVersion = '1.08.282.0' ExpectedSha256 = 'D5EBC38A4F12B772C9112A2811C290ADB6C5052D3BC2F817302D38CF55BB2CBE' - # Beide Steam-1.08-Varianten sind lokal verifiziert. Der HD-Texturen- + # Diese 1.08-Varianten sind lokal verifiziert. Der HD-Texturen- # Installer ersetzt FEAR.exe; sein Uninstaller stellt die Stock-Datei # wieder her. Ein Wechsel zwischen genau diesen beiden Hashes ist sicher. KnownRetailHashes = [ordered]@{ @@ -19,6 +26,8 @@ $script:FearVr = [ordered]@{ 'Steam, Ultimate Shooter Edition 1.08' 'D662DCCDB2EBD17D1ACED7C725A8724060010718146E0C0074DA5E8EF89B82B4' = 'Steam 1.08 + HDTextures4FEAR/XP v2.0.2' + 'C1678AA4DF37E87C097F45D8EB68A7C379D99AD12D8DA2771CF6235D9493D0B2' = + 'GOG Preservation Program 1.08 (2025-03-20)' } SdkInstallerRel = 'extras\fear_publictools_108.exe' SdkInstallerSize = 671441087 diff --git a/tools/launch-m2-fear.ps1 b/tools/launch-m2-fear.ps1 index 3ba0e2b..22e9879 100644 --- a/tools/launch-m2-fear.ps1 +++ b/tools/launch-m2-fear.ps1 @@ -35,6 +35,12 @@ param( [switch]$NoGpuHud, + [switch]$NoStereo, + + [switch]$NoCapture, + + [switch]$NoXrFramePacing, + [switch]$Wait ) @@ -204,6 +210,15 @@ if ($Milestone -in @('M4', 'M5') -and $NoHeadBob) { if ($NoGpuHud) { $steamArguments += '-fearvr-no-gpu-hud' } +if ($NoStereo) { + $steamArguments += '-fearvr-no-stereo' +} +if ($NoCapture) { + $steamArguments += '-fearvr-no-capture' +} +if ($NoXrFramePacing) { + $steamArguments += '-fearvr-no-xr-frame-pacing' +} if ($Milestone -eq 'M5') { $steamArguments += '-fearvr-input' } @@ -331,10 +346,15 @@ do { Stop-StartedM2Processes throw 'D3D9 und OpenXR verwenden unterschiedliche GPU-Adapter.' } - $bridgeReady = - $proxyText -match '"event":"adapter_match"' -and - $proxyText -match '"event":"shared_resources"' -and - $proxyText -match '"event":"frame_ready"' + if ($NoCapture) { + $bridgeReady = + $proxyText -match '"event":"capture_disabled"' + } else { + $bridgeReady = + $proxyText -match '"event":"adapter_match"' -and + $proxyText -match '"event":"shared_resources"' -and + $proxyText -match '"event":"frame_ready"' + } if ($Milestone -ne 'M2') { $hookReady = $proxyText -match '"event":"engine_interfaces_found"' -and @@ -345,7 +365,8 @@ do { } } $hostText = Get-Content -Raw -LiteralPath $hostLog.FullName - $frameImported = $hostText -match '"event":"ipc_frame"' + $frameImported = $NoCapture -or + $hostText -match '"event":"ipc_frame"' } until (($bridgeReady -and $frameImported -and $hookReady) -or (Get-Date) -ge $deadline) if (-not $bridgeReady -or -not $frameImported -or -not $hookReady) { @@ -360,6 +381,12 @@ Write-Host ( "F.E.A.R. läuft mit $milestoneLabel-Bridge (PID $($fear.Id))." ) ` -ForegroundColor Green +if ($NoCapture) { + Write-Host ( + 'Diagnose: Capture und Host-Transfer sind vollständig deaktiviert; ' + + 'das Proxylog misst die rohe Present-Rate.' + ) -ForegroundColor Yellow +} Write-Host "HID-Fix: aktiv ($($loaded['dinput8.dll']))." if ($Milestone -ne 'M2') { if ($stereoReady) { diff --git a/tools/launch-m5-fear.ps1 b/tools/launch-m5-fear.ps1 index 16cf85f..8b1b2b7 100644 --- a/tools/launch-m5-fear.ps1 +++ b/tools/launch-m5-fear.ps1 @@ -16,6 +16,12 @@ param( [switch]$NoGpuHud, + [switch]$NoStereo, + + [switch]$NoCapture, + + [switch]$NoXrFramePacing, + [string]$Runtime = 'active', [switch]$Wait @@ -24,4 +30,6 @@ param( & "$PSScriptRoot\launch-m2-fear.ps1" -Milestone M5 ` -Translation:$Translation -StereoHud:(-not $NoStereoHud) ` -NoHeadBob:$NoHeadBob -NoGpuHud:$NoGpuHud ` + -NoStereo:$NoStereo -NoCapture:$NoCapture ` + -NoXrFramePacing:$NoXrFramePacing ` -Runtime $Runtime -Wait:$Wait diff --git a/tools/prepare-m2-stage.ps1 b/tools/prepare-m2-stage.ps1 index 72b4e71..67855b1 100644 --- a/tools/prepare-m2-stage.ps1 +++ b/tools/prepare-m2-stage.ps1 @@ -52,6 +52,18 @@ foreach ($required in @( } } +$installedDeployment = $null +$installedDeploymentPath = Join-Path $cfg.RetailRoot 'FEARVR\deployment.json' +if (Test-Path -LiteralPath $installedDeploymentPath -PathType Leaf) { + try { + $installedDeployment = + Get-Content -Raw -LiteralPath $installedDeploymentPath | + ConvertFrom-Json + } catch { + $installedDeployment = $null + } +} + $dinputDestination = Join-Path $cfg.RetailRoot 'dinput8.dll' if (Test-Path -LiteralPath $dinputDestination -PathType Leaf) { $existingDinputHash = Get-FileSha256 $dinputDestination @@ -68,6 +80,11 @@ if (Test-Path -LiteralPath $dinputDestination -PathType Leaf) { } } ) + if ($null -ne $installedDeployment -and + $installedDeployment.dinputProxySha256) { + $knownPreviousHashes += + [string]$installedDeployment.dinputProxySha256 + } $sourceHash = Get-FileSha256 $dinputSource if ($existingDinputHash -ne $sourceHash -and $existingDinputHash -notin $knownPreviousHashes) { @@ -104,6 +121,12 @@ if (Test-Path -LiteralPath $d3d9Destination -PathType Leaf) { $existingIsPriorFearVr = $false } } + if (-not $existingIsPriorFearVr -and + $null -ne $installedDeployment -and + $installedDeployment.d3d9ProxySha256 -eq + $existingD3d9Hash) { + $existingIsPriorFearVr = $true + } if (-not $existingIsPriorFearVr -and (Test-Path -LiteralPath $d3d9UpstreamDestination -PathType Leaf)) { throw ( @@ -129,9 +152,29 @@ if ($actualOriginalHash -ne $expectedOriginalHash) { $stageRoot = Assert-UnderProjectRoot ( Join-Path $cfg.ProjectRoot "stage\$milestoneSlug-game" ) -$userDirectory = Assert-UnderProjectRoot ( - Join-Path $cfg.ProjectRoot "stage\userdata-$milestoneSlug" -) +$userDirectoryOverride = $env:FEARVR_USER_DIRECTORY +if ([string]::IsNullOrWhiteSpace($userDirectoryOverride)) { + $userDirectory = Assert-UnderProjectRoot ( + Join-Path $cfg.ProjectRoot "stage\userdata-$milestoneSlug" + ) +} else { + $userDirectory = [IO.Path]::GetFullPath($userDirectoryOverride) + $installedUserDirectory = if ( + $null -ne $installedDeployment -and + $installedDeployment.userDirectory + ) { + [IO.Path]::GetFullPath( + [string]$installedDeployment.userDirectory) + } else { + $null + } + if ($null -eq $installedUserDirectory -or + $userDirectory -ne $installedUserDirectory) { + throw ( + 'FEARVR_USER_DIRECTORY must exactly match the userdata path ' + + 'recorded by the verified installed FEARVR deployment.') + } +} $logDirectory = Assert-UnderProjectRoot ( Join-Path $cfg.ProjectRoot 'logs' ) diff --git a/tools/release/_fearvr-release.ps1 b/tools/release/_fearvr-release.ps1 index bf7e217..fe9dfb8 100644 --- a/tools/release/_fearvr-release.ps1 +++ b/tools/release/_fearvr-release.ps1 @@ -11,7 +11,8 @@ $script:FearVrRelease = [ordered]@{ ExpectedSha256 = 'D5EBC38A4F12B772C9112A2811C290ADB6C5052D3BC2F817302D38CF55BB2CBE' # Bekannte, tatsächlich getestete FEAR.exe-Builds. Jede andere 1.08-EXE - # (GOG, Retail-DVD) kann laufen, ist hier aber nicht bestätigt: Alle + # (weitere GOG-Revisionen, Retail-DVD) kann laufen, ist hier aber nicht + # bestätigt: Alle # versionsabhängigen Signaturen dieses Mods liegen in GameOrig.dll aus den # Public Tools, nicht in FEAR.exe. Deshalb wird ein unbekannter Hash nur # noch gemeldet und nicht mehr als Abbruchgrund behandelt. @@ -20,6 +21,8 @@ $script:FearVrRelease = [ordered]@{ 'Steam, Ultimate Shooter Edition 1.08' 'D662DCCDB2EBD17D1ACED7C725A8724060010718146E0C0074DA5E8EF89B82B4' = 'Steam 1.08 + HDTextures4FEAR/XP v2.0.2' + 'C1678AA4DF37E87C097F45D8EB68A7C379D99AD12D8DA2771CF6235D9493D0B2' = + 'GOG Preservation Program 1.08 (2025-03-20)' } # Unverändertes VC7.1-GameClient.dll aus den Public Tools 1.08. Dient als diff --git a/tools/release/install.ps1 b/tools/release/install.ps1 index f5584ef..25556f7 100644 --- a/tools/release/install.ps1 +++ b/tools/release/install.ps1 @@ -264,8 +264,8 @@ if (-not $retail.Verified) { # in GameOrig.dll aus den Public Tools, nicht in FEAR.exe. Write-Host @" [!] This FEAR.exe build has not been tested with the mod. - Only the Steam Ultimate Shooter Edition 1.08 is confirmed; other 1.08 - builds (GOG, retail disc) are expected to work but are unverified. + The listed Steam and GOG Preservation 1.08 builds are confirmed; other + 1.08 builds (other GOG revisions, retail disc) remain unverified. Please report success or failure so the build can be listed. SHA-256: $($retail.Sha256) "@ -ForegroundColor Yellow