Skip to content

fix(unified water): stabilize LOD water during transitions#2391

Open
FIocker wants to merge 1 commit into
community-shaders:devfrom
FIocker:fix/unified-water-lod-stability
Open

fix(unified water): stabilize LOD water during transitions#2391
FIocker wants to merge 1 commit into
community-shaders:devfrom
FIocker:fix/unified-water-lod-stability

Conversation

@FIocker
Copy link
Copy Markdown
Contributor

@FIocker FIocker commented May 20, 2026

These rewrites attempt to make Unified Water a little more conservative about how generated LOD water is attached, culled, and cleaned up during cell/worldspace transitions

The main goal is to avoid cases where close water and generated LOD water briefly disagree about ownership after streaming, which can show up as flickering water, missing LOD water, or water disappearing after entering/leaving interiors. One such example the combination of UW and using the Frogs mod from Mihail's Monsters and Animals series.

Changes:

  • keep generated Unified Water tiles under the managed LOD parent after terrain attach
  • preserve valid uncovered water subtrees when rebuilding a terrain block
  • retry child-worldspace LOD culling during transition frames
  • refresh BSWaterShaderProperty planes before drawing to avoid stale water-plane state
  • ignore placed water overlay objects when pruning duplicate water-system entries
  • retain the newer duplicate cleanup and safer detach behavior from recent UW commits

This keeps the appearance closer to the earlier Unified Water builds behavior while carrying forward the useful stability work from the newer implementation (newer UW builds had tiling issues between UW water and LOD water)

Testing:

  • shader tests passed: 166 assertions
  • tested in-game with Unified Water enabled
  • verified LOD water remains visible after interior/exterior transitions
  • verified close water no longer flickers after returning from interiors
  • verified affected water areas no longer disappear after streaming in

Remaining issues found during testing:

  • None

Summary by CodeRabbit

  • Refactor
    • Enhanced water rendering system with improved culling and level-of-detail management for better performance across different world areas.
    • Optimized water handling during worldspace transitions to reduce potential visual artifacts and system strain.
    • Improved water attachment logic for terrain to ensure more stable and efficient water system behavior.

Review Change Stack

Keep generated Unified Water tiles under the feature-managed LOD parent after terrain attach instead of routing them through vanilla LOD-water ownership. This keeps the later cleanup around duplicate water entries and safer block rebuilding, while avoiding state churn that can leave close water and generated LOD water fighting after cell and worldspace transitions.

Restore the child-worldspace cull retry path so already-attached LOD blocks are culled once their replacement cells finish attaching. Also keep water shader planes refreshed before draw, preserve uncovered water subtrees when rebuilding a terrain block, and ignore placed water overlays when pruning duplicate water-system objects.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

UnifiedWater refactors water culling to defer child-worldspace transitions using atomic state, grid-aware cell testing, and terrain-block attachment rewrites. New hooks (TES_SetWorldSpace, TES_DestroySkyCell) track worldspace changes; deferred completion logic preserves uncovered water subtrees, applies unified shader flags, and recombines water via TES gridCell inspection during terrain-block rebuild and displacement-mesh updates.

Changes

Deferred Culling and Worldspace Transitions

