Skip to content

Texture streaming#34

Open
jiayev wants to merge 10 commits into
Gistix:mainfrom
jiayev:texturestreaming
Open

Texture streaming#34
jiayev wants to merge 10 commits into
Gistix:mainfrom
jiayev:texturestreaming

Conversation

@jiayev

@jiayev jiayev commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added texture memory statistics logging capability.
    • Introduced texture streaming with configurable modes (Off, Conservative, Balanced, Aggressive) and memory budget controls.
    • Implemented asynchronous texture uploads for improved performance.
  • Bug Fixes

    • Fixed a code structure issue in Material handling.
  • Chores

    • Updated external library reference.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds configurable texture streaming via a TextureStreamingMode enum and budget settings in ExperimentalSettings. CreateTextureAndSRV::thunk now computes a streamingMipBias and allocates only resident mip levels and dimensions for both D3D12 and D3D11 paths, replacing synchronous upload waits with event-query-based async retirement. TextureManager gains per-texture resident mip offset tracking, metadata fields on TextureReference, and a LogMemoryStats() method exposed through a new LogTextureMemoryStats() C API.

Changes

Texture Streaming & Async Upload

Layer / File(s) Summary
Streaming settings contract
src/Types/Settings.h
Introduces TextureStreamingMode enum (Off, Conservative, Balanced, Aggressive) and adds TextureStreamingMode, TextureBudgetMB, TextureMaxMipBias fields to ExperimentalSettings.
TextureReference metadata and resident mip tracking
src/Core/TextureManager.h, src/Core/TextureManager.cpp
Extends TextureReference with dimension, mip, format, and residentMipOffset fields. Adds a global mutex-protected map, RegisterResidentMipOffset, LogMemoryStats, and wires the offset through GetDescriptor into TextureReference construction. Sets MipLevels from the native D3D12 resource desc.
CreateTextureAndSRV streaming logic and async upload
src/Hooks.cpp
Adds format classification helpers, mip extent calculation, and selectStreamingMipBias in an unnamed namespace. Applies resident dimensions/mip counts to D3D12 and D3D11 texture descriptions, uploads only resident mips at the bias offset, registers mip offsets with TextureManager, and replaces waitForIdle with event-query-based async upload retirement via a mutex-protected pending queue.
LogTextureMemoryStats public API
src/API.h, src/API.cpp
Declares and implements CERT_API LogTextureMemoryStats(), which walks Scene→SceneGraph→TextureManager with null guards and calls LogMemoryStats().
Minor fixes and submodule update
src/Core/MaterialBase.cpp, src/Core/Texture.h, extern/RTXDI-Library, AGENTS.md
Fixes missing closing brace in MaterialBase::GetTexture, updates Texture struct terminator, bumps the RTXDI-Library submodule commit, and removes trailing lines from AGENTS.md.

Sequence Diagram

sequenceDiagram
  participant Game as Game Engine
  participant Hook as CreateTextureAndSRV::thunk
  participant TM as TextureManager
  participant D3D12 as D3D12 Device
  participant Queue as PendingUploadQueue
  participant API as LogTextureMemoryStats API

  rect rgba(100, 149, 237, 0.5)
    note over Hook,D3D12: Streaming texture creation
    Game->>Hook: CreateTextureAndSRV(data, desc)
    Hook->>Hook: selectStreamingMipBias(ExperimentalSettings, dims)
    Hook->>D3D12: CreateCommittedResource(residentMips, residentDims)
    Hook->>TM: RegisterResidentMipOffset(resource, mipBias)
    Hook->>D3D12: upload resident mips from a_data[mipBias offset]
    Hook->>D3D12: createEventQuery + executeCommandList
    Hook->>Queue: push(cmdList, eventQuery) for async retirement
  end

  rect rgba(144, 238, 144, 0.5)
    note over TM,API: Descriptor registration and stats logging
    Hook->>TM: GetDescriptor(d3d11res, d3d12res, type)
    TM->>TM: lookup+erase residentMipOffset from map
    TM->>TM: construct TextureReference(texture, descMgr, residentMipOffset)
    Game->>API: LogTextureMemoryStats()
    API->>TM: LogMemoryStats()
    TM-->>API: logger::info(streamed vs standard counts/sizes)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hop hop, the mips now stream with grace,
