From c54219171494f09e477e16648968e78f3f12d3cb Mon Sep 17 00:00:00 2001 From: remleo Date: Fri, 28 Aug 2026 18:22:12 +0200 Subject: [PATCH] FRenderTarget: fix the gamma hook landing on a garbage vtable slot The check on the gamma index compared a std::optional with 0. An empty optional compares as less than any number, so "display_gamma_index != 0" was true exactly when the index had not been found. Dereferencing it wrote gamma_increase_fn at whatever slot that produced, and the render target's vtable pointer was then swapped to that copy. In the log it shows up as a failed search followed by "Hooked FRenderTarget!" on the next line, and the second eye keeps the gamma the hook was supposed to fix. Use has_value, and give up if the index is past the end of the copied vtable instead of writing outside it. Also log which of the two sources the gamma came from, once. A viewport that reports 2.2 and a missing viewport falling back to the constant 2.2 look the same on screen, so there was no way to tell a working hook from one that happens to look right. Only the first of them follows the game's own gamma setting. On its own this turns a silent vtable corruption into a warning. For the index to be found at all the searches in UESDK need fixing too, which is a separate change over there. With both, the hook installs on The Outer Worlds 2, Silent Hill 2 and Gylt, at gamma index 7, 5 and 4, and the second eye matches the first. --- src/mods/vr/FFakeStereoRenderingHook.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/mods/vr/FFakeStereoRenderingHook.cpp b/src/mods/vr/FFakeStereoRenderingHook.cpp index 18d1f4e57..148f14391 100644 --- a/src/mods/vr/FFakeStereoRenderingHook.cpp +++ b/src/mods/vr/FFakeStereoRenderingHook.cpp @@ -7278,9 +7278,18 @@ bool VRRenderTargetManager_Base::create_scene_capture() try { auto viewport = rtm != nullptr ? rtm->get_viewport() : nullptr; if (viewport != nullptr) { - return viewport->get_display_gamma(); + const auto gamma = viewport->get_display_gamma(); + + // Logged because the two branches look the same on screen. A viewport reporting 2.2 and a + // missing viewport falling back to 2.2 give the same picture, but only this one follows the + // game's own gamma setting. + SPDLOG_INFO_ONCE("[FRenderTarget] Matching the scene capture's display gamma to the viewport's, currently {}", gamma); + + return gamma; } + SPDLOG_WARN_ONCE("[FRenderTarget] No viewport to read the display gamma from, assuming 2.2"); + return 2.2f; }; @@ -7295,7 +7304,18 @@ bool VRRenderTargetManager_Base::create_scene_capture() try { auto& vtable = *(void**)frt; memcpy(original_frender_target_vtable.data(), vtable, original_frender_target_vtable.size() * sizeof(uintptr_t)); - if (auto display_gamma_index = sdk::FRenderTarget::get_display_gamma_index(); display_gamma_index != 0) { + // has_value(), not != 0. + // + // An empty std::optional compares as less than any number, so "index != 0" was true exactly when + // the index had not been found. Dereferencing it wrote gamma_increase_fn at whatever slot that + // produced, and the vtable was swapped to that copy. In the log it shows up as a failed search + // with "Hooked FRenderTarget!" on the next line. + if (const auto display_gamma_index = sdk::FRenderTarget::get_display_gamma_index(); display_gamma_index.has_value()) { + if (*display_gamma_index >= original_frender_target_vtable.size()) { + SPDLOG_WARN("[FRenderTarget] Gamma index {} is out of range, can't hook!", *display_gamma_index); + return; + } + original_frender_target_vtable[*display_gamma_index] = (uintptr_t)gamma_increase_fn; vtable = original_frender_target_vtable.data(); SPDLOG_INFO("[FRenderTarget] Hooked FRenderTarget!");