Layer / File(s) Summary
State Tracking and Hook Registration
src/Features/UnifiedWater.h, src/Features/UnifiedWater.cpp (includes)
Header includes introduce <atomic>; new thunk structs TES_SetWorldSpace and TES_DestroySkyCell and atomic members for worldspace, pending culls, transition frames, and cached TES snapshot enable deferred state management.
Cell-Cull and Water-Object Removal Helpers
src/Features/UnifiedWater.cpp (utility functions)
Grid-aware cell-cull helper reports whether target cell is inside the grid; water-object removal traverses child trees; ancestry predicate generalizes node comparison; whitelisting helpers distinguish placed-water overlays to skip during duplicate removal.
Water Preservation and Deferred-Culling Completion Logic
src/Features/UnifiedWater.cpp (BuildWaterForBlock replacement & TryCompleteDeferredChildWorldspaceCull)
Tracks registered water objects, preserves uncovered water subtrees during tile rebuilds, applies unified shader water flags, and implements TryCompleteDeferredChildWorldspaceCull to finalize deferred culling by examining TES gridCells and recombining preserved/attached water.
Hook Installation and Worldspace Transition Entry Points
src/Features/UnifiedWater.cpp (hook installation & thunk implementations)
PostPostLoad registers TES_SetWorldSpace and TES_DestroySkyCell detours; TES_SetWorldSpace updates atomic state, schedules cull frames, enables water system, and queues/defers retries on child-worldspace entry; TES_DestroySkyCell clears state and disables pending culls.
Terrain Block Attachment and Displacement Updates
src/Features/UnifiedWater.cpp (BGSTerrainBlock_Attach, BGSTerrainBlock_Detach, TESWaterSystem_UpdateDisplacementMeshPosition)
BGSTerrainBlock_Attach runs deferred completion, rebuilds tiles with preservation/reattachment of uncovered subtrees, applies unified flags, removes duplicates, and culls newly built tiles in child worldspace; BGSTerrainBlock_Detach preserves water pointers; displacement updates add duplicate removal and deferred completion/retry decrement.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • community-shaders/skyrim-community-shaders#2019: Implements the same deferred child-worldspace water LOD culling flow—introducing TryCompleteDeferredChildWorldspaceCull, TES_SetWorldSpace/TES_DestroySkyCell hooks, and integrating retry/completion into BGSTerrainBlock_Attach and displacement-mesh updates.
  • community-shaders/skyrim-community-shaders#2367: Modifies UnifiedWater terrain-block detach/reattach and water-state cleanup around interior/exterior worldspace transitions, with focus on shader-plane stabilization; this PR expands transition/culling logic and detachment behavior.
  • community-shaders/skyrim-community-shaders#2353: Directly touches deferred child-worldspace culling and TES_SetWorldSpace / TES_DestroySkyCell hooks; removes those hooks and completion mechanism where this PR adds/expands them.

Suggested reviewers

  • alandtse
  • davo0411
  • sicsix

Poem

🐰 Water flows through worlds in curious ways,
Deferred culls softly guide the days,
Grid cells dance, subtrees stay preserved,
Shaders unified, transitions served!
One commit brings order to the tides.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(unified water): stabilize LOD water during transitions' directly aligns with the main objective: preventing water flickering/disappearance during cell/worldspace transitions through more conservative attachment and culling logic.
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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.21.0)

OpenGrep fatal error (exit code 2): [00.14][ERROR]: Error: exception Unix_error: No such file or directory stat src/Features/UnifiedWater.h
Raised by primitive operation at UTmp.replace_named_pipe_by_regular_file_if_needed in file "libs/commons/UTmp.ml", line 145, characters 8-27
Called from Scan_CLI.replace_target_roots_by_regular_files_where_needed.(fun) in file "src/osemgrep/cli_scan/Scan_CLI.ml", lines 1086-1087, characters 19-65
Called from List_.fast_map in file "libs/commons/List_.ml", line 81, characters 17-20
Called from


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.

@github-actions
Copy link
Copy Markdown

