Skip to content

Commit c068241

Browse files
Remleoclaude
andcommitted
Slate: find the viewport when UE5.5 passes all windows as one array
On Stalker 2 (UE 5.5.4) the hooked draw function takes a TConstArrayView of per-window args in a3, not FViewportInfo&, and a4 is not the other 5.5 variant either. The viewport was never found, so the UI was not moved to its own layer and did not show in the headset. When a4 is not the 5.5 variant, read a3 as {data, num} and take the viewport of the first window, using the offsets UESDK reads from the code. Every step checks for a live object: array data in the heap, window and viewport with a vtable inside a module. The heap check keeps builds that pass FViewportInfo& here on their old path, since its first field is a vtable inside the image. There is no capacity after num; that word measured 0 and must not be used as a bound. Needs the UESDK change that adds sdk::slate::locate_windows_array_offsets. Measured: UI shows on Stalker 2, no rejected frames over a session; The Outer Worlds 2 (UE 5.4) keeps its FViewportInfo path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4ee5c6b commit c068241

1 file changed

Lines changed: 62 additions & 1 deletion

File tree

‎src/mods/vr/FFakeStereoRenderingHook.cpp‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5811,7 +5811,61 @@ void FFakeStereoRenderingHook::post_init_properties(uintptr_t localplayer) {
58115811
g_hook->m_fixed_localplayer_view_count = true;
58125812
}
58135813

5814-
void* FFakeStereoRenderingHook::slate_draw_window_render_thread(void* renderer, void* a2, void* a3,
5814+
// Returns the viewport of the first window in a TArray of per-window args, or nullptr when anything on
5815+
// the way does not look like a live object. Only the first window is used: the game viewport is the main window.
5816+
static sdk::ISlateViewport* get_windows_array_viewport(void* args_array, const sdk::slate::WindowsArrayOffsets& offsets) {
5817+
// A TConstArrayView, not a TArray: there is no capacity after num, and the word there is garbage (measured 0).
5818+
struct TArrayHeader {
5819+
uintptr_t data;
5820+
int32_t num;
5821+
};
5822+
5823+
constexpr int32_t MAX_WINDOWS = 64;
5824+
5825+
auto is_object = [](uintptr_t obj) {
5826+
if (obj == 0 || IsBadReadPtr((void*)obj, sizeof(void*))) {
5827+
return false;
5828+
}
5829+
5830+
const auto vtable = *(uintptr_t*)obj;
5831+
return vtable != 0 && utility::get_module_within(vtable).has_value();
5832+
};
5833+
5834+
if (args_array == nullptr || IsBadReadPtr(args_array, sizeof(TArrayHeader))) {
5835+
return nullptr;
5836+
}
5837+
5838+
const auto& header = *(TArrayHeader*)args_array;
5839+
5840+
// TArray data is heap memory. On builds that pass FViewportInfo& here the first field is a vtable
5841+
// inside the image, which this rejects before anything is dereferenced.
5842+
if (header.num < 1 || header.num > MAX_WINDOWS || header.data == 0 ||
5843+
utility::get_module_within(header.data).has_value() ||
5844+
IsBadReadPtr((void*)(header.data + offsets.window), sizeof(void*)))
5845+
{
5846+
SPDLOG_INFO_EVERY_N_SEC(5, "[SlateRHIRenderer::DrawWindow_RenderThread] a3 is not a windows array (data {:x}, num {})", header.data, header.num);
5847+
return nullptr;
5848+
}
5849+
5850+
const auto window = *(uintptr_t*)(header.data + offsets.window);
5851+
5852+
if (!is_object(window) || IsBadReadPtr((void*)(window + offsets.viewport), sizeof(void*))) {
5853+
SPDLOG_INFO_EVERY_N_SEC(5, "[SlateRHIRenderer::DrawWindow_RenderThread] No live window at args + 0x{:x} ({:x})", offsets.window, window);
5854+
return nullptr;
5855+
}
5856+
5857+
const auto viewport = *(uintptr_t*)(window + offsets.viewport);
5858+
5859+
if (!is_object(viewport)) {
5860+
SPDLOG_INFO_EVERY_N_SEC(5, "[SlateRHIRenderer::DrawWindow_RenderThread] Window {:x} has no viewport at 0x{:x}", window, offsets.viewport);
5861+
return nullptr;
5862+
}
5863+
5864+
SPDLOG_INFO_ONCE("[SlateRHIRenderer::DrawWindow_RenderThread] Using viewport {:x} of window {:x} ({} windows)", viewport, window, header.num);
5865+
return (sdk::ISlateViewport*)viewport;
5866+
}
5867+
5868+
void* FFakeStereoRenderingHook::slate_draw_window_render_thread(void* renderer, void* a2, void* a3,
58155869
void* a4, void* params, void* unk1, void* unk2)
58165870
{
58175871
#ifdef FFAKE_STEREO_RENDERING_LOG_ALL_CALLS
@@ -5848,6 +5902,13 @@ void* FFakeStereoRenderingHook::slate_draw_window_render_thread(void* renderer,
58485902
if (!a4_is_ue_5_5_variant) {
58495903
// How are we going to fix this on UE5.5?
58505904
g_hook->get_slate_thread_worker()->execute((FRHICommandListImmediate*)a2);
5905+
5906+
// UE5.5 builds that draw all windows in one call pass TArray<args> in a3 (seen on Stalker 2, 5.5.4).
5907+
static const auto windows_array_offsets = sdk::slate::locate_windows_array_offsets((uintptr_t)g_hook->m_slate_thread_hook.target_address());
5908+
5909+
if (windows_array_offsets) {
5910+
slate_viewport = get_windows_array_viewport(a3, *windows_array_offsets);
5911+
}
58515912
} else {
58525913
const auto window = (uintptr_t)a4_ptr[2];
58535914

0 commit comments

Comments
 (0)