Resident slices fill the VRAM space.
Event queries watch uploads retire,
No idle waits to slow the fire.
LogMemoryStats counts every byte—
This bunny's texture pipeline is tight! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Texture streaming' accurately summarizes the main objective of the changeset, which introduces texture streaming support throughout the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Core/TextureManager.cpp`:
- Around line 97-103: The standardBytes counter is accumulating all textures
without excluding streamed ones, so streamed textures are being counted in both
standard and streamed categories. Modify the code to only add to standardBytes
when the texture is not a streamed texture by adding a condition that excludes
textures where residentMipOffset is greater than 0. This will ensure the
standard and streamed byte counts are mutually exclusive and the log breakdown
is accurate. Apply the same fix to the similar code block referenced at lines
112-119.
- Around line 24-31: The RegisterResidentMipOffset function in
TextureManager.cpp stores raw IUnknown pointers as keys in the
g_ResidentMipOffsets map, but these entries are only removed when GetDescriptor
consumes them. If a registered resource is destroyed without reaching descriptor
creation, the stale pointer key persists in the map. When another COM object
later reuses the same memory address, the old entry will incorrectly apply a
stale mip offset to the new object. Implement proper cleanup by registering a
release callback or using COM object lifecycle tracking (such as implementing
IUnknown::Release wrapping or using weak_ptr alternatives) to remove entries
from g_ResidentMipOffsets when resources are actually destroyed, rather than
deferring cleanup until descriptor consumption.

In `@src/Hooks.cpp`:
- Around line 113-155: The GetTextureStreamingMipBias function determines
texture streaming bias but never uses the TextureBudgetMB setting from
ExperimentalSettings, making the memory budget configuration ineffective.
Integrate TextureBudgetMB into the bias calculation by computing an estimated
memory footprint for the texture at different mip levels, then adjust the
desiredBias upward as needed to keep memory consumption within the configured
budget limit. This adjustment should happen after the initial bias is determined
by TextureStreamingMode but before the final clamping operations, ensuring the
budget constraint overrides mode-based decisions when memory limits would be
exceeded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a30ea6a0-e526-4e30-869d-897c421e9496

📥 Commits

Reviewing files that changed from the base of the PR and between ba5b864 and 37db002.

📒 Files selected for processing (10)
  • AGENTS.md
  • extern/RTXDI-Library
  • src/API.cpp
  • src/API.h
  • src/Core/MaterialBase.cpp
  • src/Core/Texture.h
  • src/Core/TextureManager.cpp
  • src/Core/TextureManager.h
  • src/Hooks.cpp
  • src/Types/Settings.h
💤 Files with no reviewable changes (1)
  • AGENTS.md

Comment on lines +24 to 31
void TextureManager::RegisterResidentMipOffset(IUnknown* resource, uint32_t mipOffset)
{
if (!resource || mipOffset == 0)
return;

std::scoped_lock lock(g_ResidentMipOffsetsMutex);
g_ResidentMipOffsets[resource] = mipOffset;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resident mip-offset registry can retain stale pointer entries.

Offsets are only removed on GetDescriptor consumption. If a registered resource never reaches descriptor creation, the raw-pointer key persists and can later be reused by another COM object, applying an incorrect mip offset.

Suggested mitigation
 void TextureManager::ReleaseTexture(RE::BSGraphics::Texture* texture)
 {
   if (!texture)
     return;

   IUnknown* key = nullptr;
@@
 `#endif`
     key = texture->texture;

+  {
+    std::scoped_lock lock(g_ResidentMipOffsetsMutex);
+    g_ResidentMipOffsets.erase(key);
+  }
+
   m_Textures.erase(key);
   m_NormalMaps.erase(key);
 }