No actionable suggestions for changed features.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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/Features/UnifiedWater.cpp`:
- Around line 425-485: The whitelist currently keys by BSTriShape*
(s_placedWaterWhitelist) causing stale entries and memory-growth; change the
whitelist to key by the reference identity returned by FindRefForWaterGeometry
(e.g., store RE::TESObjectREFR* or better RE::FormID) and update
IsPlacedWaterWhitelisted and TryWhitelistPlacedWaterOverlay to use that
ref/formID instead of the raw shape pointer; specifically: change
s_placedWaterWhitelist's type, have TryWhitelistPlacedWaterOverlay call
FindRefForWaterGeometry and insert the ref's identifier (after
IsPlacedWaterCandidate check) under s_placedWaterMutex, and have
IsPlacedWaterWhitelisted lookup by the same ref identifier (or return false when
no ref found); keep IsPlacedWaterCandidate and FindRefForWaterGeometry as-is and
adjust locking/unlocking points accordingly to prevent the stale-pointer and
monotonic-growth issues.
- Around line 554-568: The deferred cull flag pendingChildWsCull is only cleared
when CullAllWaterLODParents(...) returns true, causing
TryCompleteDeferredChildWorldspaceCull to spin forever; modify
TryCompleteDeferredChildWorldspaceCull (and/or CullAllWaterLODParents) to also
clear pendingChildWsCull when there are no remaining in-grid candidates or when
a transition timeout expires: add a check after calling CullAllWaterLODParents
to detect "nothing left to cull" (e.g., zero returned candidates or an exposed
boolean from CullAllWaterLODParents) and store(false) on pendingChildWsCull, and
additionally track/reset a transitionCullFrames counter (decrement per call and
clear pendingChildWsCull when it reaches zero) so the pending state is cleared
after the transitionCullFrames timeout even if CullAllWaterLODParents never
returns success; use currentPlayerWorldSpace, cachedTes and gWaterLOD as
existing context to locate the logic to change.
- Around line 993-1059: Preserve-only water blocks never set
block->waterAttached so they hit the early return and never get reattached;
after calling PreserveUncoveredWaterSubtrees (and/or before the early return
that checks attaching and block->waterAttached) detect if any rescued.root or
any rescued.shapes contains a valid shape+form and, if so, set
block->waterAttached = true (and ensure attaching remains true) so the
subsequent loop that re-registers preserved shapes and reattaches block->water
under uw.gWaterLOD runs; update the logic around PreserveUncoveredWaterSubtrees,
preserved, block->waterAttached and the early return accordingly.
- Around line 802-803: The detour registrations for TES_SetWorldSpace and
TES_DestroySkyCell use REL::RelocationID pairs but do not provide VR relocations
while SupportsVR() returns true, so the hooks will fail on VR; fix by either (A)
switching these registrations to use REL::VariantID and supply the VR offsets
(matching the pattern used in Hooks.cpp and WetnessEffects.cpp) for
TES_SetWorldSpace and TES_DestroySkyCell, or (B) guard the detour_thunk calls
behind a runtime check that skips them when running in VR (or disable
SupportsVR() if full VR support is not provided). Update the calls referencing
TES_SetWorldSpace, TES_DestroySkyCell, and any related installation logic to use
VariantID or conditional installation so VR runs do not attempt to install
non-existent relocations.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ccfe0ba6-1537-4fb2-bb52-8c1d0bffa6cb

📥 Commits

Reviewing files that changed from the base of the PR and between 1a64e6c and 0f6eaeb.

📒 Files selected for processing (2)
  • src/Features/UnifiedWater.cpp
  • src/Features/UnifiedWater.h

Comment on lines +425 to 485
static std::shared_mutex s_placedWaterMutex;
static std::unordered_set<RE::BSTriShape*> s_placedWaterWhitelist;

if (!attaching || !block->waterAttached)
static bool IsTemporaryFormID(RE::FormID formID)
{
return (formID & 0xFF000000) == 0xFF000000;
}

static bool IsPlacedWaterCandidate(const RE::TESObjectREFR* ref)
{
const auto base = ref ? ref->GetBaseObject() : nullptr;
if (!base)
return false;

for (auto& [shape, instruction] : built) {
AddLODWater(waterSystem, shape, worldSpace, *gWaterLOD, block->water);
// Procedural close water can resolve through FindReferenceFor3D as an Activator
// with temporary FF... forms. Those are real replacement water surfaces and must
// still be allowed to cull UW LOD, otherwise interior/exterior flicker returns.
if (IsTemporaryFormID(ref->formID) || IsTemporaryFormID(base->formID))
return false;

if (const auto prop = shape->GetGeometryRuntimeData().shaderProperty.get(); prop && prop->GetRTTI() == globals::rtti::BSWaterShaderPropertyRTTI.get()) {
const auto waterShaderProp = static_cast<RE::BSWaterShaderProperty*>(prop);
REX::EnumSet waterFlags = static_cast<RE::BSWaterShaderProperty::WaterFlag>(0b10000100);
waterFlags |= RE::BSWaterShaderProperty::WaterFlag::kUseCubemapReflections;
waterFlags |= RE::BSWaterShaderProperty::WaterFlag::kUseReflections;
if (instruction->form.ptr->flags.any(RE::TESWaterForm::Flag::kEnableFlowmap))
waterFlags |= RE::BSWaterShaderProperty::WaterFlag::kEnableFlowmap;
if (instruction->form.ptr->flags.any(RE::TESWaterForm::Flag::kBlendNormals))
waterFlags |= RE::BSWaterShaderProperty::WaterFlag::kBlendNormals;
waterShaderProp->waterFlags = waterFlags;
}
const auto formType = base->GetFormType();
return formType == RE::FormType::Activator || formType == RE::FormType::Static;
}

static bool IsPlacedWaterWhitelisted(const RE::BSTriShape* shape)
{
if (!shape)
return false;

// Vanilla AddLODWater routes through TESWaterSystem::AddWater and attaches
// the water parent to gWaterLOD. Use the matching vanilla LOD remove wrapper
// to unwind the water-system side state, then reattach the parent once below.
RemoveLODWater(waterSystem, shape, *gWaterLOD);
std::shared_lock lock(s_placedWaterMutex);
return s_placedWaterWhitelist.contains(const_cast<RE::BSTriShape*>(shape));
}

static RE::TESObjectREFR* FindRefForWaterGeometry(RE::NiAVObject* object)
{
for (auto current = object; current; current = current->parent) {
if (const auto ref = RE::TESObjectREFR::FindReferenceFor3D(current))
return ref;
}

RemoveDuplicateWaterSystemObjects(waterSystem, *gWaterLOD);
DetachAllChildOccurrences(*gWaterLOD, block->water);
(*gWaterLOD)->AttachChild(block->water, true);
waterSystem->Enable();
return nullptr;
}

static bool TryWhitelistPlacedWaterOverlay(RE::BSTriShape* shape, const char* source)
{
if (!shape)
return false;

if (IsPlacedWaterWhitelisted(shape))
return true;

const auto ref = FindRefForWaterGeometry(shape);
if (!ref || !IsPlacedWaterCandidate(ref))
return false;

(void)source;
std::unique_lock lock(s_placedWaterMutex);
s_placedWaterWhitelist.insert(shape);

return true;
}
Copy link
Copy Markdown
Contributor

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

Key the placed-water whitelist by ref identity, not BSTriShape*.

s_placedWaterWhitelist only ever inserts raw shape pointers and never removes them. After enough streaming churn, allocator reuse can make an unrelated water shape inherit an old whitelist entry, which will incorrectly skip duplicate pruning and HasRegisteredWaterObjectInCell() replacement checks. The set also grows monotonically for the lifetime of the process.

🤖 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/Features/UnifiedWater.cpp` around lines 425 - 485, The whitelist
currently keys by BSTriShape* (s_placedWaterWhitelist) causing stale entries and
memory-growth; change the whitelist to key by the reference identity returned by
FindRefForWaterGeometry (e.g., store RE::TESObjectREFR* or better RE::FormID)
and update IsPlacedWaterWhitelisted and TryWhitelistPlacedWaterOverlay to use
that ref/formID instead of the raw shape pointer; specifically: change
s_placedWaterWhitelist's type, have TryWhitelistPlacedWaterOverlay call
FindRefForWaterGeometry and insert the ref's identifier (after
IsPlacedWaterCandidate check) under s_placedWaterMutex, and have
IsPlacedWaterWhitelisted lookup by the same ref identifier (or return false when
no ref found); keep IsPlacedWaterCandidate and FindRefForWaterGeometry as-is and
adjust locking/unlocking points accordingly to prevent the stale-pointer and
monotonic-growth issues.

Comment on lines +554 to +568
void UnifiedWater::TryCompleteDeferredChildWorldspaceCull(RE::TES* tes)
{
if (!pendingChildWsCull.load(std::memory_order_acquire) ||
!IsChildWorldSpace(currentPlayerWorldSpace.load(std::memory_order_acquire)) ||
!gWaterLOD || !*gWaterLOD)
return;

if (!tes)
tes = cachedTes.load(std::memory_order_acquire);
if (!tes || !tes->gridCells)
return;

if (CullAllWaterLODParents(*gWaterLOD, tes))
pendingChildWsCull.store(false, std::memory_order_release);
}
Copy link
Copy Markdown
Contributor

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

Don’t leave deferred child-worldspace culling latched forever.

pendingChildWsCull only resets when CullAllWaterLODParents() reports success. In the “nothing stale left to cull” case, that never happens, so TryCompleteDeferredChildWorldspaceCull() keeps walking every LOD parent on each displacement update for the rest of the child-worldspace session. Please tie completion to transitionCullFrames or clear the pending state once no in-grid candidates remain.

🤖 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/Features/UnifiedWater.cpp` around lines 554 - 568, The deferred cull flag
pendingChildWsCull is only cleared when CullAllWaterLODParents(...) returns
true, causing TryCompleteDeferredChildWorldspaceCull to spin forever; modify
TryCompleteDeferredChildWorldspaceCull (and/or CullAllWaterLODParents) to also
clear pendingChildWsCull when there are no remaining in-grid candidates or when
a transition timeout expires: add a check after calling CullAllWaterLODParents
to detect "nothing left to cull" (e.g., zero returned candidates or an exposed
boolean from CullAllWaterLODParents) and store(false) on pendingChildWsCull, and
additionally track/reset a transitionCullFrames counter (decrement per call and
clear pendingChildWsCull when it reaches zero) so the pending state is cleared
after the transitionCullFrames timeout even if CullAllWaterLODParents never
returns success; use currentPlayerWorldSpace, cachedTes and gWaterLOD as
existing context to locate the logic to change.

Comment on lines +802 to +803
stl::detour_thunk<TES_SetWorldSpace>(REL::RelocationID(13170, 13315));
stl::detour_thunk<TES_DestroySkyCell>(REL::RelocationID(20029, 20463));
Copy link
Copy Markdown
Contributor

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'UnifiedWater worldspace hook sites:\n'
rg -n -C2 'TES_SetWorldSpace|TES_DestroySkyCell' src/Features/UnifiedWater.h src/Features/UnifiedWater.cpp

printf '\nExisting VR-aware relocation patterns in the repo:\n'
rg -n -C2 'VariantID|RelocateMember|Module::IsVR|Offset::get\(\)' src

Repository: community-shaders/skyrim-community-shaders

Length of output: 28919


🏁 Script executed:

# Check SupportsVR for UnifiedWater
rg -n 'class UnifiedWater|SupportsVR' src/Features/UnifiedWater.h src/Features/UnifiedWater.cpp | head -30

# Look for VR relocation IDs for these functions
rg -n 'TES_SetWorldSpace|TES_DestroySkyCell' src --type-list | head -20

# Find similar detour patterns with VariantID for VR
rg -n 'stl::detour_thunk.*VariantID' src

Repository: community-shaders/skyrim-community-shaders

Length of output: 578


🏁 Script executed:

# Check if there are other VR-aware detours in Features
rg -n 'stl::detour_thunk.*VariantID|stl::detour_thunk.*IsVR' src/Features

Repository: community-shaders/skyrim-community-shaders

Length of output: 68


🏁 Script executed:

# Look at the full UnifiedWater class definition including SupportsVR
sed -n '1,100p' src/Features/UnifiedWater.h | cat -n

Repository: community-shaders/skyrim-community-shaders

Length of output: 3947


🏁 Script executed:

# Search for VR address library patterns or relocation data
find . -name "*.json" -o -name "*offset*" -o -name "*relocation*" 2>/dev/null | head -20

# Check if there are conditional detours based on IsVR in UnifiedWater or similar features
rg -n 'IsVR.*detour|detour.*IsVR' src/Features

# Look at how other features handle worldspace-related hooks with VR
rg -n 'TESWorldSpace|worldSpace' src/Features --type cpp | head -20

Repository: community-shaders/skyrim-community-shaders

Length of output: 2533


🏁 Script executed:

# Search across the entire codebase for these specific function names to see if there are VR variants
rg -n 'SetWorldSpace|DestroySkyCell' src -A 3 -B 1 | head -50

Repository: community-shaders/skyrim-community-shaders

Length of output: 2329


🏁 Script executed:

# Check if there's conditional installation or if hooks should branch by IsVR
sed -n '800,830p' src/Features/UnifiedWater.cpp | cat -n

# Look for address library or VR-specific IDs in dependencies
find . -name "*.cpp" -o -name "*.h" | xargs grep -l "address.*library\|AddressLibrary" | head -5

Repository: community-shaders/skyrim-community-shaders

Length of output: 2015


🏁 Script executed:

# Check the broader context - are these hooks supposed to be installed conditionally?
sed -n '795,810p' src/Features/UnifiedWater.cpp | cat -n

# Search for similar patterns where RelocationID is used for hooks that support VR
rg -n 'SupportsVR.*true' src/Features/*.h -A 50 | grep -A 20 'PostPostLoad\|detour' | head -40

Repository: community-shaders/skyrim-community-shaders

Length of output: 3646


🏁 Script executed:

# Look for address library version checking or VR ID patterns
rg -n 'VariantID|VariantOffset' src --type cpp -B 2 -A 2 | head -60

# Check if there are any hooks that conditionally install based on IsVR
rg -n 'if.*IsVR.*detour|if.*IsVR.*write_thunk' src --type cpp

Repository: community-shaders/skyrim-community-shaders

Length of output: 922


🏁 Script executed:

# Search for how the codebase typically handles detours with VR - look for patterns
sed -n '920,940p' src/Features/UnifiedWater.cpp | cat -n

# Check if there are similar worldspace/TES hooks elsewhere with VR handling
rg -n 'TES.*thunk|worldSpace.*thunk' src/Features --type cpp -B 5 -A 5 | head -50

Repository: community-shaders/skyrim-community-shaders

Length of output: 4180


🏁 Script executed:

# Check if there are conditional VR branches or workarounds for these hooks elsewhere
rg -n 'TES_SetWorldSpace|TES_DestroySkyCell' src --type cpp -B 5 -A 5

# Look for out-of-band VR handling in UnifiedWater
grep -n 'IsVR\|VariantID' src/Features/UnifiedWater.cpp src/Features/UnifiedWater.h

Repository: community-shaders/skyrim-community-shaders

Length of output: 4343


🏁 Script executed:

# Check if there's any setup code checking for VR before these detours
sed -n '790,850p' src/Features/UnifiedWater.cpp | cat -n

Repository: community-shaders/skyrim-community-shaders

Length of output: 3787


Add VR relocation handling for the new worldspace hooks or disable VR support.

Lines 802-803 register detours with REL::RelocationID(SE, AE) pairs, but SupportsVR() returns true. These hooks will fail to install on VR since no VR address IDs are provided. The codebase supports REL::VariantID with VR offsets (e.g., Hooks.cpp:928, WetnessEffects.cpp:234), but this feature's worldspace hooks lack them. Either provide VR relocation IDs using VariantID, conditionally skip these hooks on VR if they're non-essential, or update SupportsVR() to reflect actual VR compatibility.

🤖 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/Features/UnifiedWater.cpp` around lines 802 - 803, The detour
registrations for TES_SetWorldSpace and TES_DestroySkyCell use REL::RelocationID
pairs but do not provide VR relocations while SupportsVR() returns true, so the
hooks will fail on VR; fix by either (A) switching these registrations to use
REL::VariantID and supply the VR offsets (matching the pattern used in Hooks.cpp
and WetnessEffects.cpp) for TES_SetWorldSpace and TES_DestroySkyCell, or (B)
guard the detour_thunk calls behind a runtime check that skips them when running
in VR (or disable SupportsVR() if full VR support is not provided). Update the
calls referencing TES_SetWorldSpace, TES_DestroySkyCell, and any related
installation logic to use VariantID or conditional installation so VR runs do
not attempt to install non-existent relocations.

Comment on lines +993 to +1059
PreserveUncoveredWaterSubtrees(water, waterSystem, *instructions, worldSpace, preserved);
ClearWaterNodeChildren(water, waterSystem);
block->waterAttached = false;
for (auto& rescued : preserved) {
if (rescued.root) {
water->AttachChild(rescued.root.get(), true);
SetSubtreeAppCulled(rescued.root.get(), false);
}
}

attaching = true;

for (auto& instruction : *instructions) {
if (!instruction.form.ptr)
continue;

RE::NiCloningProcess cloningProcess;

const auto targetShape = lodLevel > 4 || uw.settings.UseOptimisedMeshes ? uw.optimisedWaterMesh : uw.waterMesh;
RE::BSTriShape* shape = targetShape->CreateClone(cloningProcess)->AsTriShape();

const auto posX = (instruction.x - node->baseCellX) * 4096.0f + instruction.size * 2048.0f;
const auto posY = (instruction.y - node->baseCellY) * 4096.0f + instruction.size * 2048.0f;
shape->local.scale = static_cast<float>(instruction.size);
shape->local.translate = { posX, posY, instruction.waterHeight };

water->AttachChild(shape, true);
built.emplace_back(shape, &instruction);

block->waterAttached = true;
}
}

func(block);

if (!attaching || !block->waterAttached)
return;

for (auto& [shape, instruction] : built) {
waterSystem->AddWater(shape, instruction->form.ptr, instruction->waterHeight, nullptr, true, false);

ApplyUnifiedWaterFlags(shape, instruction->form.ptr);

// Remove from WaterSystem, will manage it ourselves
if (!waterSystem->waterObjects.empty()) {
waterSystem->waterObjects.pop_back();
}
}
for (auto& rescued : preserved) {
if (rescued.root)
SetSubtreeAppCulled(rescued.root.get(), false);

for (auto& preservedShape : rescued.shapes) {
if (!preservedShape.shape || !preservedShape.form)
continue;

waterSystem->AddWater(preservedShape.shape.get(), preservedShape.form, preservedShape.waterHeight, nullptr, true, false);
ApplyUnifiedWaterFlags(preservedShape.shape.get(), preservedShape.form);
preservedShape.shape->SetAppCulled(false);
}
}

RemoveDuplicateWaterSystemObjects(waterSystem, *uw.gWaterLOD);
if (block && block->water)
DetachAllChildOccurrences(*uw.gWaterLOD, block->water);
(*uw.gWaterLOD)->AttachChild(block->water, true);
waterSystem->Enable();
Copy link
Copy Markdown
Contributor

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

Preserved-only water blocks still fall through the attach path.

block->waterAttached is only flipped when a new generated tile is built. If PreserveUncoveredWaterSubtrees() found rescueable water but every instruction.form.ptr is null, the early return at Line 1028 skips re-registering those preserved shapes and never reattaches block->water under gWaterLOD, so the preservation path still drops the block.

Suggested fix
  func(block);

- if (!attaching || !block->waterAttached)
+ const bool hasManagedWater = !built.empty() || !preserved.empty();
+ block->waterAttached = hasManagedWater;
+ if (!attaching || !hasManagedWater)
    return;
🤖 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/Features/UnifiedWater.cpp` around lines 993 - 1059, Preserve-only water
blocks never set block->waterAttached so they hit the early return and never get
reattached; after calling PreserveUncoveredWaterSubtrees (and/or before the
early return that checks attaching and block->waterAttached) detect if any
rescued.root or any rescued.shapes contains a valid shape+form and, if so, set
block->waterAttached = true (and ensure attaching remains true) so the
subsequent loop that re-registers preserved shapes and reattaches block->water
under uw.gWaterLOD runs; update the logic around PreserveUncoveredWaterSubtrees,
preserved, block->waterAttached and the early return accordingly.

@github-actions
Copy link
Copy Markdown

✅ A pre-release build is available for this PR:
Download

@soda3000
Copy link
Copy Markdown
Contributor

@FIocker please review the suggestions Coderabbit made, and action any that are valid, or for those that are invalid, you can respond with why, then mark the thread as resolved.

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.

2 participants