From 3ab4dcb317751c1fdf835f8a8276939f6c942969 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:44:38 +0000 Subject: [PATCH 1/5] Initial plan From a18005f478b2afc72519281ff9137476c14262d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:50:50 +0000 Subject: [PATCH 2/5] Add 4-View Foveated ISR Pipeline core components and example game integration Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/games/ExampleUE/ExampleUECameraModule.cpp | 48 +++++ src/games/ExampleUE/ExampleUECameraModule.h | 31 ++++ src/games/ExampleUE/ExampleUEEntry.cpp | 19 ++ src/games/ExampleUE/ExampleUEEntry.h | 20 ++ .../ExampleUE/ExampleUERendererModule.cpp | 47 +++++ src/games/ExampleUE/ExampleUERendererModule.h | 24 +++ src/games/ExampleUE/memory/offsets.h | 35 ++++ src/mods/foveated/FoveatedAtlas.cpp | 173 ++++++++++++++++++ src/mods/foveated/FoveatedAtlas.hpp | 64 +++++++ src/mods/foveated/StereoEmulator.cpp | 148 +++++++++++++++ src/mods/foveated/StereoEmulator.hpp | 65 +++++++ src/mods/foveated/ViewInjector.cpp | 80 ++++++++ src/mods/foveated/ViewInjector.hpp | 52 ++++++ src/mods/foveated/VisibilityCache.cpp | 117 ++++++++++++ src/mods/foveated/VisibilityCache.hpp | 52 ++++++ 15 files changed, 975 insertions(+) create mode 100644 src/games/ExampleUE/ExampleUECameraModule.cpp create mode 100644 src/games/ExampleUE/ExampleUECameraModule.h create mode 100644 src/games/ExampleUE/ExampleUEEntry.cpp create mode 100644 src/games/ExampleUE/ExampleUEEntry.h create mode 100644 src/games/ExampleUE/ExampleUERendererModule.cpp create mode 100644 src/games/ExampleUE/ExampleUERendererModule.h create mode 100644 src/games/ExampleUE/memory/offsets.h create mode 100644 src/mods/foveated/FoveatedAtlas.cpp create mode 100644 src/mods/foveated/FoveatedAtlas.hpp create mode 100644 src/mods/foveated/StereoEmulator.cpp create mode 100644 src/mods/foveated/StereoEmulator.hpp create mode 100644 src/mods/foveated/ViewInjector.cpp create mode 100644 src/mods/foveated/ViewInjector.hpp create mode 100644 src/mods/foveated/VisibilityCache.cpp create mode 100644 src/mods/foveated/VisibilityCache.hpp diff --git a/src/games/ExampleUE/ExampleUECameraModule.cpp b/src/games/ExampleUE/ExampleUECameraModule.cpp new file mode 100644 index 0000000..b625ad7 --- /dev/null +++ b/src/games/ExampleUE/ExampleUECameraModule.cpp @@ -0,0 +1,48 @@ +#include "ExampleUECameraModule.h" +#include "memory/offsets.h" +#include "aer/ConstantsPool.h" +#include "mods/VR.hpp" + +void ExampleUECameraModule::installHooks() { + auto calcViewFn = memory::calcViewAddr(); + if (calcViewFn != 0) { + m_calcViewHook = safetyhook::create_inline( + reinterpret_cast(calcViewFn), + reinterpret_cast(&onCalcView) + ); + } + + auto getProjectionFn = memory::getProjectionAddr(); + if (getProjectionFn != 0) { + m_getProjectionHook = safetyhook::create_inline( + reinterpret_cast(getProjectionFn), + reinterpret_cast(&onGetProjection) + ); + } +} + +void ExampleUECameraModule::onCalcView(sdk::APlayerCameraManager* camMgr, float dt, sdk::FMinimalViewInfo* outView) { + auto inst = get(); + inst->m_calcViewHook.call(camMgr, dt, outView); + + auto vr = VR::get(); + if (vr->is_hmd_active()) { + // Get VR transforms + [[maybe_unused]] auto eye = vr->get_current_eye_transform(); + [[maybe_unused]] auto hmd = vr->get_transform(0); + [[maybe_unused]] auto offset = vr->get_transform_offset(); + + // Apply VR transform to outView->Location/Rotation + // This is game-specific and requires knowledge of the SDK structures + } +} + +void ExampleUECameraModule::onGetProjection(glm::mat4* outProj, float fov, float aspect, float nearZ, float farZ) { + auto inst = get(); + inst->m_getProjectionHook.call(outProj, fov, aspect, nearZ, farZ); + + auto vr = VR::get(); + if (vr->is_hmd_active() && outProj != nullptr) { + GlobalPool::submit_projection(*outProj, vr->m_render_frame_count); + } +} diff --git a/src/games/ExampleUE/ExampleUECameraModule.h b/src/games/ExampleUE/ExampleUECameraModule.h new file mode 100644 index 0000000..b58ce16 --- /dev/null +++ b/src/games/ExampleUE/ExampleUECameraModule.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +// Forward declarations for SDK types +namespace sdk { + struct FMinimalViewInfo; + struct APlayerCameraManager; +} + +class ExampleUECameraModule { +public: + static ExampleUECameraModule* get() { + static auto inst = new ExampleUECameraModule(); + return inst; + } + + void installHooks(); + +private: + ExampleUECameraModule() = default; + ~ExampleUECameraModule() = default; + + safetyhook::InlineHook m_calcViewHook{}; + safetyhook::InlineHook m_getProjectionHook{}; + + static void onCalcView(sdk::APlayerCameraManager* camMgr, float dt, sdk::FMinimalViewInfo* outView); + static void onGetProjection(glm::mat4* outProj, float fov, float aspect, float nearZ, float farZ); +}; diff --git a/src/games/ExampleUE/ExampleUEEntry.cpp b/src/games/ExampleUE/ExampleUEEntry.cpp new file mode 100644 index 0000000..1248b3a --- /dev/null +++ b/src/games/ExampleUE/ExampleUEEntry.cpp @@ -0,0 +1,19 @@ +#include "ExampleUEEntry.h" +#include "ExampleUERendererModule.h" +#include "ExampleUECameraModule.h" +#include + +std::optional ExampleUEEntry::on_initialize() { + ExampleUERendererModule::get()->installHooks(); + ExampleUECameraModule::get()->installHooks(); + return std::nullopt; +} + +void ExampleUEEntry::on_draw_ui() { + if (!ImGui::CollapsingHeader(get_name().data())) { + return; + } + + m_hudScale->draw("HUD Scale"); + m_decoupledPitch->draw("Decoupled Pitch"); +} diff --git a/src/games/ExampleUE/ExampleUEEntry.h b/src/games/ExampleUE/ExampleUEEntry.h new file mode 100644 index 0000000..632a632 --- /dev/null +++ b/src/games/ExampleUE/ExampleUEEntry.h @@ -0,0 +1,20 @@ +#pragma once + +#include "Mod.hpp" +#include + +class ExampleUEEntry : public Mod { +public: + static std::shared_ptr& get() { + static auto inst = std::make_shared(); + return inst; + } + + std::string_view get_name() const override { return "ExampleUE VR"; } + std::optional on_initialize() override; + void on_draw_ui() override; + +private: + ModSlider::Ptr m_hudScale{ModSlider::create("HudScale", 0.1f, 1.0f, 0.5f)}; + ModToggle::Ptr m_decoupledPitch{ModToggle::create("DecoupledPitch", false)}; +}; diff --git a/src/games/ExampleUE/ExampleUERendererModule.cpp b/src/games/ExampleUE/ExampleUERendererModule.cpp new file mode 100644 index 0000000..1a98ce1 --- /dev/null +++ b/src/games/ExampleUE/ExampleUERendererModule.cpp @@ -0,0 +1,47 @@ +#include "ExampleUERendererModule.h" +#include "memory/offsets.h" +#include "mods/VR.hpp" +#include "Framework.hpp" + +void ExampleUERendererModule::installHooks() { + auto beginFrameFn = memory::beginFrameAddr(); + if (beginFrameFn != 0) { + m_beginFrameHook = safetyhook::create_inline( + reinterpret_cast(beginFrameFn), + reinterpret_cast(&onBeginFrame) + ); + } + + auto beginRenderFn = memory::beginRenderAddr(); + if (beginRenderFn != 0) { + m_beginRenderHook = safetyhook::create_inline( + reinterpret_cast(beginRenderFn), + reinterpret_cast(&onBeginRender) + ); + } +} + +uintptr_t ExampleUERendererModule::onBeginFrame() { + auto inst = get(); + return inst->m_beginFrameHook.call(); +} + +uintptr_t ExampleUERendererModule::onBeginRender(void* ctx) { + auto inst = get(); + + if (g_framework->is_ready()) { + auto vr = VR::get(); + vr->m_engine_frame_count++; + g_framework->enable_engine_thread(); + g_framework->run_imgui_frame(false); + vr->m_render_frame_count = vr->m_engine_frame_count; + vr->on_begin_rendering(vr->m_render_frame_count); + vr->update_hmd_state(vr->m_render_frame_count); + + auto result = inst->m_beginRenderHook.call(ctx); + vr->m_presenter_frame_count = vr->m_render_frame_count; + return result; + } + + return inst->m_beginRenderHook.call(ctx); +} diff --git a/src/games/ExampleUE/ExampleUERendererModule.h b/src/games/ExampleUE/ExampleUERendererModule.h new file mode 100644 index 0000000..47095d6 --- /dev/null +++ b/src/games/ExampleUE/ExampleUERendererModule.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +class ExampleUERendererModule { +public: + static ExampleUERendererModule* get() { + static auto inst = new ExampleUERendererModule(); + return inst; + } + + void installHooks(); + +private: + ExampleUERendererModule() = default; + ~ExampleUERendererModule() = default; + + safetyhook::InlineHook m_beginFrameHook{}; + safetyhook::InlineHook m_beginRenderHook{}; + + static uintptr_t onBeginFrame(); + static uintptr_t onBeginRender(void* context); +}; diff --git a/src/games/ExampleUE/memory/offsets.h b/src/games/ExampleUE/memory/offsets.h new file mode 100644 index 0000000..65e98d3 --- /dev/null +++ b/src/games/ExampleUE/memory/offsets.h @@ -0,0 +1,35 @@ +#pragma once + +#include "memory/memory_mul.h" +#include + +namespace memory { + +// Game-specific pattern/offset definitions +// These patterns need to be updated for each game version + +inline uintptr_t beginFrameAddr() { + // Pattern for UE BeginFrame function + // Example: "48 89 5C 24 ? 57 48 83 EC 20 48 8B D9 E8" + return FuncRelocation("BeginFrame", "48 89 5C 24 ? 57 48 83 EC 20 48 8B D9 E8", 0x0); +} + +inline uintptr_t beginRenderAddr() { + // Pattern for UE BeginRender function + // Example: "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 30" + return FuncRelocation("BeginRender", "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 30", 0x0); +} + +inline uintptr_t calcViewAddr() { + // Pattern for APlayerCameraManager::CalcView + // Example: "40 53 48 83 EC 40 48 8B DA 48 8B D1" + return FuncRelocation("CalcView", "40 53 48 83 EC 40 48 8B DA 48 8B D1", 0x0); +} + +inline uintptr_t getProjectionAddr() { + // Pattern for projection matrix calculation + // Example: "48 83 EC 48 0F 29 74 24 ?" + return FuncRelocation("GetProjection", "48 83 EC 48 0F 29 74 24 ?", 0x0); +} + +} // namespace memory diff --git a/src/mods/foveated/FoveatedAtlas.cpp b/src/mods/foveated/FoveatedAtlas.cpp new file mode 100644 index 0000000..b8c797c --- /dev/null +++ b/src/mods/foveated/FoveatedAtlas.cpp @@ -0,0 +1,173 @@ +#include "FoveatedAtlas.hpp" +#include + +namespace foveated { + +bool FoveatedAtlas::initialize(ID3D12Device* dev, const AtlasConfig& cfg) { + if (!dev) { + spdlog::error("[FoveatedAtlas] Cannot initialize with null device"); + return false; + } + + m_cfg = cfg; + + // Create RTV descriptor heap + D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc{}; + rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; + rtvHeapDesc.NumDescriptors = 1; + rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + + HRESULT hr = dev->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&m_rtvHeap)); + if (FAILED(hr)) { + spdlog::error("[FoveatedAtlas] Failed to create RTV descriptor heap: {:x}", hr); + return false; + } + + // Create DSV descriptor heap + D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc{}; + dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; + dsvHeapDesc.NumDescriptors = 1; + dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + + hr = dev->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&m_dsvHeap)); + if (FAILED(hr)) { + spdlog::error("[FoveatedAtlas] Failed to create DSV descriptor heap: {:x}", hr); + return false; + } + + // Create atlas render target (double-height for 4-view layout) + D3D12_RESOURCE_DESC rtDesc{}; + rtDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + rtDesc.Alignment = 0; + rtDesc.Width = getTotalWidth(); + rtDesc.Height = getTotalHeight(); + rtDesc.DepthOrArraySize = 1; + rtDesc.MipLevels = 1; + rtDesc.Format = cfg.format; + rtDesc.SampleDesc.Count = 1; + rtDesc.SampleDesc.Quality = 0; + rtDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + rtDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + + D3D12_HEAP_PROPERTIES heapProps{}; + heapProps.Type = D3D12_HEAP_TYPE_DEFAULT; + + D3D12_CLEAR_VALUE clearValue{}; + clearValue.Format = cfg.format; + clearValue.Color[0] = 0.0f; + clearValue.Color[1] = 0.0f; + clearValue.Color[2] = 0.0f; + clearValue.Color[3] = 1.0f; + + hr = dev->CreateCommittedResource( + &heapProps, + D3D12_HEAP_FLAG_NONE, + &rtDesc, + D3D12_RESOURCE_STATE_RENDER_TARGET, + &clearValue, + IID_PPV_ARGS(&m_atlas) + ); + + if (FAILED(hr)) { + spdlog::error("[FoveatedAtlas] Failed to create atlas resource: {:x}", hr); + return false; + } + + // Create depth stencil buffer + D3D12_RESOURCE_DESC depthDesc{}; + depthDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + depthDesc.Alignment = 0; + depthDesc.Width = getTotalWidth(); + depthDesc.Height = getTotalHeight(); + depthDesc.DepthOrArraySize = 1; + depthDesc.MipLevels = 1; + depthDesc.Format = DXGI_FORMAT_D32_FLOAT; + depthDesc.SampleDesc.Count = 1; + depthDesc.SampleDesc.Quality = 0; + depthDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + depthDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; + + D3D12_CLEAR_VALUE depthClearValue{}; + depthClearValue.Format = DXGI_FORMAT_D32_FLOAT; + depthClearValue.DepthStencil.Depth = 1.0f; + depthClearValue.DepthStencil.Stencil = 0; + + hr = dev->CreateCommittedResource( + &heapProps, + D3D12_HEAP_FLAG_NONE, + &depthDesc, + D3D12_RESOURCE_STATE_DEPTH_WRITE, + &depthClearValue, + IID_PPV_ARGS(&m_depth) + ); + + if (FAILED(hr)) { + spdlog::error("[FoveatedAtlas] Failed to create depth resource: {:x}", hr); + return false; + } + + // Create RTV + m_rtv = m_rtvHeap->GetCPUDescriptorHandleForHeapStart(); + dev->CreateRenderTargetView(m_atlas.Get(), nullptr, m_rtv); + + // Create DSV + m_dsv = m_dsvHeap->GetCPUDescriptorHandleForHeapStart(); + dev->CreateDepthStencilView(m_depth.Get(), nullptr, m_dsv); + + m_state = D3D12_RESOURCE_STATE_RENDER_TARGET; + + spdlog::info("[FoveatedAtlas] Initialized atlas {}x{} (foveal: {}x{}, peripheral: {}x{})", + getTotalWidth(), getTotalHeight(), + cfg.fovealWidth, cfg.fovealHeight, + cfg.peripheralWidth, cfg.peripheralHeight); + + return true; +} + +void FoveatedAtlas::shutdown() { + m_atlas.Reset(); + m_depth.Reset(); + m_rtvHeap.Reset(); + m_dsvHeap.Reset(); + m_rtv = {}; + m_dsv = {}; + m_state = D3D12_RESOURCE_STATE_COMMON; + + spdlog::info("[FoveatedAtlas] Shutdown complete"); +} + +void FoveatedAtlas::transitionToRT(ID3D12GraphicsCommandList* cmd) { + if (!cmd || !m_atlas || m_state == D3D12_RESOURCE_STATE_RENDER_TARGET) { + return; + } + + D3D12_RESOURCE_BARRIER barrier{}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + barrier.Transition.pResource = m_atlas.Get(); + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrier.Transition.StateBefore = m_state; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + + cmd->ResourceBarrier(1, &barrier); + m_state = D3D12_RESOURCE_STATE_RENDER_TARGET; +} + +void FoveatedAtlas::transitionToSRV(ID3D12GraphicsCommandList* cmd) { + if (!cmd || !m_atlas || m_state == D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) { + return; + } + + D3D12_RESOURCE_BARRIER barrier{}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + barrier.Transition.pResource = m_atlas.Get(); + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrier.Transition.StateBefore = m_state; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + + cmd->ResourceBarrier(1, &barrier); + m_state = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; +} + +} // namespace foveated diff --git a/src/mods/foveated/FoveatedAtlas.hpp b/src/mods/foveated/FoveatedAtlas.hpp new file mode 100644 index 0000000..bfd9f4c --- /dev/null +++ b/src/mods/foveated/FoveatedAtlas.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include + +namespace foveated { + +struct AtlasConfig { + uint32_t fovealWidth{1440}; + uint32_t fovealHeight{1600}; + uint32_t peripheralWidth{720}; + uint32_t peripheralHeight{800}; + DXGI_FORMAT format{DXGI_FORMAT_R10G10B10A2_UNORM}; +}; + +class FoveatedAtlas { +public: + static FoveatedAtlas& get() { + static FoveatedAtlas instance; + return instance; + } + + bool initialize(ID3D12Device* dev, const AtlasConfig& cfg); + void shutdown(); + + ID3D12Resource* getTexture() const { return m_atlas.Get(); } + ID3D12Resource* getDepthTexture() const { return m_depth.Get(); } + D3D12_CPU_DESCRIPTOR_HANDLE getRTV() const { return m_rtv; } + D3D12_CPU_DESCRIPTOR_HANDLE getDSV() const { return m_dsv; } + + uint32_t getTotalWidth() const { return m_cfg.fovealWidth * 2; } + uint32_t getTotalHeight() const { return m_cfg.fovealHeight + m_cfg.peripheralHeight; } + + uint32_t getFovealWidth() const { return m_cfg.fovealWidth; } + uint32_t getFovealHeight() const { return m_cfg.fovealHeight; } + uint32_t getPeripheralWidth() const { return m_cfg.peripheralWidth; } + uint32_t getPeripheralHeight() const { return m_cfg.peripheralHeight; } + + const AtlasConfig& getConfig() const { return m_cfg; } + + void transitionToRT(ID3D12GraphicsCommandList* cmd); + void transitionToSRV(ID3D12GraphicsCommandList* cmd); + + bool isInitialized() const { return m_atlas.Get() != nullptr; } + +private: + FoveatedAtlas() = default; + ~FoveatedAtlas() = default; + + FoveatedAtlas(const FoveatedAtlas&) = delete; + FoveatedAtlas& operator=(const FoveatedAtlas&) = delete; + + AtlasConfig m_cfg{}; + Microsoft::WRL::ComPtr m_atlas; + Microsoft::WRL::ComPtr m_depth; + Microsoft::WRL::ComPtr m_rtvHeap; + Microsoft::WRL::ComPtr m_dsvHeap; + D3D12_CPU_DESCRIPTOR_HANDLE m_rtv{}; + D3D12_CPU_DESCRIPTOR_HANDLE m_dsv{}; + D3D12_RESOURCE_STATES m_state{D3D12_RESOURCE_STATE_COMMON}; +}; + +} // namespace foveated diff --git a/src/mods/foveated/StereoEmulator.cpp b/src/mods/foveated/StereoEmulator.cpp new file mode 100644 index 0000000..235580e --- /dev/null +++ b/src/mods/foveated/StereoEmulator.cpp @@ -0,0 +1,148 @@ +#include "StereoEmulator.hpp" +#include + +namespace foveated { + +void StereoEmulator::initialize(VRRuntime* runtime) { + m_runtime = runtime; + m_stereoActive = (runtime != nullptr && runtime->ready()); + + // Initialize views with default values + for (size_t i = 0; i < 4; ++i) { + m_views[i].type = static_cast(i); + m_views[i].stereoPassMask = (i % 2 == 0) ? 0x1 : 0x2; // Primary or Secondary + m_views[i].fovScale = (i < 2) ? m_fovealScale : m_peripheralScale; + } +} + +void StereoEmulator::beginFrame(int frameIndex) { + if (!m_stereoActive || !m_runtime) { + return; + } + + buildViewMatrices(frameIndex); +} + +void StereoEmulator::buildViewMatrices(int frame) { + if (!m_runtime || !m_runtime->ready()) { + return; + } + + // Get eye transforms from runtime + const auto& eyes = m_runtime->eyes; + + // Build foveal views (higher resolution, narrower FOV) + m_views[0].view = eyes[0]; // Left eye + m_views[1].view = eyes[1]; // Right eye + + // Peripheral views share the same eye positions but with wider FOV + m_views[2].view = eyes[0]; // Left peripheral + m_views[3].view = eyes[1]; // Right peripheral + + // Compute frustum planes for culling optimization + for (auto& view : m_views) { + computeFrustumPlanes(view); + } +} + +void StereoEmulator::computeFrustumPlanes(EmulatedView& v) { + // Extract frustum planes from projection * view matrix for culling + glm::mat4 vp = v.projection * v.view; + + // Left plane + v.frustumPlanes[0] = glm::vec4( + vp[0][3] + vp[0][0], + vp[1][3] + vp[1][0], + vp[2][3] + vp[2][0], + vp[3][3] + vp[3][0] + ); + + // Right plane + v.frustumPlanes[1] = glm::vec4( + vp[0][3] - vp[0][0], + vp[1][3] - vp[1][0], + vp[2][3] - vp[2][0], + vp[3][3] - vp[3][0] + ); + + // Bottom plane + v.frustumPlanes[2] = glm::vec4( + vp[0][3] + vp[0][1], + vp[1][3] + vp[1][1], + vp[2][3] + vp[2][1], + vp[3][3] + vp[3][1] + ); + + // Top plane + v.frustumPlanes[3] = glm::vec4( + vp[0][3] - vp[0][1], + vp[1][3] - vp[1][1], + vp[2][3] - vp[2][1], + vp[3][3] - vp[3][1] + ); + + // Near plane + v.frustumPlanes[4] = glm::vec4( + vp[0][2], + vp[1][2], + vp[2][2], + vp[3][2] + ); + + // Far plane + v.frustumPlanes[5] = glm::vec4( + vp[0][3] - vp[0][2], + vp[1][3] - vp[1][2], + vp[2][3] - vp[2][2], + vp[3][3] - vp[3][2] + ); + + // Normalize planes + for (int i = 0; i < 6; ++i) { + float len = glm::length(glm::vec3(v.frustumPlanes[i])); + if (len > 0.0f) { + v.frustumPlanes[i] /= len; + } + } +} + +std::array StereoEmulator::computeAtlasViewports(uint32_t w, uint32_t h) const { + std::array viewports{}; + + // Atlas layout: [FovealL|FovealR] top, [PeriphL|PeriphR] bottom + float halfWidth = static_cast(w) / 2.0f; + float fovealHeight = static_cast(h) * 0.6f; // 60% for foveal + float peripheralHeight = static_cast(h) * 0.4f; // 40% for peripheral + + // Foveal Left (top-left) + viewports[0] = {0.0f, 0.0f, halfWidth, fovealHeight, 0.0f, 1.0f}; + + // Foveal Right (top-right) + viewports[1] = {halfWidth, 0.0f, halfWidth, fovealHeight, 0.0f, 1.0f}; + + // Peripheral Left (bottom-left) + viewports[2] = {0.0f, fovealHeight, halfWidth, peripheralHeight, 0.0f, 1.0f}; + + // Peripheral Right (bottom-right) + viewports[3] = {halfWidth, fovealHeight, halfWidth, peripheralHeight, 0.0f, 1.0f}; + + return viewports; +} + +void StereoEmulator::configureFOV(float fovealDeg, float peripheralDeg) { + m_fovealFov = fovealDeg; + m_peripheralFov = peripheralDeg; + + // Update scales based on FOV ratio + if (peripheralDeg > 0.0f) { + m_fovealScale = 1.0f; + m_peripheralScale = fovealDeg / peripheralDeg; + } + + // Update view fov scales + for (size_t i = 0; i < 4; ++i) { + m_views[i].fovScale = (i < 2) ? m_fovealScale : m_peripheralScale; + } +} + +} // namespace foveated diff --git a/src/mods/foveated/StereoEmulator.hpp b/src/mods/foveated/StereoEmulator.hpp new file mode 100644 index 0000000..17899d3 --- /dev/null +++ b/src/mods/foveated/StereoEmulator.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include "mods/vr/runtimes/VRRuntime.hpp" + +namespace foveated { + +enum class ViewType : uint8_t { + FOVEAL_LEFT_PRIMARY = 0, + FOVEAL_RIGHT_SECONDARY = 1, + PERIPHERAL_LEFT_PRIMARY = 2, + PERIPHERAL_RIGHT_SECONDARY = 3 +}; + +struct EmulatedView { + glm::mat4 projection{1.0f}; + glm::mat4 view{1.0f}; + glm::vec4 frustumPlanes[6]{}; + D3D12_VIEWPORT viewport{}; + float fovScale{1.0f}; + ViewType type{ViewType::FOVEAL_LEFT_PRIMARY}; + uint32_t stereoPassMask{0x1}; // 0x1=Primary, 0x2=Secondary +}; + +class StereoEmulator { +public: + static StereoEmulator& get() { + static StereoEmulator instance; + return instance; + } + + void initialize(VRRuntime* runtime); + void beginFrame(int frameIndex); + + bool isStereoActive() const { return m_stereoActive; } + + const EmulatedView& getView(ViewType t) const { + return m_views[static_cast(t)]; + } + + std::array computeAtlasViewports(uint32_t w, uint32_t h) const; + void configureFOV(float fovealDeg, float peripheralDeg); + +private: + StereoEmulator() = default; + ~StereoEmulator() = default; + + StereoEmulator(const StereoEmulator&) = delete; + StereoEmulator& operator=(const StereoEmulator&) = delete; + + void buildViewMatrices(int frame); + void computeFrustumPlanes(EmulatedView& v); + + std::array m_views{}; + VRRuntime* m_runtime{nullptr}; + bool m_stereoActive{false}; + float m_fovealFov{40.0f}; + float m_peripheralFov{110.0f}; + float m_fovealScale{1.0f}; + float m_peripheralScale{0.5f}; +}; + +} // namespace foveated diff --git a/src/mods/foveated/ViewInjector.cpp b/src/mods/foveated/ViewInjector.cpp new file mode 100644 index 0000000..28f483a --- /dev/null +++ b/src/mods/foveated/ViewInjector.cpp @@ -0,0 +1,80 @@ +#include "ViewInjector.hpp" +#include "D3D12Hook.hpp" + +namespace foveated { + +void ViewInjector::install(D3D12Hook* hook) { + m_hook = hook; + + if (!hook) { + return; + } + + // The actual D3D12 hook callbacks are registered through the framework's + // existing hook system. This class provides the logic for viewport/RT injection. +} + +void ViewInjector::setAtlasRT(ID3D12Resource* rt, D3D12_CPU_DESCRIPTOR_HANDLE rtv) { + m_atlasRT = rt; + m_atlasRTV = rtv; +} + +void ViewInjector::updateAtlasViewports(uint32_t width, uint32_t height) { + m_atlasViewports = StereoEmulator::get().computeAtlasViewports(width, height); +} + +void ViewInjector::onSetViewports(ID3D12GraphicsCommandList5* cmd, UINT num, const D3D12_VIEWPORT* vps) { + if (!StereoEmulator::get().isStereoActive() || !m_atlasRT) { + return; + } + + // Redirect viewports to atlas regions based on current rendering pass + if (m_isRenderingFoveal) { + injectFovealPair(cmd); + } else { + injectPeripheralPair(cmd); + } +} + +void ViewInjector::onSetRenderTargets(ID3D12GraphicsCommandList5* cmd, UINT num, + const D3D12_CPU_DESCRIPTOR_HANDLE* rtvs, BOOL single, D3D12_CPU_DESCRIPTOR_HANDLE* dsv) { + + if (!StereoEmulator::get().isStereoActive() || !m_atlasRT) { + return; + } + + // Redirect render targets to atlas RT + // The actual RT redirection is handled by the caller +} + +void ViewInjector::injectFovealPair(ID3D12GraphicsCommandList5* cmd) { + if (!cmd) { + return; + } + + // Set viewports for foveal pair (views 0 and 1) + // ISR Draw Pair A: views 0+1 (Foveal Left + Foveal Right) + std::array fovealViewports = { + m_atlasViewports[0], + m_atlasViewports[1] + }; + + cmd->RSSetViewports(2, fovealViewports.data()); +} + +void ViewInjector::injectPeripheralPair(ID3D12GraphicsCommandList5* cmd) { + if (!cmd) { + return; + } + + // Set viewports for peripheral pair (views 2 and 3) + // ISR Draw Pair B: views 2+3 (Peripheral Left + Peripheral Right) + std::array peripheralViewports = { + m_atlasViewports[2], + m_atlasViewports[3] + }; + + cmd->RSSetViewports(2, peripheralViewports.data()); +} + +} // namespace foveated diff --git a/src/mods/foveated/ViewInjector.hpp b/src/mods/foveated/ViewInjector.hpp new file mode 100644 index 0000000..12b63e3 --- /dev/null +++ b/src/mods/foveated/ViewInjector.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "StereoEmulator.hpp" +#include +#include + +class D3D12Hook; + +namespace foveated { + +class ViewInjector { +public: + static ViewInjector& get() { + static ViewInjector instance; + return instance; + } + + void install(D3D12Hook* hook); + void setAtlasRT(ID3D12Resource* rt, D3D12_CPU_DESCRIPTOR_HANDLE rtv); + + bool isInstalled() const { return m_hook != nullptr; } + bool isRenderingFoveal() const { return m_isRenderingFoveal; } + + void setRenderingFoveal(bool foveal) { m_isRenderingFoveal = foveal; } + + const std::array& getAtlasViewports() const { + return m_atlasViewports; + } + + void updateAtlasViewports(uint32_t width, uint32_t height); + +private: + ViewInjector() = default; + ~ViewInjector() = default; + + ViewInjector(const ViewInjector&) = delete; + ViewInjector& operator=(const ViewInjector&) = delete; + + void onSetViewports(ID3D12GraphicsCommandList5* cmd, UINT num, const D3D12_VIEWPORT* vps); + void onSetRenderTargets(ID3D12GraphicsCommandList5* cmd, UINT num, + const D3D12_CPU_DESCRIPTOR_HANDLE* rtvs, BOOL single, D3D12_CPU_DESCRIPTOR_HANDLE* dsv); + void injectFovealPair(ID3D12GraphicsCommandList5* cmd); + void injectPeripheralPair(ID3D12GraphicsCommandList5* cmd); + + D3D12Hook* m_hook{nullptr}; + ID3D12Resource* m_atlasRT{nullptr}; + D3D12_CPU_DESCRIPTOR_HANDLE m_atlasRTV{}; + std::array m_atlasViewports{}; + bool m_isRenderingFoveal{false}; +}; + +} // namespace foveated diff --git a/src/mods/foveated/VisibilityCache.cpp b/src/mods/foveated/VisibilityCache.cpp new file mode 100644 index 0000000..122194c --- /dev/null +++ b/src/mods/foveated/VisibilityCache.cpp @@ -0,0 +1,117 @@ +#include "VisibilityCache.hpp" +#include +#include + +namespace foveated { + +void VisibilityCache::initialize(uint32_t maxPrimitives) { + std::unique_lock lock(m_mtx); + + m_maxPrimitives = maxPrimitives; + + // Calculate bytes needed for bit mask (1 bit per primitive) + size_t bytesNeeded = (maxPrimitives + 7) / 8; + + for (auto& frame : m_frames) { + for (auto& mask : frame.masks) { + mask.resize(bytesNeeded, 0); + } + frame.valid.fill(false); + } + + m_current = 0; +} + +void VisibilityCache::beginFrame(int frameIdx) { + std::unique_lock lock(m_mtx); + + m_current = frameIdx % FRAMES; + + // Reset validity for the new frame + m_frames[m_current].valid.fill(false); + + // Clear visibility masks + for (auto& mask : m_frames[m_current].masks) { + std::fill(mask.begin(), mask.end(), static_cast(0)); + } +} + +void VisibilityCache::recordVisibility(uint32_t viewIdx, const uint8_t* bits, size_t count) { + if (viewIdx >= VIEWS || !bits || count == 0) { + return; + } + + std::unique_lock lock(m_mtx); + + auto& frame = m_frames[m_current]; + auto& mask = frame.masks[viewIdx]; + + size_t bytesToCopy = std::min(count, mask.size()); + std::memcpy(mask.data(), bits, bytesToCopy); + + frame.valid[viewIdx] = true; +} + +const uint8_t* VisibilityCache::getVisibility(uint32_t viewIdx) const { + if (viewIdx >= VIEWS) { + return nullptr; + } + + std::shared_lock lock(m_mtx); + + const auto& frame = m_frames[m_current]; + if (!frame.valid[viewIdx]) { + return nullptr; + } + + return frame.masks[viewIdx].data(); +} + +bool VisibilityCache::canShare(uint32_t src, uint32_t dst) const { + // Mega-frustum optimization: foveal views can share culling with peripheral views + // View 0 (Foveal Left) can share with View 2 (Peripheral Left) + // View 1 (Foveal Right) can share with View 3 (Peripheral Right) + + if (src == 0 && dst == 2) return true; + if (src == 1 && dst == 3) return true; + + return false; +} + +void VisibilityCache::copyVisibility(uint32_t src, uint32_t dst) { + if (!canShare(src, dst)) { + return; + } + + std::unique_lock lock(m_mtx); + + auto& frame = m_frames[m_current]; + + if (!frame.valid[src]) { + return; + } + + // Copy visibility from source to destination + frame.masks[dst] = frame.masks[src]; + frame.valid[dst] = true; +} + +bool VisibilityCache::isValid(uint32_t viewIdx) const { + if (viewIdx >= VIEWS) { + return false; + } + + std::shared_lock lock(m_mtx); + return m_frames[m_current].valid[viewIdx]; +} + +size_t VisibilityCache::getVisibilityCount(uint32_t viewIdx) const { + if (viewIdx >= VIEWS) { + return 0; + } + + std::shared_lock lock(m_mtx); + return m_frames[m_current].masks[viewIdx].size(); +} + +} // namespace foveated diff --git a/src/mods/foveated/VisibilityCache.hpp b/src/mods/foveated/VisibilityCache.hpp new file mode 100644 index 0000000..1464923 --- /dev/null +++ b/src/mods/foveated/VisibilityCache.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +namespace foveated { + +class VisibilityCache { +public: + static VisibilityCache& get() { + static VisibilityCache instance; + return instance; + } + + void initialize(uint32_t maxPrimitives); + void beginFrame(int frameIdx); + + void recordVisibility(uint32_t viewIdx, const uint8_t* bits, size_t count); + const uint8_t* getVisibility(uint32_t viewIdx) const; + + // Check if visibility can be shared between views (0->2, 1->3) + // Foveal left can share with peripheral left, foveal right with peripheral right + bool canShare(uint32_t src, uint32_t dst) const; + void copyVisibility(uint32_t src, uint32_t dst); + + bool isValid(uint32_t viewIdx) const; + size_t getVisibilityCount(uint32_t viewIdx) const; + +private: + VisibilityCache() = default; + ~VisibilityCache() = default; + + VisibilityCache(const VisibilityCache&) = delete; + VisibilityCache& operator=(const VisibilityCache&) = delete; + + static constexpr size_t FRAMES = 3; + static constexpr size_t VIEWS = 4; + + struct Frame { + std::array, VIEWS> masks{}; + std::array valid{}; + }; + + std::array m_frames{}; + int m_current{0}; + uint32_t m_maxPrimitives{0}; + mutable std::shared_mutex m_mtx; +}; + +} // namespace foveated From 139707dd5010fc215c33b88128e18b773acd7a96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:53:10 +0000 Subject: [PATCH 3/5] Address code review feedback: improve FOV validation, implement RT redirection, enhance documentation Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/games/ExampleUE/ExampleUECameraModule.cpp | 25 +++++++++++++------ src/games/ExampleUE/memory/offsets.h | 25 ++++++++++++++----- src/mods/foveated/StereoEmulator.cpp | 7 ++++-- src/mods/foveated/ViewInjector.cpp | 7 +++--- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/games/ExampleUE/ExampleUECameraModule.cpp b/src/games/ExampleUE/ExampleUECameraModule.cpp index b625ad7..6afb016 100644 --- a/src/games/ExampleUE/ExampleUECameraModule.cpp +++ b/src/games/ExampleUE/ExampleUECameraModule.cpp @@ -26,14 +26,25 @@ void ExampleUECameraModule::onCalcView(sdk::APlayerCameraManager* camMgr, float inst->m_calcViewHook.call(camMgr, dt, outView); auto vr = VR::get(); - if (vr->is_hmd_active()) { - // Get VR transforms - [[maybe_unused]] auto eye = vr->get_current_eye_transform(); - [[maybe_unused]] auto hmd = vr->get_transform(0); - [[maybe_unused]] auto offset = vr->get_transform_offset(); + if (vr->is_hmd_active() && outView != nullptr) { + // Get VR transforms for applying to camera view + auto eye = vr->get_current_eye_transform(); + auto hmd = vr->get_transform(0); + auto offset = vr->get_transform_offset(); - // Apply VR transform to outView->Location/Rotation - // This is game-specific and requires knowledge of the SDK structures + // TODO: Apply VR transform to outView->Location/Rotation + // This requires game-specific SDK structure knowledge: + // - Extract position from hmd matrix + // - Extract rotation from eye matrix + // - Apply offset compensation + // - Write to outView->Location and outView->Rotation + // + // Example implementation (requires actual SDK structures): + // glm::vec3 hmdPos = glm::vec3(hmd[3]); + // outView->Location = FVector(hmdPos.x, hmdPos.y, hmdPos.z); + (void)eye; + (void)hmd; + (void)offset; } } diff --git a/src/games/ExampleUE/memory/offsets.h b/src/games/ExampleUE/memory/offsets.h index 65e98d3..96077f1 100644 --- a/src/games/ExampleUE/memory/offsets.h +++ b/src/games/ExampleUE/memory/offsets.h @@ -5,30 +5,43 @@ namespace memory { -// Game-specific pattern/offset definitions -// These patterns need to be updated for each game version +/** + * Game-specific pattern/offset definitions for ExampleUE + * + * IMPORTANT: These byte patterns are fragile and may break with game updates. + * + * Recommended approaches for production use: + * 1. Implement version detection to select patterns based on game version + * 2. Use multiple fallback patterns for each function + * 3. Consider using RTTI-based lookup when available (e.g., VTable method) + * 4. Store static offsets for known game versions as fallback + * + * Pattern format uses '?' for wildcard bytes that may vary between versions. + * The FuncRelocation function will use signature scanning when SIGNATURE_SCAN + * is defined, otherwise falls back to the static offset parameter. + */ inline uintptr_t beginFrameAddr() { // Pattern for UE BeginFrame function - // Example: "48 89 5C 24 ? 57 48 83 EC 20 48 8B D9 E8" + // This pattern targets the function prologue which is more stable return FuncRelocation("BeginFrame", "48 89 5C 24 ? 57 48 83 EC 20 48 8B D9 E8", 0x0); } inline uintptr_t beginRenderAddr() { // Pattern for UE BeginRender function - // Example: "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 30" + // Uses standard x64 calling convention prologue return FuncRelocation("BeginRender", "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 30", 0x0); } inline uintptr_t calcViewAddr() { // Pattern for APlayerCameraManager::CalcView - // Example: "40 53 48 83 EC 40 48 8B DA 48 8B D1" + // Alternative: Use VTable("APlayerCameraManager", offset) if RTTI available return FuncRelocation("CalcView", "40 53 48 83 EC 40 48 8B DA 48 8B D1", 0x0); } inline uintptr_t getProjectionAddr() { // Pattern for projection matrix calculation - // Example: "48 83 EC 48 0F 29 74 24 ?" + // Look for SSE register saves as they're common in matrix functions return FuncRelocation("GetProjection", "48 83 EC 48 0F 29 74 24 ?", 0x0); } diff --git a/src/mods/foveated/StereoEmulator.cpp b/src/mods/foveated/StereoEmulator.cpp index 235580e..ac580f0 100644 --- a/src/mods/foveated/StereoEmulator.cpp +++ b/src/mods/foveated/StereoEmulator.cpp @@ -1,5 +1,6 @@ #include "StereoEmulator.hpp" #include +#include namespace foveated { @@ -134,9 +135,11 @@ void StereoEmulator::configureFOV(float fovealDeg, float peripheralDeg) { m_peripheralFov = peripheralDeg; // Update scales based on FOV ratio - if (peripheralDeg > 0.0f) { + // Guard against near-zero values that could cause extreme scale ratios + constexpr float MIN_FOV_DEG = 1.0f; + if (peripheralDeg >= MIN_FOV_DEG) { m_fovealScale = 1.0f; - m_peripheralScale = fovealDeg / peripheralDeg; + m_peripheralScale = std::clamp(fovealDeg / peripheralDeg, 0.1f, 1.0f); } // Update view fov scales diff --git a/src/mods/foveated/ViewInjector.cpp b/src/mods/foveated/ViewInjector.cpp index 28f483a..c8e32b9 100644 --- a/src/mods/foveated/ViewInjector.cpp +++ b/src/mods/foveated/ViewInjector.cpp @@ -39,12 +39,13 @@ void ViewInjector::onSetViewports(ID3D12GraphicsCommandList5* cmd, UINT num, con void ViewInjector::onSetRenderTargets(ID3D12GraphicsCommandList5* cmd, UINT num, const D3D12_CPU_DESCRIPTOR_HANDLE* rtvs, BOOL single, D3D12_CPU_DESCRIPTOR_HANDLE* dsv) { - if (!StereoEmulator::get().isStereoActive() || !m_atlasRT) { + if (!StereoEmulator::get().isStereoActive() || !m_atlasRT || !cmd) { return; } - // Redirect render targets to atlas RT - // The actual RT redirection is handled by the caller + // Redirect render targets to the foveated atlas RT + // This replaces the game's render targets with our atlas + cmd->OMSetRenderTargets(1, &m_atlasRTV, FALSE, dsv); } void ViewInjector::injectFovealPair(ID3D12GraphicsCommandList5* cmd) { From 2f203f156d7895cd278f6d5357525461986297cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:52:51 +0000 Subject: [PATCH 4/5] Refactor frame lifecycle and atlas config per review feedback Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- .../ExampleUE/ExampleUERendererModule.cpp | 35 ++++++++++++----- src/games/ExampleUE/ExampleUERendererModule.h | 8 ++++ src/games/ExampleUE/memory/offsets.h | 14 +++++++ src/mods/foveated/FoveatedAtlas.cpp | 15 +++++-- src/mods/foveated/FoveatedAtlas.hpp | 39 ++++++++++++++----- src/mods/foveated/StereoEmulator.cpp | 18 +++++---- 6 files changed, 98 insertions(+), 31 deletions(-) diff --git a/src/games/ExampleUE/ExampleUERendererModule.cpp b/src/games/ExampleUE/ExampleUERendererModule.cpp index 1a98ce1..29f41dc 100644 --- a/src/games/ExampleUE/ExampleUERendererModule.cpp +++ b/src/games/ExampleUE/ExampleUERendererModule.cpp @@ -3,6 +3,9 @@ #include "mods/VR.hpp" #include "Framework.hpp" +// Cached pointer to engine's frame counter (initialized during hook installation) +static int* g_engineFrameCounterPtr = nullptr; + void ExampleUERendererModule::installHooks() { auto beginFrameFn = memory::beginFrameAddr(); if (beginFrameFn != 0) { @@ -19,29 +22,43 @@ void ExampleUERendererModule::installHooks() { reinterpret_cast(&onBeginRender) ); } + + // Cache pointer to engine's frame counter for direct access + auto frameCounterAddr = memory::engineFrameCounterAddr(); + if (frameCounterAddr != 0) { + g_engineFrameCounterPtr = reinterpret_cast(frameCounterAddr); + } } uintptr_t ExampleUERendererModule::onBeginFrame() { auto inst = get(); + + // Cache the engine frame count before the frame begins + // This reads directly from the engine's GFrameNumber or equivalent + if (g_engineFrameCounterPtr != nullptr) { + inst->cacheEngineFrameCount(*g_engineFrameCounterPtr); + } + return inst->m_beginFrameHook.call(); } uintptr_t ExampleUERendererModule::onBeginRender(void* ctx) { auto inst = get(); + // Call the original function first to let the engine update its frame state + auto result = inst->m_beginRenderHook.call(ctx); + if (g_framework->is_ready()) { auto vr = VR::get(); - vr->m_engine_frame_count++; + + // Use the engine's frame count directly - avoids sync issues + int engineFrame = inst->getEngineFrameCount(); + g_framework->enable_engine_thread(); g_framework->run_imgui_frame(false); - vr->m_render_frame_count = vr->m_engine_frame_count; - vr->on_begin_rendering(vr->m_render_frame_count); - vr->update_hmd_state(vr->m_render_frame_count); - - auto result = inst->m_beginRenderHook.call(ctx); - vr->m_presenter_frame_count = vr->m_render_frame_count; - return result; + vr->on_begin_rendering(engineFrame); + vr->update_hmd_state(engineFrame); } - return inst->m_beginRenderHook.call(ctx); + return result; } diff --git a/src/games/ExampleUE/ExampleUERendererModule.h b/src/games/ExampleUE/ExampleUERendererModule.h index 47095d6..360fae6 100644 --- a/src/games/ExampleUE/ExampleUERendererModule.h +++ b/src/games/ExampleUE/ExampleUERendererModule.h @@ -11,6 +11,11 @@ class ExampleUERendererModule { } void installHooks(); + + // Get the frame count directly from the engine + // Override this to read from the game's frame counter (e.g., GFrameNumber) + int getEngineFrameCount() const { return m_cachedEngineFrame; } + void cacheEngineFrameCount(int frame) { m_cachedEngineFrame = frame; } private: ExampleUERendererModule() = default; @@ -19,6 +24,9 @@ class ExampleUERendererModule { safetyhook::InlineHook m_beginFrameHook{}; safetyhook::InlineHook m_beginRenderHook{}; + // Cached engine frame - should be set by reading from engine's frame counter + int m_cachedEngineFrame{0}; + static uintptr_t onBeginFrame(); static uintptr_t onBeginRender(void* context); }; diff --git a/src/games/ExampleUE/memory/offsets.h b/src/games/ExampleUE/memory/offsets.h index 96077f1..000d1fc 100644 --- a/src/games/ExampleUE/memory/offsets.h +++ b/src/games/ExampleUE/memory/offsets.h @@ -45,4 +45,18 @@ inline uintptr_t getProjectionAddr() { return FuncRelocation("GetProjection", "48 83 EC 48 0F 29 74 24 ?", 0x0); } +/** + * Engine frame counter address (GFrameNumber or similar) + * Reading directly from the engine's frame counter ensures proper synchronization + * and eliminates frame lag issues compared to manually incrementing. + * + * For UE4/UE5: Look for GFrameNumber global variable + * Pattern typically references the frame counter increment location + */ +inline uintptr_t engineFrameCounterAddr() { + // Pattern that references GFrameNumber - adjust for specific game + // This is typically found near frame begin/end logic + return InstructionRelocation("EngineFrameCounter", "8B 05 ? ? ? ? 89 05 ? ? ? ?", 2, 6, 0x0); +} + } // namespace memory diff --git a/src/mods/foveated/FoveatedAtlas.cpp b/src/mods/foveated/FoveatedAtlas.cpp index b8c797c..480c4cb 100644 --- a/src/mods/foveated/FoveatedAtlas.cpp +++ b/src/mods/foveated/FoveatedAtlas.cpp @@ -9,6 +9,11 @@ bool FoveatedAtlas::initialize(ID3D12Device* dev, const AtlasConfig& cfg) { return false; } + if (cfg.baseWidth == 0 || cfg.baseHeight == 0) { + spdlog::error("[FoveatedAtlas] Invalid base dimensions from VR runtime"); + return false; + } + m_cfg = cfg; // Create RTV descriptor heap @@ -35,7 +40,9 @@ bool FoveatedAtlas::initialize(ID3D12Device* dev, const AtlasConfig& cfg) { return false; } - // Create atlas render target (double-height for 4-view layout) + // Create atlas render target + // Layout: 2 columns (L/R eyes) x 2 rows (Foveal/Peripheral) + // All 4 views have same dimensions based on VR size * scale percentages D3D12_RESOURCE_DESC rtDesc{}; rtDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; rtDesc.Alignment = 0; @@ -116,10 +123,10 @@ bool FoveatedAtlas::initialize(ID3D12Device* dev, const AtlasConfig& cfg) { m_state = D3D12_RESOURCE_STATE_RENDER_TARGET; - spdlog::info("[FoveatedAtlas] Initialized atlas {}x{} (foveal: {}x{}, peripheral: {}x{})", + spdlog::info("[FoveatedAtlas] Initialized atlas {}x{} (eye: {}x{}, scale: {:.0f}%x{:.0f}%)", getTotalWidth(), getTotalHeight(), - cfg.fovealWidth, cfg.fovealHeight, - cfg.peripheralWidth, cfg.peripheralHeight); + getEyeWidth(), getEyeHeight(), + cfg.horizontalScale * 100.0f, cfg.verticalScale * 100.0f); return true; } diff --git a/src/mods/foveated/FoveatedAtlas.hpp b/src/mods/foveated/FoveatedAtlas.hpp index bfd9f4c..7ebef83 100644 --- a/src/mods/foveated/FoveatedAtlas.hpp +++ b/src/mods/foveated/FoveatedAtlas.hpp @@ -6,12 +6,30 @@ namespace foveated { +/** + * Atlas configuration using percentages of VR render size + * Both foveal and peripheral views use the same texture dimensions, + * controlled by horizontal and vertical scale percentages. + */ struct AtlasConfig { - uint32_t fovealWidth{1440}; - uint32_t fovealHeight{1600}; - uint32_t peripheralWidth{720}; - uint32_t peripheralHeight{800}; + // Scale percentages relative to VR runtime's reported width/height + // These control the atlas size as a percentage of vr->get_hmd_width()/get_hmd_height() + float horizontalScale{1.0f}; // 1.0 = 100% of VR width per eye + float verticalScale{1.0f}; // 1.0 = 100% of VR height per eye + + // Base dimensions from VR runtime (set via initializeFromVR) + uint32_t baseWidth{0}; // From vr->get_hmd_width() + uint32_t baseHeight{0}; // From vr->get_hmd_height() + DXGI_FORMAT format{DXGI_FORMAT_R10G10B10A2_UNORM}; + + // Computed eye dimensions (same for foveal and peripheral) + uint32_t getEyeWidth() const { + return static_cast(baseWidth * horizontalScale); + } + uint32_t getEyeHeight() const { + return static_cast(baseHeight * verticalScale); + } }; class FoveatedAtlas { @@ -29,13 +47,14 @@ class FoveatedAtlas { D3D12_CPU_DESCRIPTOR_HANDLE getRTV() const { return m_rtv; } D3D12_CPU_DESCRIPTOR_HANDLE getDSV() const { return m_dsv; } - uint32_t getTotalWidth() const { return m_cfg.fovealWidth * 2; } - uint32_t getTotalHeight() const { return m_cfg.fovealHeight + m_cfg.peripheralHeight; } + // Atlas layout: 2 columns (L/R) x 2 rows (Foveal/Peripheral) + // All 4 views have the same dimensions + uint32_t getTotalWidth() const { return m_cfg.getEyeWidth() * 2; } + uint32_t getTotalHeight() const { return m_cfg.getEyeHeight() * 2; } - uint32_t getFovealWidth() const { return m_cfg.fovealWidth; } - uint32_t getFovealHeight() const { return m_cfg.fovealHeight; } - uint32_t getPeripheralWidth() const { return m_cfg.peripheralWidth; } - uint32_t getPeripheralHeight() const { return m_cfg.peripheralHeight; } + // All views share the same dimensions + uint32_t getEyeWidth() const { return m_cfg.getEyeWidth(); } + uint32_t getEyeHeight() const { return m_cfg.getEyeHeight(); } const AtlasConfig& getConfig() const { return m_cfg; } diff --git a/src/mods/foveated/StereoEmulator.cpp b/src/mods/foveated/StereoEmulator.cpp index ac580f0..280d6de 100644 --- a/src/mods/foveated/StereoEmulator.cpp +++ b/src/mods/foveated/StereoEmulator.cpp @@ -110,22 +110,24 @@ void StereoEmulator::computeFrustumPlanes(EmulatedView& v) { std::array StereoEmulator::computeAtlasViewports(uint32_t w, uint32_t h) const { std::array viewports{}; - // Atlas layout: [FovealL|FovealR] top, [PeriphL|PeriphR] bottom - float halfWidth = static_cast(w) / 2.0f; - float fovealHeight = static_cast(h) * 0.6f; // 60% for foveal - float peripheralHeight = static_cast(h) * 0.4f; // 40% for peripheral + // Atlas layout: 2x2 grid with equal-sized views + // [FovealL | FovealR ] top row + // [PeriphL | PeriphR ] bottom row + // All 4 views have identical dimensions (w/2 x h/2) + float eyeWidth = static_cast(w) / 2.0f; + float eyeHeight = static_cast(h) / 2.0f; // Foveal Left (top-left) - viewports[0] = {0.0f, 0.0f, halfWidth, fovealHeight, 0.0f, 1.0f}; + viewports[0] = {0.0f, 0.0f, eyeWidth, eyeHeight, 0.0f, 1.0f}; // Foveal Right (top-right) - viewports[1] = {halfWidth, 0.0f, halfWidth, fovealHeight, 0.0f, 1.0f}; + viewports[1] = {eyeWidth, 0.0f, eyeWidth, eyeHeight, 0.0f, 1.0f}; // Peripheral Left (bottom-left) - viewports[2] = {0.0f, fovealHeight, halfWidth, peripheralHeight, 0.0f, 1.0f}; + viewports[2] = {0.0f, eyeHeight, eyeWidth, eyeHeight, 0.0f, 1.0f}; // Peripheral Right (bottom-right) - viewports[3] = {halfWidth, fovealHeight, halfWidth, peripheralHeight, 0.0f, 1.0f}; + viewports[3] = {eyeWidth, eyeHeight, eyeWidth, eyeHeight, 0.0f, 1.0f}; return viewports; } From e19961016ffd6e8590b1242f5785e7c38ec050de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:54:22 +0000 Subject: [PATCH 5/5] Address code review: add thread safety and pointer validation Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/games/ExampleUE/ExampleUERendererModule.cpp | 14 ++++++++++---- src/mods/foveated/FoveatedAtlas.cpp | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/games/ExampleUE/ExampleUERendererModule.cpp b/src/games/ExampleUE/ExampleUERendererModule.cpp index 29f41dc..9c0c2f7 100644 --- a/src/games/ExampleUE/ExampleUERendererModule.cpp +++ b/src/games/ExampleUE/ExampleUERendererModule.cpp @@ -2,9 +2,11 @@ #include "memory/offsets.h" #include "mods/VR.hpp" #include "Framework.hpp" +#include // Cached pointer to engine's frame counter (initialized during hook installation) -static int* g_engineFrameCounterPtr = nullptr; +// Using atomic for thread-safe access +static std::atomic g_engineFrameCounterPtr{nullptr}; void ExampleUERendererModule::installHooks() { auto beginFrameFn = memory::beginFrameAddr(); @@ -26,7 +28,7 @@ void ExampleUERendererModule::installHooks() { // Cache pointer to engine's frame counter for direct access auto frameCounterAddr = memory::engineFrameCounterAddr(); if (frameCounterAddr != 0) { - g_engineFrameCounterPtr = reinterpret_cast(frameCounterAddr); + g_engineFrameCounterPtr.store(reinterpret_cast(frameCounterAddr)); } } @@ -35,8 +37,12 @@ uintptr_t ExampleUERendererModule::onBeginFrame() { // Cache the engine frame count before the frame begins // This reads directly from the engine's GFrameNumber or equivalent - if (g_engineFrameCounterPtr != nullptr) { - inst->cacheEngineFrameCount(*g_engineFrameCounterPtr); + int* framePtr = g_engineFrameCounterPtr.load(); + if (framePtr != nullptr) { + // Validate pointer is within expected memory range before dereferencing + if (reinterpret_cast(framePtr) > 0x10000) { + inst->cacheEngineFrameCount(*framePtr); + } } return inst->m_beginFrameHook.call(); diff --git a/src/mods/foveated/FoveatedAtlas.cpp b/src/mods/foveated/FoveatedAtlas.cpp index 480c4cb..abf84c1 100644 --- a/src/mods/foveated/FoveatedAtlas.cpp +++ b/src/mods/foveated/FoveatedAtlas.cpp @@ -123,7 +123,7 @@ bool FoveatedAtlas::initialize(ID3D12Device* dev, const AtlasConfig& cfg) { m_state = D3D12_RESOURCE_STATE_RENDER_TARGET; - spdlog::info("[FoveatedAtlas] Initialized atlas {}x{} (eye: {}x{}, scale: {:.0f}%x{:.0f}%)", + spdlog::info("[FoveatedAtlas] Initialized atlas {}x{} (eye: {}x{}, scale: {:.0f}% H, {:.0f}% V)", getTotalWidth(), getTotalHeight(), getEyeWidth(), getEyeHeight(), cfg.horizontalScale * 100.0f, cfg.verticalScale * 100.0f);