Also applies to: 172-179

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Core/TextureManager.cpp` around lines 24 - 31, The
RegisterResidentMipOffset function in TextureManager.cpp stores raw IUnknown
pointers as keys in the g_ResidentMipOffsets map, but these entries are only
removed when GetDescriptor consumes them. If a registered resource is destroyed
without reaching descriptor creation, the stale pointer key persists in the map.
When another COM object later reuses the same memory address, the old entry will
incorrectly apply a stale mip offset to the new object. Implement proper cleanup
by registering a release callback or using COM object lifecycle tracking (such
as implementing IUnknown::Release wrapping or using weak_ptr alternatives) to
remove entries from g_ResidentMipOffsets when resources are actually destroyed,
rather than deferring cleanup until descriptor consumption.

Comment on lines +97 to +103
standardBytes += texture->size;

if (texture->residentMipOffset > 0) {
streamedTextures++;
streamedBytes += texture->size;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

standard memory in the log currently includes streamed textures.

This makes the breakdown non-exclusive and easy to misread. Either rename the label or log non-streamed bytes explicitly.

Suggested fix
- uint64_t standardBytes = 0;
+ uint64_t standardBytes = 0;
+ uint64_t nonStreamedBytes = 0;
@@
   standardBytes += texture->size;

   if (texture->residentMipOffset > 0) {
     streamedTextures++;
     streamedBytes += texture->size;
+  } else {
+    nonStreamedBytes += texture->size;
   }
@@
- "TextureManager - RT texture memory: total={} MiB, standard={} MiB ({} textures), normal={} MiB ({} textures), streamed={} MiB ({} textures)",
+ "TextureManager - RT texture memory: total={} MiB, standard={} MiB ({} textures), normal={} MiB ({} textures), streamed={} MiB ({} textures)",
   (standardBytes + normalBytes) / (1024 * 1024),
-  standardBytes / (1024 * 1024),
+  nonStreamedBytes / (1024 * 1024),

Also applies to: 112-119

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Core/TextureManager.cpp` around lines 97 - 103, The standardBytes counter
is accumulating all textures without excluding streamed ones, so streamed
textures are being counted in both standard and streamed categories. Modify the
code to only add to standardBytes when the texture is not a streamed texture by
adding a condition that excludes textures where residentMipOffset is greater
than 0. This will ensure the standard and streamed byte counts are mutually
exclusive and the log breakdown is accurate. Apply the same fix to the similar
code block referenced at lines 112-119.

Comment thread src/Hooks.cpp
Comment on lines +113 to +155
uint32_t GetTextureStreamingMipBias(const ExperimentalSettings& settings, uint32_t width, uint32_t height, uint32_t mipLevels, DXGI_FORMAT format)
{
if (settings.TextureStreamingMode == TextureStreamingMode::Off || mipLevels <= 1)
return 0;

const uint32_t maxDimension = std::max(width, height);
if (maxDimension < 1024)
return 0;

if (IsCriticalStreamingFormat(format))
return 0;

uint32_t desiredBias = 0;
const bool alphaCapable = IsAlphaCapableFormat(format);
switch (settings.TextureStreamingMode) {
case TextureStreamingMode::Conservative:
if (alphaCapable)
return 0;

desiredBias = maxDimension >= 4096 ? 1 : 0;
break;
case TextureStreamingMode::Balanced:
if (alphaCapable)
return 0;

desiredBias = maxDimension >= 4096 ? 2 : 1;
break;
case TextureStreamingMode::Aggressive:
desiredBias = maxDimension >= 4096 ? 3 : (maxDimension >= 2048 ? 2 : 1);
if (alphaCapable)
desiredBias = std::min(desiredBias, 1u);
break;
default:
break;
}

desiredBias = std::min(desiredBias, settings.TextureMaxMipBias);

if (!IsBlockCompressedFormat(format))
desiredBias = std::min(desiredBias, 1u);

return std::min(desiredBias, mipLevels - 1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

TextureBudgetMB is currently a no-op in streaming decisions.

The streaming bias path reads mode and max bias, but never uses ExperimentalSettings::TextureBudgetMB, so memory budget configuration doesn’t influence residency behavior.

Also applies to: 327-341

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Hooks.cpp` around lines 113 - 155, The GetTextureStreamingMipBias
function determines texture streaming bias but never uses the TextureBudgetMB
setting from ExperimentalSettings, making the memory budget configuration
ineffective. Integrate TextureBudgetMB into the bias calculation by computing an
estimated memory footprint for the texture at different mip levels, then adjust
the desiredBias upward as needed to keep memory consumption within the
configured budget limit. This adjustment should happen after the initial bias is
determined by TextureStreamingMode but before the final clamping operations,
ensuring the budget constraint overrides mode-based decisions when memory limits
would be exceeded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant