From 01104c951aaef0a3586e3ca5fbe64ca5fbc20117 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:25 +0000 Subject: [PATCH 01/10] Initial plan From ce66b4d55c8d2290ddc22aca488e9d816df5c8f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:48:30 +0000 Subject: [PATCH 02/10] docs: add foveated isr design spec Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- FOVEATED_ISR_DESIGN.md | 410 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 FOVEATED_ISR_DESIGN.md diff --git a/FOVEATED_ISR_DESIGN.md b/FOVEATED_ISR_DESIGN.md new file mode 100644 index 0000000..c7de4e7 --- /dev/null +++ b/FOVEATED_ISR_DESIGN.md @@ -0,0 +1,410 @@ +# 4-View Foveated ISR Pipeline - Design Specification + +## Requirements + +### Prerequisites +- UE SDK headers generated (via UE4SS dump or manual RE) +- `vrframework` as submodule/dependency +- Game-specific memory offsets for hook targets + +### Folder Structure for Game Integration +``` +src/games/{GameName}/ +├── {GameName}Entry.h // Mod entry point, inherits from Mod +├── {GameName}Entry.cpp +├── {GameName}RendererModule.h // Frame/render tick hooks +├── {GameName}RendererModule.cpp +├── {GameName}CameraModule.h // View/projection matrix hooks +├── {GameName}CameraModule.cpp +├── memory/ +│ └── offsets.h // Game-specific pattern/offset definitions +└── sdk/ + └── {GameName}SDK.h // Reversed/generated engine structs +``` + +--- + +## Architecture (Flat) + +``` +D3D12Hook (vtable/pointer) --> ViewInjector --> FoveatedAtlas (double-height RT) + | | | + v v v +SetViewports/SetRTs StereoEmulator(4 views) VisibilityCache + | + v + ISR Draw Pairs: A(0+1), B(2+3) + +Atlas Layout: [FovealL|FovealR] top, [PeriphL|PeriphR] bottom +``` + +--- + +## Core Components + +### 1. StereoEmulator +Manages 4 emulated views with projection/frustum data. + +```cpp +// src/mods/foveated/StereoEmulator.hpp +#pragma once +#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, view; + glm::vec4 frustumPlanes[6]; + D3D12_VIEWPORT viewport; + float fovScale; + ViewType type; + uint32_t stereoPassMask; // 0x1=Primary, 0x2=Secondary +}; + +class StereoEmulator { +public: + static StereoEmulator& get(); + void initialize(runtimes::VRRuntime* runtime); + void beginFrame(int frameIndex); + bool isStereoActive() const { return m_stereoActive; } + const EmulatedView& getView(ViewType t) const { return m_views[(size_t)t]; } + std::array computeAtlasViewports(uint32_t w, uint32_t h) const; + void configureFOV(float fovealDeg, float peripheralDeg); +private: + void buildViewMatrices(int frame); + void computeFrustumPlanes(EmulatedView& v); + std::array m_views; + runtimes::VRRuntime* m_runtime = nullptr; + bool m_stereoActive = false; + float m_fovealFov = 40.f, m_peripheralFov = 110.f; + float m_fovealScale = 1.f, m_peripheralScale = 0.5f; +}; +} // namespace foveated +``` + +### 2. ViewInjector +Hooks D3D12 viewport/RT calls to redirect to atlas. + +```cpp +// src/mods/foveated/ViewInjector.hpp +#pragma once +#include "StereoEmulator.hpp" +#include + +namespace foveated { + +class ViewInjector { +public: + static ViewInjector& get(); + void install(class D3D12Hook* hook); + void setAtlasRT(ID3D12Resource* rt, D3D12_CPU_DESCRIPTOR_HANDLE rtv); +private: + 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 +``` + +### 3. FoveatedAtlas +Double-height render target for 4-view output. + +```cpp +// src/mods/foveated/FoveatedAtlas.hpp +#pragma once +#include +#include + +namespace foveated { + +struct AtlasConfig { + uint32_t fovealWidth = 1440, fovealHeight = 1600; + uint32_t peripheralWidth = 720, peripheralHeight = 800; + DXGI_FORMAT format = DXGI_FORMAT_R10G10B10A2_UNORM; +}; + +class FoveatedAtlas { +public: + static FoveatedAtlas& get(); + bool initialize(ID3D12Device* dev, const AtlasConfig& cfg); + void shutdown(); + ID3D12Resource* getTexture() const { return m_atlas.Get(); } + D3D12_CPU_DESCRIPTOR_HANDLE getRTV() const { return m_rtv; } + uint32_t getTotalWidth() const { return m_cfg.fovealWidth * 2; } + uint32_t getTotalHeight() const { return m_cfg.fovealHeight + m_cfg.peripheralHeight; } + void transitionToRT(ID3D12GraphicsCommandList* cmd); + void transitionToSRV(ID3D12GraphicsCommandList* cmd); +private: + AtlasConfig m_cfg; + Microsoft::WRL::ComPtr m_atlas, m_depth; + Microsoft::WRL::ComPtr m_rtvHeap, m_dsvHeap; + D3D12_CPU_DESCRIPTOR_HANDLE m_rtv{}, m_dsv{}; + D3D12_RESOURCE_STATES m_state = D3D12_RESOURCE_STATE_COMMON; +}; +} // namespace foveated +``` + +### 4. VisibilityCache (Mega-Frustum Optimization) +Shares culling results between foveal/peripheral pairs. + +```cpp +// src/mods/foveated/VisibilityCache.hpp +#pragma once +#include +#include +#include + +namespace foveated { + +class VisibilityCache { +public: + static VisibilityCache& get(); + 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; + bool canShare(uint32_t src, uint32_t dst) const; // 0->2, 1->3 + void copyVisibility(uint32_t src, uint32_t dst); +private: + static constexpr size_t FRAMES = 3, VIEWS = 4; + struct Frame { std::array, VIEWS> masks; std::array valid; }; + std::array m_frames; + int m_current = 0; + mutable std::shared_mutex m_mtx; +}; +} // namespace foveated +``` + +--- + +## Example Game Integration (UE-based) + +Following `acsvr` pattern for Unreal Engine games: + +```cpp +// src/games/ExampleUE/ExampleUEEntry.h +#pragma once +#include "Mod.hpp" + +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); +}; +``` + +```cpp +// src/games/ExampleUE/ExampleUEEntry.cpp +#include "ExampleUEEntry.h" +#include "ExampleUERendererModule.h" +#include "ExampleUECameraModule.h" + +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"); +} +``` + +```cpp +// src/games/ExampleUE/ExampleUERendererModule.h +#pragma once +#include + +class ExampleUERendererModule { +public: + static ExampleUERendererModule* get(); + void installHooks(); +private: + safetyhook::InlineHook m_beginFrameHook{}; + safetyhook::InlineHook m_beginRenderHook{}; + static uintptr_t onBeginFrame(); + static uintptr_t onBeginRender(void* context); +}; +``` + +```cpp +// src/games/ExampleUE/ExampleUERendererModule.cpp +#include "ExampleUERendererModule.h" +#include "memory/offsets.h" +#include +#include + +ExampleUERendererModule* ExampleUERendererModule::get() { + static auto inst = new ExampleUERendererModule(); + return inst; +} + +void ExampleUERendererModule::installHooks() { + auto beginFrameFn = memory::beginFrameAddr(); + m_beginFrameHook = safetyhook::create_inline((void*)beginFrameFn, (void*)&onBeginFrame); + + auto beginRenderFn = memory::beginRenderAddr(); + m_beginRenderHook = safetyhook::create_inline((void*)beginRenderFn, (void*)&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); +} +``` + +```cpp +// src/games/ExampleUE/ExampleUECameraModule.h +#pragma once +#include +#include + +namespace sdk { struct FMinimalViewInfo; struct APlayerCameraManager; } + +class ExampleUECameraModule { +public: + static ExampleUECameraModule* get(); + void installHooks(); +private: + 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); +}; +``` + +```cpp +// src/games/ExampleUE/ExampleUECameraModule.cpp +#include "ExampleUECameraModule.h" +#include "memory/offsets.h" +#include "aer/ConstantsPool.h" +#include + +ExampleUECameraModule* ExampleUECameraModule::get() { + static auto inst = new ExampleUECameraModule(); + return inst; +} + +void ExampleUECameraModule::installHooks() { + auto calcViewFn = memory::calcViewAddr(); + m_calcViewHook = safetyhook::create_inline((void*)calcViewFn, (void*)&onCalcView); + + auto getProjectionFn = memory::getProjectionAddr(); + m_getProjectionHook = safetyhook::create_inline((void*)getProjectionFn, (void*)&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()) { + 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 + } +} + +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()) { + GlobalPool::submit_projection(*outProj, vr->m_render_frame_count); + } +} +``` + +```cpp +// src/games/ExampleUE/memory/offsets.h +#pragma once +#include "memory/memory_mul.h" + +namespace memory { + inline uintptr_t beginFrameAddr() { + return FuncRelocation("BeginFrame", "48 89 5C 24 ? 57 48 83 EC 20 48 8B D9 E8", 0x0); + } + inline uintptr_t beginRenderAddr() { + return FuncRelocation("BeginRender", "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 30", 0x0); + } + inline uintptr_t calcViewAddr() { + return FuncRelocation("CalcView", "40 53 48 83 EC 40 48 8B DA 48 8B D1", 0x0); + } + inline uintptr_t getProjectionAddr() { + return FuncRelocation("GetProjection", "48 83 EC 48 0F 29 74 24 ?", 0x0); + } +} +``` + +--- + +## Integration Checklist + +1. **Create game folder**: `src/games/{GameName}/` +2. **Define offsets**: Find via signature scan or static analysis +3. **Implement modules**: + - `{GameName}Entry` - Mod registration, UI + - `{GameName}RendererModule` - Frame begin/end hooks + - `{GameName}CameraModule` - View/projection hooks +4. **Register in Mods.cpp**: Add to mod list +5. **Hook targets** (minimum required): + - Frame begin (increment frame counters) + - Render begin (call VR::on_begin_rendering) + - Projection matrix calc (inject VR projection) + - View matrix calc (inject HMD transform) + +--- + +## Key Hook Points Summary + +| Hook | Purpose | vrframework API | +|------|---------|-----------------| +| BeginFrame | Frame counter sync | `vr->m_engine_frame_count++` | +| BeginRender | VR state update | `vr->on_begin_rendering()`, `vr->update_hmd_state()` | +| CalcProjection | VR projection injection | `GlobalPool::submit_projection()` | +| CalcView | HMD transform injection | `vr->get_transform()`, `vr->get_current_eye_transform()` | From d136bfa74a6835a2dc4a450f1db3730cc4efb5d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:26:38 +0000 Subject: [PATCH 03/10] feat: implement foveated pipeline scaffolding Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/games/ExampleUE/ExampleUECameraModule.cpp | 49 +++++++ src/games/ExampleUE/ExampleUECameraModule.h | 19 +++ src/games/ExampleUE/ExampleUEEntry.cpp | 19 +++ src/games/ExampleUE/ExampleUEEntry.h | 19 +++ .../ExampleUE/ExampleUERendererModule.cpp | 44 ++++++ src/games/ExampleUE/ExampleUERendererModule.h | 16 +++ src/games/ExampleUE/memory/offsets.h | 18 +++ src/games/ExampleUE/sdk/ExampleUESDK.h | 16 +++ src/mods/foveated/FoveatedAtlas.cpp | 125 ++++++++++++++++++ src/mods/foveated/FoveatedAtlas.hpp | 43 ++++++ src/mods/foveated/StereoEmulator.cpp | 115 ++++++++++++++++ src/mods/foveated/StereoEmulator.hpp | 54 ++++++++ src/mods/foveated/ViewInjector.cpp | 94 +++++++++++++ src/mods/foveated/ViewInjector.hpp | 33 +++++ src/mods/foveated/VisibilityCache.cpp | 73 ++++++++++ src/mods/foveated/VisibilityCache.hpp | 35 +++++ 16 files changed, 772 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/games/ExampleUE/sdk/ExampleUESDK.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..438864c --- /dev/null +++ b/src/games/ExampleUE/ExampleUECameraModule.cpp @@ -0,0 +1,49 @@ +#include "ExampleUECameraModule.h" + +#include "memory/offsets.h" +#include "aer/ConstantsPool.h" +#include "sdk/ExampleUESDK.h" +#include + +ExampleUECameraModule* ExampleUECameraModule::get() { + static auto inst = new ExampleUECameraModule(); + return inst; +} + +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() && outView != nullptr) { + const auto eye = vr->get_current_eye_transform(); + const auto hmd = vr->get_transform(0); + const auto offset = vr->get_transform_offset(); + + // Minimal application of HMD transform to the view info + const auto view = hmd * offset * eye; + outView->Location = glm::vec3{ view[3].x, view[3].y, view[3].z }; + } +} + +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..0f68211 --- /dev/null +++ b/src/games/ExampleUE/ExampleUECameraModule.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace sdk { struct FMinimalViewInfo; struct APlayerCameraManager; } + +class ExampleUECameraModule { +public: + static ExampleUECameraModule* get(); + void installHooks(); + +private: + 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..956b4ff --- /dev/null +++ b/src/games/ExampleUE/ExampleUEEntry.cpp @@ -0,0 +1,19 @@ +#include "ExampleUEEntry.h" + +#include "ExampleUECameraModule.h" +#include "ExampleUERendererModule.h" + +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..d865233 --- /dev/null +++ b/src/games/ExampleUE/ExampleUEEntry.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Mod.hpp" + +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..be09b5c --- /dev/null +++ b/src/games/ExampleUE/ExampleUERendererModule.cpp @@ -0,0 +1,44 @@ +#include "ExampleUERendererModule.h" + +#include "memory/offsets.h" +#include +#include + +ExampleUERendererModule* ExampleUERendererModule::get() { + static auto inst = new ExampleUERendererModule(); + return inst; +} + +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 && 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..8e681c6 --- /dev/null +++ b/src/games/ExampleUE/ExampleUERendererModule.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +class ExampleUERendererModule { +public: + static ExampleUERendererModule* get(); + void installHooks(); + +private: + 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..65b82df --- /dev/null +++ b/src/games/ExampleUE/memory/offsets.h @@ -0,0 +1,18 @@ +#pragma once + +#include "memory/memory_mul.h" + +namespace memory { + inline uintptr_t beginFrameAddr() { + return FuncRelocation("BeginFrame", "48 89 5C 24 ? 57 48 83 EC 20 48 8B D9 E8", 0x0); + } + inline uintptr_t beginRenderAddr() { + return FuncRelocation("BeginRender", "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 30", 0x0); + } + inline uintptr_t calcViewAddr() { + return FuncRelocation("CalcView", "40 53 48 83 EC 40 48 8B DA 48 8B D1", 0x0); + } + inline uintptr_t getProjectionAddr() { + return FuncRelocation("GetProjection", "48 83 EC 48 0F 29 74 24 ?", 0x0); + } +} // namespace memory diff --git a/src/games/ExampleUE/sdk/ExampleUESDK.h b/src/games/ExampleUE/sdk/ExampleUESDK.h new file mode 100644 index 0000000..d268160 --- /dev/null +++ b/src/games/ExampleUE/sdk/ExampleUESDK.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +namespace sdk { + +struct FMinimalViewInfo { + glm::vec3 Location{0.0f}; + glm::quat Rotation{1.0f, 0.0f, 0.0f, 0.0f}; + float FOV{90.0f}; +}; + +struct APlayerCameraManager {}; + +} // namespace sdk diff --git a/src/mods/foveated/FoveatedAtlas.cpp b/src/mods/foveated/FoveatedAtlas.cpp new file mode 100644 index 0000000..9b6937d --- /dev/null +++ b/src/mods/foveated/FoveatedAtlas.cpp @@ -0,0 +1,125 @@ +#include "FoveatedAtlas.hpp" + +namespace foveated { + +FoveatedAtlas& FoveatedAtlas::get() { + static FoveatedAtlas inst; + return inst; +} + +bool FoveatedAtlas::initialize(ID3D12Device* dev, const AtlasConfig& cfg) { + if (dev == nullptr) { + return false; + } + + m_cfg = cfg; + const auto totalWidth = getTotalWidth(); + const auto totalHeight = getTotalHeight(); + + D3D12_HEAP_PROPERTIES heapProps{}; + heapProps.Type = D3D12_HEAP_TYPE_DEFAULT; + + D3D12_RESOURCE_DESC texDesc{}; + texDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + texDesc.Alignment = 0; + texDesc.Width = totalWidth; + texDesc.Height = totalHeight; + texDesc.DepthOrArraySize = 1; + texDesc.MipLevels = 1; + texDesc.Format = m_cfg.format; + texDesc.SampleDesc.Count = 1; + texDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + texDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + + D3D12_CLEAR_VALUE clear{}; + clear.Format = m_cfg.format; + clear.Color[0] = 0.0f; + clear.Color[1] = 0.0f; + clear.Color[2] = 0.0f; + clear.Color[3] = 1.0f; + + if (FAILED(dev->CreateCommittedResource(&heapProps, D3D12_HEAP_FLAG_NONE, &texDesc, + D3D12_RESOURCE_STATE_RENDER_TARGET, &clear, IID_PPV_ARGS(&m_atlas)))) { + return false; + } + + D3D12_DESCRIPTOR_HEAP_DESC rtvDesc{}; + rtvDesc.NumDescriptors = 1; + rtvDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; + if (FAILED(dev->CreateDescriptorHeap(&rtvDesc, IID_PPV_ARGS(&m_rtvHeap)))) { + shutdown(); + return false; + } + + m_rtv = m_rtvHeap->GetCPUDescriptorHandleForHeapStart(); + dev->CreateRenderTargetView(m_atlas.Get(), nullptr, m_rtv); + + // Depth buffer (optional) + D3D12_DESCRIPTOR_HEAP_DESC dsvDesc{}; + dsvDesc.NumDescriptors = 1; + dsvDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; + dev->CreateDescriptorHeap(&dsvDesc, IID_PPV_ARGS(&m_dsvHeap)); + if (m_dsvHeap) { + D3D12_RESOURCE_DESC depthDesc = texDesc; + depthDesc.Format = DXGI_FORMAT_D32_FLOAT; + depthDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; + + D3D12_CLEAR_VALUE depthClear{}; + depthClear.Format = DXGI_FORMAT_D32_FLOAT; + depthClear.DepthStencil.Depth = 1.0f; + depthClear.DepthStencil.Stencil = 0; + + if (SUCCEEDED(dev->CreateCommittedResource(&heapProps, D3D12_HEAP_FLAG_NONE, &depthDesc, + D3D12_RESOURCE_STATE_DEPTH_WRITE, &depthClear, IID_PPV_ARGS(&m_depth)))) { + m_dsv = m_dsvHeap->GetCPUDescriptorHandleForHeapStart(); + dev->CreateDepthStencilView(m_depth.Get(), nullptr, m_dsv); + } + } + + m_state = D3D12_RESOURCE_STATE_RENDER_TARGET; + 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; +} + +void FoveatedAtlas::transitionToRT(ID3D12GraphicsCommandList* cmd) { + if (!m_atlas || cmd == nullptr || m_state == D3D12_RESOURCE_STATE_RENDER_TARGET) { + return; + } + + D3D12_RESOURCE_BARRIER barrier{}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + 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 (!m_atlas || cmd == nullptr || m_state == D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) { + return; + } + + D3D12_RESOURCE_BARRIER barrier{}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + 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..1a7b011 --- /dev/null +++ b/src/mods/foveated/FoveatedAtlas.hpp @@ -0,0 +1,43 @@ +#pragma once + +#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(); + + bool initialize(ID3D12Device* dev, const AtlasConfig& cfg); + void shutdown(); + + ID3D12Resource* getTexture() const { return m_atlas.Get(); } + D3D12_CPU_DESCRIPTOR_HANDLE getRTV() const { return m_rtv; } + + uint32_t getTotalWidth() const { return m_cfg.fovealWidth * 2; } + uint32_t getTotalHeight() const { return m_cfg.fovealHeight + m_cfg.peripheralHeight; } + + void transitionToRT(ID3D12GraphicsCommandList* cmd); + void transitionToSRV(ID3D12GraphicsCommandList* cmd); + +private: + 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..201269d --- /dev/null +++ b/src/mods/foveated/StereoEmulator.cpp @@ -0,0 +1,115 @@ +#include "StereoEmulator.hpp" + +#include + +namespace foveated { + +StereoEmulator& StereoEmulator::get() { + static StereoEmulator inst; + return inst; +} + +void StereoEmulator::initialize(runtimes::VRRuntime* runtime) { + m_runtime = runtime; + + m_views[0].type = ViewType::FOVEAL_LEFT_PRIMARY; + m_views[0].stereoPassMask = 0x1; + m_views[1].type = ViewType::FOVEAL_RIGHT_SECONDARY; + m_views[1].stereoPassMask = 0x2; + m_views[2].type = ViewType::PERIPHERAL_LEFT_PRIMARY; + m_views[2].stereoPassMask = 0x1; + m_views[3].type = ViewType::PERIPHERAL_RIGHT_SECONDARY; + m_views[3].stereoPassMask = 0x2; +} + +void StereoEmulator::configureFOV(float fovealDeg, float peripheralDeg) { + m_fovealFov = fovealDeg; + m_peripheralFov = peripheralDeg; + m_fovealScale = glm::tan(glm::radians(fovealDeg * 0.5f)); + m_peripheralScale = glm::tan(glm::radians(peripheralDeg * 0.5f)); +} + +void StereoEmulator::beginFrame(int frameIndex) { + if (m_runtime == nullptr) { + m_stereoActive = false; + return; + } + + m_stereoActive = m_runtime->ready(); + if (!m_stereoActive) { + return; + } + + buildViewMatrices(frameIndex); +} + +std::array StereoEmulator::computeAtlasViewports(uint32_t width, uint32_t height) const { + std::array result{}; + + const float halfWidth = static_cast(width) * 0.5f; + const float fovealHeight = static_cast(height) * 0.5f; + const float peripheralHeight = static_cast(height) - fovealHeight; + + // Top row: foveal + result[0] = {0.0f, 0.0f, halfWidth, fovealHeight, 0.0f, 1.0f}; + result[1] = {halfWidth, 0.0f, halfWidth, fovealHeight, 0.0f, 1.0f}; + + // Bottom row: peripheral + result[2] = {0.0f, fovealHeight, halfWidth, peripheralHeight, 0.0f, 1.0f}; + result[3] = {halfWidth, fovealHeight, halfWidth, peripheralHeight, 0.0f, 1.0f}; + + return result; +} + +void StereoEmulator::computeFrustumPlanes(EmulatedView& v) { + // Simple placeholder extraction assuming column-major matrices + const auto m = v.projection * v.view; + // Left + v.frustumPlanes[0] = glm::vec4(m[0][3] + m[0][0], m[1][3] + m[1][0], m[2][3] + m[2][0], m[3][3] + m[3][0]); + // Right + v.frustumPlanes[1] = glm::vec4(m[0][3] - m[0][0], m[1][3] - m[1][0], m[2][3] - m[2][0], m[3][3] - m[3][0]); + // Bottom + v.frustumPlanes[2] = glm::vec4(m[0][3] + m[0][1], m[1][3] + m[1][1], m[2][3] + m[2][1], m[3][3] + m[3][1]); + // Top + v.frustumPlanes[3] = glm::vec4(m[0][3] - m[0][1], m[1][3] - m[1][1], m[2][3] - m[2][1], m[3][3] - m[3][1]); + // Near + v.frustumPlanes[4] = glm::vec4(m[0][2], m[1][2], m[2][2], m[3][2]); + // Far + v.frustumPlanes[5] = glm::vec4(m[0][3] - m[0][2], m[1][3] - m[1][2], m[2][3] - m[2][2], m[3][3] - m[3][2]); +} + +void StereoEmulator::buildViewMatrices(int) { + // Basic left/right eye offsets derived from IPD; peripheral scaled by peripheral scale + const float ipd = m_runtime ? m_runtime->get_ipd() : 0.064f; + const float halfIpd = ipd * 0.5f; + + const glm::vec3 leftOffset{-halfIpd, 0.0f, 0.0f}; + const glm::vec3 rightOffset{halfIpd, 0.0f, 0.0f}; + + m_views[0].view = glm::translate(glm::mat4{1.0f}, -leftOffset); + m_views[1].view = glm::translate(glm::mat4{1.0f}, -rightOffset); + m_views[2].view = glm::translate(glm::mat4{1.0f}, -leftOffset); + m_views[3].view = glm::translate(glm::mat4{1.0f}, -rightOffset); + + const float nearZ = 0.05f; + const float farZ = 1000.0f; + + const auto projFoveal = glm::perspective(glm::radians(m_fovealFov), 1.0f, nearZ, farZ); + const auto projPeripheral = glm::perspective(glm::radians(m_peripheralFov), 1.0f, nearZ, farZ); + + m_views[0].projection = projFoveal; + m_views[1].projection = projFoveal; + m_views[2].projection = projPeripheral; + m_views[3].projection = projPeripheral; + + m_views[0].fovScale = m_fovealScale; + m_views[1].fovScale = m_fovealScale; + m_views[2].fovScale = m_peripheralScale; + m_views[3].fovScale = m_peripheralScale; + + for (auto& v : m_views) { + computeFrustumPlanes(v); + } +} + +} // namespace foveated diff --git a/src/mods/foveated/StereoEmulator.hpp b/src/mods/foveated/StereoEmulator.hpp new file mode 100644 index 0000000..36510ba --- /dev/null +++ b/src/mods/foveated/StereoEmulator.hpp @@ -0,0 +1,54 @@ +#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{0}; +}; + +class StereoEmulator { +public: + static StereoEmulator& get(); + + void initialize(runtimes::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 width, uint32_t height) const; + void configureFOV(float fovealDeg, float peripheralDeg); + +private: + void buildViewMatrices(int frame); + void computeFrustumPlanes(EmulatedView& v); + + std::array m_views{}; + runtimes::VRRuntime* m_runtime{nullptr}; + bool m_stereoActive{false}; + float m_fovealFov{40.f}; + float m_peripheralFov{110.f}; + float m_fovealScale{1.f}; + 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..63469e6 --- /dev/null +++ b/src/mods/foveated/ViewInjector.cpp @@ -0,0 +1,94 @@ +#include "ViewInjector.hpp" + +#include + +#include "D3D12Hook.hpp" +#include "FoveatedAtlas.hpp" + +namespace foveated { + +ViewInjector& ViewInjector::get() { + static ViewInjector inst; + return inst; +} + +void ViewInjector::install(D3D12Hook* hook) { + if (hook == nullptr) { + return; + } + m_hook = hook; + + hook->on_set_viewports([this](D3D12Hook&, ID3D12GraphicsCommandList5* cmd, UINT num, const D3D12_VIEWPORT* vps) { + onSetViewports(cmd, num, vps); + }); + + hook->on_set_render_targets([this](D3D12Hook&, ID3D12GraphicsCommandList5* cmd, UINT num, + const D3D12_CPU_DESCRIPTOR_HANDLE* rtvs, BOOL single, D3D12_CPU_DESCRIPTOR_HANDLE* dsv) { + onSetRenderTargets(cmd, num, rtvs, single, dsv); + }); +} + +void ViewInjector::setAtlasRT(ID3D12Resource* rt, D3D12_CPU_DESCRIPTOR_HANDLE rtv) { + m_atlasRT = rt; + m_atlasRTV = rtv; + if (m_atlasRT) { + auto desc = m_atlasRT->GetDesc(); + m_atlasViewports = StereoEmulator::get().computeAtlasViewports(static_cast(desc.Width), desc.Height); + } +} + +void ViewInjector::onSetViewports(ID3D12GraphicsCommandList5* cmd, UINT num, const D3D12_VIEWPORT* vps) { + if (!cmd) { + return; + } + + const auto& stereo = StereoEmulator::get(); + if (m_atlasRT && stereo.isStereoActive()) { + cmd->RSSetViewports(static_cast(m_atlasViewports.size()), m_atlasViewports.data()); + return; + } + + if (vps && num > 0) { + cmd->RSSetViewports(num, vps); + } +} + +void ViewInjector::onSetRenderTargets(ID3D12GraphicsCommandList5* cmd, UINT num, + const D3D12_CPU_DESCRIPTOR_HANDLE* rtvs, BOOL single, D3D12_CPU_DESCRIPTOR_HANDLE* dsv) { + if (!cmd) { + return; + } + + const auto& stereo = StereoEmulator::get(); + if (m_atlasRT && stereo.isStereoActive()) { + // Ensure atlas is bound + cmd->OMSetRenderTargets(1, &m_atlasRTV, FALSE, dsv); + if (!m_isRenderingFoveal) { + injectFovealPair(cmd); + } else { + injectPeripheralPair(cmd); + } + m_isRenderingFoveal = !m_isRenderingFoveal; + return; + } + + if (rtvs && num > 0) { + cmd->OMSetRenderTargets(num, rtvs, single, dsv); + } +} + +void ViewInjector::injectFovealPair(ID3D12GraphicsCommandList5* cmd) { + if (!cmd) { + return; + } + cmd->RSSetViewports(2, m_atlasViewports.data()); +} + +void ViewInjector::injectPeripheralPair(ID3D12GraphicsCommandList5* cmd) { + if (!cmd) { + return; + } + cmd->RSSetViewports(2, m_atlasViewports.data() + 2); +} + +} // namespace foveated diff --git a/src/mods/foveated/ViewInjector.hpp b/src/mods/foveated/ViewInjector.hpp new file mode 100644 index 0000000..e8b013f --- /dev/null +++ b/src/mods/foveated/ViewInjector.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#include "StereoEmulator.hpp" + +class D3D12Hook; + +namespace foveated { + +class ViewInjector { +public: + static ViewInjector& get(); + + void install(D3D12Hook* hook); + void setAtlasRT(ID3D12Resource* rt, D3D12_CPU_DESCRIPTOR_HANDLE rtv); + +private: + 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..6812775 --- /dev/null +++ b/src/mods/foveated/VisibilityCache.cpp @@ -0,0 +1,73 @@ +#include "VisibilityCache.hpp" + +#include +#include + +namespace foveated { + +VisibilityCache& VisibilityCache::get() { + static VisibilityCache inst; + return inst; +} + +void VisibilityCache::initialize(uint32_t maxPrimitives) { + std::unique_lock lock{m_mtx}; + const size_t bytes = (maxPrimitives + 7u) / 8u; + for (auto& frame : m_frames) { + for (auto& mask : frame.masks) { + mask.resize(bytes); + } + frame.valid.fill(false); + } +} + +void VisibilityCache::beginFrame(int frameIdx) { + std::unique_lock lock{m_mtx}; + m_current = frameIdx % static_cast(FRAMES); + m_frames[m_current].valid.fill(false); +} + +void VisibilityCache::recordVisibility(uint32_t viewIdx, const uint8_t* bits, size_t count) { + if (viewIdx >= VIEWS) { + return; + } + std::unique_lock lock{m_mtx}; + auto& frame = m_frames[m_current]; + auto& mask = frame.masks[viewIdx]; + if (mask.size() < count) { + mask.resize(count); + } + std::memcpy(mask.data(), bits, count); + 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] || frame.masks[viewIdx].empty()) { + return nullptr; + } + return frame.masks[viewIdx].data(); +} + +bool VisibilityCache::canShare(uint32_t src, uint32_t dst) const { + return (src == 0 && dst == 2) || (src == 1 && dst == 3); +} + +void VisibilityCache::copyVisibility(uint32_t src, uint32_t dst) { + if (src >= VIEWS || dst >= VIEWS || !canShare(src, dst)) { + return; + } + std::unique_lock lock{m_mtx}; + auto& frame = m_frames[m_current]; + if (!frame.valid[src]) { + return; + } + frame.masks[dst] = frame.masks[src]; + frame.valid[dst] = true; +} + +} // namespace foveated diff --git a/src/mods/foveated/VisibilityCache.hpp b/src/mods/foveated/VisibilityCache.hpp new file mode 100644 index 0000000..879118b --- /dev/null +++ b/src/mods/foveated/VisibilityCache.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include + +namespace foveated { + +class VisibilityCache { +public: + static VisibilityCache& get(); + + 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; + bool canShare(uint32_t src, uint32_t dst) const; // 0->2, 1->3 + void copyVisibility(uint32_t src, uint32_t dst); + +private: + 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}; + mutable std::shared_mutex m_mtx; +}; + +} // namespace foveated From a95a9c7e25dbcb914345317a48c0966f8ed50ad3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:27:51 +0000 Subject: [PATCH 04/10] chore: address review feedback Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/games/ExampleUE/memory/offsets.h | 1 + src/mods/foveated/FoveatedAtlas.cpp | 3 +-- src/mods/foveated/VisibilityCache.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/games/ExampleUE/memory/offsets.h b/src/games/ExampleUE/memory/offsets.h index 65b82df..82027be 100644 --- a/src/games/ExampleUE/memory/offsets.h +++ b/src/games/ExampleUE/memory/offsets.h @@ -3,6 +3,7 @@ #include "memory/memory_mul.h" namespace memory { + // Placeholder ExampleUE signatures; replace with game-specific patterns/offsets when integrating. inline uintptr_t beginFrameAddr() { return FuncRelocation("BeginFrame", "48 89 5C 24 ? 57 48 83 EC 20 48 8B D9 E8", 0x0); } diff --git a/src/mods/foveated/FoveatedAtlas.cpp b/src/mods/foveated/FoveatedAtlas.cpp index 9b6937d..317301c 100644 --- a/src/mods/foveated/FoveatedAtlas.cpp +++ b/src/mods/foveated/FoveatedAtlas.cpp @@ -58,8 +58,7 @@ bool FoveatedAtlas::initialize(ID3D12Device* dev, const AtlasConfig& cfg) { D3D12_DESCRIPTOR_HEAP_DESC dsvDesc{}; dsvDesc.NumDescriptors = 1; dsvDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; - dev->CreateDescriptorHeap(&dsvDesc, IID_PPV_ARGS(&m_dsvHeap)); - if (m_dsvHeap) { + if (SUCCEEDED(dev->CreateDescriptorHeap(&dsvDesc, IID_PPV_ARGS(&m_dsvHeap)))) { D3D12_RESOURCE_DESC depthDesc = texDesc; depthDesc.Format = DXGI_FORMAT_D32_FLOAT; depthDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; diff --git a/src/mods/foveated/VisibilityCache.cpp b/src/mods/foveated/VisibilityCache.cpp index 6812775..4016f46 100644 --- a/src/mods/foveated/VisibilityCache.cpp +++ b/src/mods/foveated/VisibilityCache.cpp @@ -23,7 +23,7 @@ void VisibilityCache::initialize(uint32_t maxPrimitives) { void VisibilityCache::beginFrame(int frameIdx) { std::unique_lock lock{m_mtx}; - m_current = frameIdx % static_cast(FRAMES); + m_current = static_cast(frameIdx % FRAMES); m_frames[m_current].valid.fill(false); } From c9d25d702b5dfbcf80d7e8199ede906856676090 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:28:27 +0000 Subject: [PATCH 05/10] chore: sanitize frame index wraparound Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/mods/foveated/VisibilityCache.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mods/foveated/VisibilityCache.cpp b/src/mods/foveated/VisibilityCache.cpp index 4016f46..022c82b 100644 --- a/src/mods/foveated/VisibilityCache.cpp +++ b/src/mods/foveated/VisibilityCache.cpp @@ -23,7 +23,8 @@ void VisibilityCache::initialize(uint32_t maxPrimitives) { void VisibilityCache::beginFrame(int frameIdx) { std::unique_lock lock{m_mtx}; - m_current = static_cast(frameIdx % FRAMES); + const auto wrapped = static_cast(frameIdx); + m_current = static_cast(wrapped % FRAMES); m_frames[m_current].valid.fill(false); } From 95f5ea8e5f0b473a813cbd2ada8ac3e4d056e133 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:29:05 +0000 Subject: [PATCH 06/10] fix: include imgui for ExampleUE entry Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/games/ExampleUE/ExampleUEEntry.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/games/ExampleUE/ExampleUEEntry.cpp b/src/games/ExampleUE/ExampleUEEntry.cpp index 956b4ff..86573e0 100644 --- a/src/games/ExampleUE/ExampleUEEntry.cpp +++ b/src/games/ExampleUE/ExampleUEEntry.cpp @@ -2,6 +2,7 @@ #include "ExampleUECameraModule.h" #include "ExampleUERendererModule.h" +#include std::optional ExampleUEEntry::on_initialize() { ExampleUERendererModule::get()->installHooks(); From 385115240f2423c15bfb5d8dbfe7c555fa19bca4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:30:07 +0000 Subject: [PATCH 07/10] chore: tidy reviewer nits Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/games/ExampleUE/ExampleUECameraModule.cpp | 4 ++-- src/games/ExampleUE/ExampleUERendererModule.cpp | 4 ++-- src/games/ExampleUE/memory/offsets.h | 4 ++-- src/mods/foveated/VisibilityCache.cpp | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/games/ExampleUE/ExampleUECameraModule.cpp b/src/games/ExampleUE/ExampleUECameraModule.cpp index 438864c..e537739 100644 --- a/src/games/ExampleUE/ExampleUECameraModule.cpp +++ b/src/games/ExampleUE/ExampleUECameraModule.cpp @@ -6,8 +6,8 @@ #include ExampleUECameraModule* ExampleUECameraModule::get() { - static auto inst = new ExampleUECameraModule(); - return inst; + static ExampleUECameraModule inst; + return &inst; } void ExampleUECameraModule::installHooks() { diff --git a/src/games/ExampleUE/ExampleUERendererModule.cpp b/src/games/ExampleUE/ExampleUERendererModule.cpp index be09b5c..315367b 100644 --- a/src/games/ExampleUE/ExampleUERendererModule.cpp +++ b/src/games/ExampleUE/ExampleUERendererModule.cpp @@ -5,8 +5,8 @@ #include ExampleUERendererModule* ExampleUERendererModule::get() { - static auto inst = new ExampleUERendererModule(); - return inst; + static ExampleUERendererModule inst; + return &inst; } void ExampleUERendererModule::installHooks() { diff --git a/src/games/ExampleUE/memory/offsets.h b/src/games/ExampleUE/memory/offsets.h index 82027be..2bb499e 100644 --- a/src/games/ExampleUE/memory/offsets.h +++ b/src/games/ExampleUE/memory/offsets.h @@ -5,10 +5,10 @@ namespace memory { // Placeholder ExampleUE signatures; replace with game-specific patterns/offsets when integrating. inline uintptr_t beginFrameAddr() { - return FuncRelocation("BeginFrame", "48 89 5C 24 ? 57 48 83 EC 20 48 8B D9 E8", 0x0); + return FuncRelocation("BeginFrame", "48 89 5C 24 ? 57 48 83 EC 20 48 8B D9 E8", 0x0); } inline uintptr_t beginRenderAddr() { - return FuncRelocation("BeginRender", "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 30", 0x0); + return FuncRelocation("BeginRender", "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 30", 0x0); } inline uintptr_t calcViewAddr() { return FuncRelocation("CalcView", "40 53 48 83 EC 40 48 8B DA 48 8B D1", 0x0); diff --git a/src/mods/foveated/VisibilityCache.cpp b/src/mods/foveated/VisibilityCache.cpp index 022c82b..be2ac16 100644 --- a/src/mods/foveated/VisibilityCache.cpp +++ b/src/mods/foveated/VisibilityCache.cpp @@ -24,7 +24,7 @@ void VisibilityCache::initialize(uint32_t maxPrimitives) { void VisibilityCache::beginFrame(int frameIdx) { std::unique_lock lock{m_mtx}; const auto wrapped = static_cast(frameIdx); - m_current = static_cast(wrapped % FRAMES); + m_current = static_cast(wrapped % static_cast(FRAMES)); m_frames[m_current].valid.fill(false); } From a4b7fe8640d46db503cd16f26f85a64c914e158b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:31:00 +0000 Subject: [PATCH 08/10] chore: add safety checks and lambda annotations Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/mods/foveated/ViewInjector.cpp | 4 ++-- src/mods/foveated/VisibilityCache.cpp | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/mods/foveated/ViewInjector.cpp b/src/mods/foveated/ViewInjector.cpp index 63469e6..f6c58e0 100644 --- a/src/mods/foveated/ViewInjector.cpp +++ b/src/mods/foveated/ViewInjector.cpp @@ -18,11 +18,11 @@ void ViewInjector::install(D3D12Hook* hook) { } m_hook = hook; - hook->on_set_viewports([this](D3D12Hook&, ID3D12GraphicsCommandList5* cmd, UINT num, const D3D12_VIEWPORT* vps) { + hook->on_set_viewports([this]([[maybe_unused]] D3D12Hook& hookRef, ID3D12GraphicsCommandList5* cmd, UINT num, const D3D12_VIEWPORT* vps) { onSetViewports(cmd, num, vps); }); - hook->on_set_render_targets([this](D3D12Hook&, ID3D12GraphicsCommandList5* cmd, UINT num, + hook->on_set_render_targets([this]([[maybe_unused]] D3D12Hook& hookRef, ID3D12GraphicsCommandList5* cmd, UINT num, const D3D12_CPU_DESCRIPTOR_HANDLE* rtvs, BOOL single, D3D12_CPU_DESCRIPTOR_HANDLE* dsv) { onSetRenderTargets(cmd, num, rtvs, single, dsv); }); diff --git a/src/mods/foveated/VisibilityCache.cpp b/src/mods/foveated/VisibilityCache.cpp index be2ac16..5e78597 100644 --- a/src/mods/foveated/VisibilityCache.cpp +++ b/src/mods/foveated/VisibilityCache.cpp @@ -32,6 +32,9 @@ void VisibilityCache::recordVisibility(uint32_t viewIdx, const uint8_t* bits, si if (viewIdx >= VIEWS) { return; } + if (bits == nullptr || count == 0) { + return; + } std::unique_lock lock{m_mtx}; auto& frame = m_frames[m_current]; auto& mask = frame.masks[viewIdx]; From 62646f5c9ae589c81ac22811ce853c9abf7f6185 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:32:36 +0000 Subject: [PATCH 09/10] chore: normalize sdk header naming Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/games/ExampleUE/ExampleUECameraModule.cpp | 2 +- src/games/ExampleUE/sdk/{ExampleUESDK.h => ExampleUESDK.hpp} | 0 src/mods/foveated/VisibilityCache.cpp | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/games/ExampleUE/sdk/{ExampleUESDK.h => ExampleUESDK.hpp} (100%) diff --git a/src/games/ExampleUE/ExampleUECameraModule.cpp b/src/games/ExampleUE/ExampleUECameraModule.cpp index e537739..00a3eb8 100644 --- a/src/games/ExampleUE/ExampleUECameraModule.cpp +++ b/src/games/ExampleUE/ExampleUECameraModule.cpp @@ -2,7 +2,7 @@ #include "memory/offsets.h" #include "aer/ConstantsPool.h" -#include "sdk/ExampleUESDK.h" +#include "sdk/ExampleUESDK.hpp" #include ExampleUECameraModule* ExampleUECameraModule::get() { diff --git a/src/games/ExampleUE/sdk/ExampleUESDK.h b/src/games/ExampleUE/sdk/ExampleUESDK.hpp similarity index 100% rename from src/games/ExampleUE/sdk/ExampleUESDK.h rename to src/games/ExampleUE/sdk/ExampleUESDK.hpp diff --git a/src/mods/foveated/VisibilityCache.cpp b/src/mods/foveated/VisibilityCache.cpp index 5e78597..9f3d45d 100644 --- a/src/mods/foveated/VisibilityCache.cpp +++ b/src/mods/foveated/VisibilityCache.cpp @@ -23,8 +23,8 @@ void VisibilityCache::initialize(uint32_t maxPrimitives) { void VisibilityCache::beginFrame(int frameIdx) { std::unique_lock lock{m_mtx}; - const auto wrapped = static_cast(frameIdx); - m_current = static_cast(wrapped % static_cast(FRAMES)); + const int frames = static_cast(FRAMES); + m_current = ((frameIdx % frames) + frames) % frames; m_frames[m_current].valid.fill(false); } From e1426ebd86a878c2ca13dc2c7f17502d697e87f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:51:43 +0000 Subject: [PATCH 10/10] fix: move ue frame counter to begin frame Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/games/ExampleUE/ExampleUERendererModule.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/games/ExampleUE/ExampleUERendererModule.cpp b/src/games/ExampleUE/ExampleUERendererModule.cpp index 315367b..17136c8 100644 --- a/src/games/ExampleUE/ExampleUERendererModule.cpp +++ b/src/games/ExampleUE/ExampleUERendererModule.cpp @@ -23,6 +23,10 @@ void ExampleUERendererModule::installHooks() { uintptr_t ExampleUERendererModule::onBeginFrame() { auto inst = get(); + if (g_framework && g_framework->is_ready()) { + auto vr = VR::get(); + vr->m_engine_frame_count++; + } return inst->m_beginFrameHook.call(); } @@ -30,7 +34,6 @@ uintptr_t ExampleUERendererModule::onBeginRender(void* ctx) { auto inst = get(); if (g_framework && 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;