From b5622743916255df5a3046170c06f5cabb9638ce Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Fri, 27 Mar 2026 13:41:38 +0100 Subject: [PATCH 01/15] fix: first debug attempts --- src/Features/UnifiedWater.cpp | 161 +++++++++++++++++++++++++++------- src/Features/UnifiedWater.h | 3 + 2 files changed, 134 insertions(+), 30 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index cb6a911c2e..84b598fa89 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -10,6 +10,48 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( UnifiedWater::Settings, UseOptimisedMeshes) +static bool IsChildWorldSpace(const RE::TESWorldSpace* ws) +{ + return ws && ws->parentWorld && + ws->parentUseFlags.all(RE::TESWorldSpace::ParentUseFlag::kUseLODData); +} + +// Cull all tile children of waterParent based on the current tes->gridCells. +// Returns {culled, total} for diagnostic purposes. +static std::pair CullWaterParentByGridCells(RE::BSMultiBoundNode* waterParent) +{ + const auto tes = globals::game::tes; + if (!tes || !tes->gridCells || !waterParent) + return { 0, 0 }; + + const auto& gridCells = tes->gridCells; + const int32_t offsetX = tes->currentGridX - static_cast(gridCells->length >> 1); + const int32_t offsetY = tes->currentGridY - static_cast(gridCells->length >> 1); + const int32_t length = static_cast(gridCells->length); + + int32_t culled = 0, total = 0; + for (const auto& child : waterParent->GetChildren()) { + if (!child) + continue; + total++; + int32_t x, y; + Util::WorldToCell(child->world.translate, x, y); + x -= offsetX; + y -= offsetY; + bool cull = false; + if (x >= 0 && y >= 0 && x < length && y < length) { + if (const auto cell = gridCells->GetCell(x, y); cell && cell->cellState.any( + RE::TESObjectCELL::CellState::kAttached, + static_cast(6))) + cull = true; + } + child->SetAppCulled(cull); + if (cull) + culled++; + } + return { culled, total }; +} + void UnifiedWater::LoadSettings(json& o_json) { settings = o_json; @@ -316,16 +358,58 @@ int32_t UnifiedWater::BSWaterShaderMaterial_ComputeCRC32::thunk(RE::BSWaterShade void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* worldSpace, bool isExterior) { + // Unconditional diagnostic: log every worldspace transition to identify what fires on child entry + { + const char* wsName = worldSpace ? worldSpace->GetFormEditorID() : "(null)"; + const bool hasParent = worldSpace && worldSpace->parentWorld; + const bool hasLODFlag = hasParent && worldSpace->parentUseFlags.all(RE::TESWorldSpace::ParentUseFlag::kUseLODData); + const char* parentName = hasParent ? worldSpace->parentWorld->GetFormEditorID() : "(none)"; + logger::info("[Unified Water] [Anim] TES_SetWorldSpace: ws='{}' isExterior={} hasParent={} kUseLODData={} parent='{}'", + wsName, isExterior, hasParent, hasLODFlag, parentName); + } + + const bool enteringChild = IsChildWorldSpace(worldSpace); + + // Set currentPlayerWorldSpace BEFORE func so BGSTerrainNode_UpdateWaterMeshSubVisibility + // sees the correct worldspace when it fires during cell attachment inside func. + auto& uw = globals::features::unifiedWater; + uw.currentPlayerWorldSpace = worldSpace; + if (!enteringChild) + uw.pendingChildWsCull = false; // leaving child WS: discard any stale pending cull + func(tes, worldSpace, isExterior); - globals::features::unifiedWater.waterCache->SetCurrentWorldSpace(worldSpace); + if (enteringChild && uw.gWaterLOD && *uw.gWaterLOD) { + logger::info("[Unified Water] [Anim] After func, *gWaterLOD child count: {}", + (*uw.gWaterLOD)->GetChildren().size()); + } + + uw.waterCache->SetCurrentWorldSpace(worldSpace); + + if (enteringChild) { + // BGSTerrainBlock_Attach calls waterSystem->Enable() when blocks attach. + // In child worldspaces, already-attached LOD blocks don't re-attach, so Enable() + // is never called after the transition — leaving LOD water tiles unanimated. + if (const auto waterSystem = RE::TESWaterSystem::GetSingleton()) + waterSystem->Enable(); + + // BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces, + // and tes/gridCells are null this early in the transition. + // Set a flag so BGSTerrainBlock_Attach can do the cull once cells are loaded. + uw.pendingChildWsCull = true; + logger::info("[Unified Water] [Cull] pendingChildWsCull set on child WS entry (existing LOD count: {})", + (uw.gWaterLOD && *uw.gWaterLOD) ? (*uw.gWaterLOD)->GetChildren().size() : 0); + } } void UnifiedWater::TES_DestroySkyCell::thunk(RE::TES* tes) { func(tes); - globals::features::unifiedWater.waterCache->SetCurrentWorldSpace(nullptr); + auto& uw = globals::features::unifiedWater; + uw.currentPlayerWorldSpace = nullptr; + uw.pendingChildWsCull = false; + uw.waterCache->SetCurrentWorldSpace(nullptr); } void UnifiedWater::BGSTerrainNode_UpdateWaterMeshSubVisibility::thunk(const RE::BGSTerrainNode* node, RE::BSMultiBoundNode* waterParent) @@ -336,34 +420,7 @@ void UnifiedWater::BGSTerrainNode_UpdateWaterMeshSubVisibility::thunk(const RE:: if (node->GetLODLevel() != 4) return; - const auto tes = globals::game::tes; - if (!tes || !tes->gridCells) - return; - - const auto& gridCells = tes->gridCells; - - const int32_t offsetX = tes->currentGridX - static_cast(gridCells->length >> 1); - const int32_t offsetY = tes->currentGridY - static_cast(gridCells->length >> 1); - const int32_t length = static_cast(gridCells->length); - - for (const auto& child : waterParent->GetChildren()) { - if (!child) - continue; - - int32_t x, y; - Util::WorldToCell(child->world.translate, x, y); - - x -= offsetX; - y -= offsetY; - - bool cull = false; - if (x >= 0 && y >= 0 && x < length && y < length) { - if (const auto cell = gridCells->GetCell(x, y); cell && cell->cellState.any(RE::TESObjectCELL::CellState::kAttached, static_cast(6))) - cull = true; - } - - child->SetAppCulled(cull); - } + CullWaterParentByGridCells(waterParent); } void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) @@ -451,6 +508,50 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) (*singleton.gWaterLOD)->AttachChild(block->water, true); waterSystem->Enable(); + + // BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces. + // Cull new tiles immediately, and if a full cull pass is pending (transition case + // where pre-existing LOD blocks weren't re-attached), do it now that tes/gridCells + // are valid. + if (IsChildWorldSpace(singleton.currentPlayerWorldSpace)) { + const auto tes = globals::game::tes; + if (tes && tes->gridCells) { + // Cull the new tiles for this block + const auto& gridCells = tes->gridCells; + const int32_t offsetX = tes->currentGridX - static_cast(gridCells->length >> 1); + const int32_t offsetY = tes->currentGridY - static_cast(gridCells->length >> 1); + const int32_t length = static_cast(gridCells->length); + for (const auto& [shape, instruction] : built) { + const int32_t ix = instruction->x - offsetX; + const int32_t iy = instruction->y - offsetY; + bool cull = false; + if (ix >= 0 && iy >= 0 && ix < length && iy < length) { + if (const auto cell = gridCells->GetCell(ix, iy); cell && cell->cellState.any( + RE::TESObjectCELL::CellState::kAttached, + static_cast(6))) + cull = true; + } + shape->SetAppCulled(cull); + } + + // Consume pending full-cull flag (set on transition where pre-existing LOD + // blocks weren't re-attached and couldn't be culled in TES_SetWorldSpace). + auto& uw = globals::features::unifiedWater; + if (uw.pendingChildWsCull && uw.gWaterLOD && *uw.gWaterLOD) { + uw.pendingChildWsCull = false; + int32_t culled = 0, total = 0; + for (const auto& waterParentPtr : (*uw.gWaterLOD)->GetChildren()) { + if (!waterParentPtr) + continue; + const auto waterParent = static_cast(waterParentPtr.get()); + auto [c, t] = CullWaterParentByGridCells(waterParent); + culled += c; + total += t; + } + logger::info("[Unified Water] [Cull] Deferred full cull on child WS entry: {}/{} tiles culled", culled, total); + } + } + } } void UnifiedWater::BGSTerrainBlock_Detach::thunk(RE::BGSTerrainBlock* block) diff --git a/src/Features/UnifiedWater.h b/src/Features/UnifiedWater.h index ba10e1a3e9..666b14e11a 100644 --- a/src/Features/UnifiedWater.h +++ b/src/Features/UnifiedWater.h @@ -111,6 +111,9 @@ struct UnifiedWater : OverlayFeature RE::NiPoint2* gDisplacementMeshPos = nullptr; RE::NiPoint2* gDisplacementMeshFlowCellOffset = nullptr; + RE::TESWorldSpace* currentPlayerWorldSpace = nullptr; + bool pendingChildWsCull = false; + void SetFlowmapTex() const; static bool LoadOrderChanged(); }; From 96ace8be9c0eec225cb00e8819a2ebeb0d7a96a3 Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Fri, 27 Mar 2026 16:06:09 +0100 Subject: [PATCH 02/15] chore: more debugging --- src/Features/UnifiedWater.cpp | 135 ++++++++++++++++++++++++++-------- 1 file changed, 103 insertions(+), 32 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 84b598fa89..1951c3a614 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -16,11 +16,13 @@ static bool IsChildWorldSpace(const RE::TESWorldSpace* ws) ws->parentUseFlags.all(RE::TESWorldSpace::ParentUseFlag::kUseLODData); } -// Cull all tile children of waterParent based on the current tes->gridCells. +// Cull all tile children of waterParent based on tes->gridCells. +// Pass an explicit tes when globals::game::tes is not yet populated (e.g., during TES_SetWorldSpace). // Returns {culled, total} for diagnostic purposes. -static std::pair CullWaterParentByGridCells(RE::BSMultiBoundNode* waterParent) +static std::pair CullWaterParentByGridCells(RE::BSMultiBoundNode* waterParent, RE::TES* tes = nullptr) { - const auto tes = globals::game::tes; + if (!tes) + tes = globals::game::tes; if (!tes || !tes->gridCells || !waterParent) return { 0, 0 }; @@ -393,11 +395,25 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor if (const auto waterSystem = RE::TESWaterSystem::GetSingleton()) waterSystem->Enable(); - // BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces, - // and tes/gridCells are null this early in the transition. - // Set a flag so BGSTerrainBlock_Attach can do the cull once cells are loaded. + // Try an immediate cull using the tes parameter (globals::game::tes may be null here). + // Cells just transitioned: they're likely not kAttached yet, so this usually culls 0 tiles. + // pendingChildWsCull is always set so BSWaterShader_SetupGeometry can retry once cells load. + if (uw.gWaterLOD && *uw.gWaterLOD && tes && tes->gridCells) { + int32_t culled = 0, total = 0; + for (const auto& waterParentPtr : (*uw.gWaterLOD)->GetChildren()) { + if (!waterParentPtr) + continue; + const auto waterParent = static_cast(waterParentPtr.get()); + auto [c, t] = CullWaterParentByGridCells(waterParent, tes); + culled += c; + total += t; + } + logger::info("[Unified Water] [Cull] Early cull on child WS entry: {}/{} tiles culled", culled, total); + } + // Always set the deferred flag: cells may not be kAttached until several frames later, + // and BSWaterShader_SetupGeometry will retry until the cull actually takes effect. uw.pendingChildWsCull = true; - logger::info("[Unified Water] [Cull] pendingChildWsCull set on child WS entry (existing LOD count: {})", + logger::info("[Unified Water] [Cull] pendingChildWsCull set (LOD blocks={})", (uw.gWaterLOD && *uw.gWaterLOD) ? (*uw.gWaterLOD)->GetChildren().size() : 0); } } @@ -428,6 +444,29 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) const auto waterSystem = RE::TESWaterSystem::GetSingleton(); const auto& singleton = globals::features::unifiedWater; + // Consume pending child WS cull on the first block attach after entering a child worldspace. + // BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces, so we cull here + // instead. Placed before instruction lookup so it fires even for blocks with no UW tiles. + { + auto& uw = globals::features::unifiedWater; + if (uw.pendingChildWsCull && IsChildWorldSpace(uw.currentPlayerWorldSpace) && uw.gWaterLOD && *uw.gWaterLOD) { + const auto tes = globals::game::tes; + if (tes && tes->gridCells) { + uw.pendingChildWsCull = false; + int32_t culled = 0, total = 0; + for (const auto& waterParentPtr : (*uw.gWaterLOD)->GetChildren()) { + if (!waterParentPtr) + continue; + const auto waterParent = static_cast(waterParentPtr.get()); + auto [c, t] = CullWaterParentByGridCells(waterParent); + culled += c; + total += t; + } + logger::info("[Unified Water] [Cull] Deferred full cull on child WS entry: {}/{} tiles culled", culled, total); + } + } + } + std::vector> built; bool attaching = false; @@ -533,23 +572,6 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) } shape->SetAppCulled(cull); } - - // Consume pending full-cull flag (set on transition where pre-existing LOD - // blocks weren't re-attached and couldn't be culled in TES_SetWorldSpace). - auto& uw = globals::features::unifiedWater; - if (uw.pendingChildWsCull && uw.gWaterLOD && *uw.gWaterLOD) { - uw.pendingChildWsCull = false; - int32_t culled = 0, total = 0; - for (const auto& waterParentPtr : (*uw.gWaterLOD)->GetChildren()) { - if (!waterParentPtr) - continue; - const auto waterParent = static_cast(waterParentPtr.get()); - auto [c, t] = CullWaterParentByGridCells(waterParent); - culled += c; - total += t; - } - logger::info("[Unified Water] [Cull] Deferred full cull on child WS entry: {}/{} tiles culled", culled, total); - } } } } @@ -577,6 +599,34 @@ void UnifiedWater::BGSTerrainBlock_Detach::thunk(RE::BGSTerrainBlock* block) void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, RE::BSRenderPass* pass) { const auto& singleton = globals::features::unifiedWater; + + // Deferred child-WS cull: cells are not kAttached immediately after TES_SetWorldSpace, so we + // retry here (render thread) until globals::game::tes->gridCells has attached cells. + // One-shot diagnostic on first invocation so we can see tes/gridCells state. + if (singleton.pendingChildWsCull && IsChildWorldSpace(singleton.currentPlayerWorldSpace) && singleton.gWaterLOD && *singleton.gWaterLOD) { + const auto tes = globals::game::tes; + static bool diagLogged = false; + if (!diagLogged) { + logger::info("[Unified Water] [Cull] BSWaterShader_SetupGeometry deferred: tes={}, gridCells={}", + (void*)tes, tes ? (void*)tes->gridCells : nullptr); + diagLogged = true; + } + if (tes && tes->gridCells) { + auto& uw = globals::features::unifiedWater; + uw.pendingChildWsCull = false; + int32_t culled = 0, total = 0; + for (const auto& waterParentPtr : (*uw.gWaterLOD)->GetChildren()) { + if (!waterParentPtr) + continue; + const auto waterParent = static_cast(waterParentPtr.get()); + auto [c, t] = CullWaterParentByGridCells(waterParent); + culled += c; + total += t; + } + logger::info("[Unified Water] [Cull] Deferred cull (BSWaterShader): {}/{} tiles culled", culled, total); + } + } + // Fix BSWaterShaderProperty.plane after interior->exterior transitions. // The plane feeds ReflectPlane in the PerGeometry cbuffer. When corrupted (e.g., plane.constant = 0 // or garbage), the shader's refractionPlaneMul calculation produces extreme values causing flickering. @@ -630,17 +680,38 @@ void UnifiedWater::TESWaterSystem_UpdateDisplacementMeshPosition::thunk(RE::TESW { func(waterSystem); - const auto& singleton = globals::features::unifiedWater; - if (!singleton.flowmap) + auto& uw = globals::features::unifiedWater; + + // BGSTerrainBlock_Attach doesn't fire for already-attached LOD blocks when entering a child + // worldspace, and BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces. + // This hook runs on the game thread with valid tes/gridCells, so consume the pending cull here. + if (uw.pendingChildWsCull && IsChildWorldSpace(uw.currentPlayerWorldSpace) && uw.gWaterLOD && *uw.gWaterLOD) { + const auto tes = globals::game::tes; + if (tes && tes->gridCells) { + uw.pendingChildWsCull = false; + int32_t culled = 0, total = 0; + for (const auto& waterParentPtr : (*uw.gWaterLOD)->GetChildren()) { + if (!waterParentPtr) + continue; + const auto waterParent = static_cast(waterParentPtr.get()); + auto [c, t] = CullWaterParentByGridCells(waterParent); + culled += c; + total += t; + } + logger::info("[Unified Water] [Cull] Deferred full cull on child WS entry: {}/{} tiles culled", culled, total); + } + } + + if (!uw.flowmap) return; - const float posX = singleton.gDisplacementMeshPos->x / 4096.0f; - const float posY = singleton.gDisplacementMeshPos->y / 4096.0f; - const float offsetX = static_cast(singleton.flowmap->GetOffsetX()); - const float offsetY = static_cast(singleton.flowmap->GetOffsetY()); - const float height = static_cast(singleton.flowmap->GetHeight()); + const float posX = uw.gDisplacementMeshPos->x / 4096.0f; + const float posY = uw.gDisplacementMeshPos->y / 4096.0f; + const float offsetX = static_cast(uw.flowmap->GetOffsetX()); + const float offsetY = static_cast(uw.flowmap->GetOffsetY()); + const float height = static_cast(uw.flowmap->GetHeight()); // CellTexCoordOffset.xyzw below - applies to displacement water only // Previously the values were calculated relative to the 5x5 flow grid - *singleton.gDisplacementCellTexCoordOffset = float4(posX + offsetX, height - (posY + offsetY), posX, 1 - posY); + *uw.gDisplacementCellTexCoordOffset = float4(posX + offsetX, height - (posY + offsetY), posX, 1 - posY); } \ No newline at end of file From 689989b30462a72a6effc3b6459adac211f87290 Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Sat, 28 Mar 2026 00:19:29 +0100 Subject: [PATCH 03/15] fix: flicker --- src/Features/UnifiedWater.cpp | 18 +++++++----------- src/Features/UnifiedWater.h | 3 +++ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 1951c3a614..c7491b6906 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -376,6 +376,7 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor // sees the correct worldspace when it fires during cell attachment inside func. auto& uw = globals::features::unifiedWater; uw.currentPlayerWorldSpace = worldSpace; + uw.cachedTes = tes; // globals::game::tes is null on the render thread; cache here for later use if (!enteringChild) uw.pendingChildWsCull = false; // leaving child WS: discard any stale pending cull @@ -600,17 +601,12 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, { const auto& singleton = globals::features::unifiedWater; - // Deferred child-WS cull: cells are not kAttached immediately after TES_SetWorldSpace, so we - // retry here (render thread) until globals::game::tes->gridCells has attached cells. - // One-shot diagnostic on first invocation so we can see tes/gridCells state. + // Deferred child-WS cull: cells are not kAttached immediately after TES_SetWorldSpace. + // Use cachedTes (saved on the game thread where it's valid) instead of globals::game::tes + // which is null on the render thread due to initialization ordering. + // Keep retrying (pendingChildWsCull stays true) until gridCells is populated with kAttached cells. if (singleton.pendingChildWsCull && IsChildWorldSpace(singleton.currentPlayerWorldSpace) && singleton.gWaterLOD && *singleton.gWaterLOD) { - const auto tes = globals::game::tes; - static bool diagLogged = false; - if (!diagLogged) { - logger::info("[Unified Water] [Cull] BSWaterShader_SetupGeometry deferred: tes={}, gridCells={}", - (void*)tes, tes ? (void*)tes->gridCells : nullptr); - diagLogged = true; - } + const auto tes = singleton.cachedTes; if (tes && tes->gridCells) { auto& uw = globals::features::unifiedWater; uw.pendingChildWsCull = false; @@ -619,7 +615,7 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, if (!waterParentPtr) continue; const auto waterParent = static_cast(waterParentPtr.get()); - auto [c, t] = CullWaterParentByGridCells(waterParent); + auto [c, t] = CullWaterParentByGridCells(waterParent, tes); culled += c; total += t; } diff --git a/src/Features/UnifiedWater.h b/src/Features/UnifiedWater.h index 666b14e11a..9b20030c7a 100644 --- a/src/Features/UnifiedWater.h +++ b/src/Features/UnifiedWater.h @@ -113,6 +113,9 @@ struct UnifiedWater : OverlayFeature RE::TESWorldSpace* currentPlayerWorldSpace = nullptr; bool pendingChildWsCull = false; + // Cached from TES_SetWorldSpace::thunk (game thread) for use on the render thread. + // globals::game::tes is null on the render thread (cached before TES singleton existed). + RE::TES* cachedTes = nullptr; void SetFlowmapTex() const; static bool LoadOrderChanged(); From e2a99bd7d338c278a93845f804380ceee556d807 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:43:41 +0000 Subject: [PATCH 04/15] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/Features/UnifiedWater.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index c7491b6906..53eb604880 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -43,8 +43,8 @@ static std::pair CullWaterParentByGridCells(RE::BSMultiBoundNo bool cull = false; if (x >= 0 && y >= 0 && x < length && y < length) { if (const auto cell = gridCells->GetCell(x, y); cell && cell->cellState.any( - RE::TESObjectCELL::CellState::kAttached, - static_cast(6))) + RE::TESObjectCELL::CellState::kAttached, + static_cast(6))) cull = true; } child->SetAppCulled(cull); @@ -567,8 +567,8 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) bool cull = false; if (ix >= 0 && iy >= 0 && ix < length && iy < length) { if (const auto cell = gridCells->GetCell(ix, iy); cell && cell->cellState.any( - RE::TESObjectCELL::CellState::kAttached, - static_cast(6))) + RE::TESObjectCELL::CellState::kAttached, + static_cast(6))) cull = true; } shape->SetAppCulled(cull); From 28615f64467a1b7cc10b3b4243d8914e0e5bf49a Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Sat, 28 Mar 2026 15:20:03 +0100 Subject: [PATCH 05/15] chore: cleanup --- src/Features/UnifiedWater.cpp | 93 +++++++++-------------------------- 1 file changed, 24 insertions(+), 69 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 53eb604880..56bc260aac 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -18,24 +18,21 @@ static bool IsChildWorldSpace(const RE::TESWorldSpace* ws) // Cull all tile children of waterParent based on tes->gridCells. // Pass an explicit tes when globals::game::tes is not yet populated (e.g., during TES_SetWorldSpace). -// Returns {culled, total} for diagnostic purposes. -static std::pair CullWaterParentByGridCells(RE::BSMultiBoundNode* waterParent, RE::TES* tes = nullptr) +static void CullWaterParentByGridCells(RE::BSMultiBoundNode* waterParent, RE::TES* tes = nullptr) { if (!tes) tes = globals::game::tes; if (!tes || !tes->gridCells || !waterParent) - return { 0, 0 }; + return; const auto& gridCells = tes->gridCells; const int32_t offsetX = tes->currentGridX - static_cast(gridCells->length >> 1); const int32_t offsetY = tes->currentGridY - static_cast(gridCells->length >> 1); const int32_t length = static_cast(gridCells->length); - int32_t culled = 0, total = 0; for (const auto& child : waterParent->GetChildren()) { if (!child) continue; - total++; int32_t x, y; Util::WorldToCell(child->world.translate, x, y); x -= offsetX; @@ -48,10 +45,21 @@ static std::pair CullWaterParentByGridCells(RE::BSMultiBoundNo cull = true; } child->SetAppCulled(cull); - if (cull) - culled++; } - return { culled, total }; +} + +// Cull every tile under all water LOD parent nodes. +static void CullAllWaterLODParents(RE::NiNode* waterLOD, RE::TES* tes = nullptr) +{ + if (!waterLOD) + return; + + for (const auto& waterParentPtr : waterLOD->GetChildren()) { + if (!waterParentPtr) + continue; + const auto waterParent = static_cast(waterParentPtr.get()); + CullWaterParentByGridCells(waterParent, tes); + } } void UnifiedWater::LoadSettings(json& o_json) @@ -360,16 +368,6 @@ int32_t UnifiedWater::BSWaterShaderMaterial_ComputeCRC32::thunk(RE::BSWaterShade void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* worldSpace, bool isExterior) { - // Unconditional diagnostic: log every worldspace transition to identify what fires on child entry - { - const char* wsName = worldSpace ? worldSpace->GetFormEditorID() : "(null)"; - const bool hasParent = worldSpace && worldSpace->parentWorld; - const bool hasLODFlag = hasParent && worldSpace->parentUseFlags.all(RE::TESWorldSpace::ParentUseFlag::kUseLODData); - const char* parentName = hasParent ? worldSpace->parentWorld->GetFormEditorID() : "(none)"; - logger::info("[Unified Water] [Anim] TES_SetWorldSpace: ws='{}' isExterior={} hasParent={} kUseLODData={} parent='{}'", - wsName, isExterior, hasParent, hasLODFlag, parentName); - } - const bool enteringChild = IsChildWorldSpace(worldSpace); // Set currentPlayerWorldSpace BEFORE func so BGSTerrainNode_UpdateWaterMeshSubVisibility @@ -382,11 +380,6 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor func(tes, worldSpace, isExterior); - if (enteringChild && uw.gWaterLOD && *uw.gWaterLOD) { - logger::info("[Unified Water] [Anim] After func, *gWaterLOD child count: {}", - (*uw.gWaterLOD)->GetChildren().size()); - } - uw.waterCache->SetCurrentWorldSpace(worldSpace); if (enteringChild) { @@ -398,24 +391,13 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor // Try an immediate cull using the tes parameter (globals::game::tes may be null here). // Cells just transitioned: they're likely not kAttached yet, so this usually culls 0 tiles. - // pendingChildWsCull is always set so BSWaterShader_SetupGeometry can retry once cells load. - if (uw.gWaterLOD && *uw.gWaterLOD && tes && tes->gridCells) { - int32_t culled = 0, total = 0; - for (const auto& waterParentPtr : (*uw.gWaterLOD)->GetChildren()) { - if (!waterParentPtr) - continue; - const auto waterParent = static_cast(waterParentPtr.get()); - auto [c, t] = CullWaterParentByGridCells(waterParent, tes); - culled += c; - total += t; - } - logger::info("[Unified Water] [Cull] Early cull on child WS entry: {}/{} tiles culled", culled, total); - } + // pendingChildWsCull is always set so deferred hooks can retry once cells load. + if (uw.gWaterLOD && *uw.gWaterLOD && tes && tes->gridCells) + CullAllWaterLODParents(*uw.gWaterLOD, tes); + // Always set the deferred flag: cells may not be kAttached until several frames later, - // and BSWaterShader_SetupGeometry will retry until the cull actually takes effect. + // and deferred cull hooks will retry until the cull actually takes effect. uw.pendingChildWsCull = true; - logger::info("[Unified Water] [Cull] pendingChildWsCull set (LOD blocks={})", - (uw.gWaterLOD && *uw.gWaterLOD) ? (*uw.gWaterLOD)->GetChildren().size() : 0); } } @@ -454,16 +436,7 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) const auto tes = globals::game::tes; if (tes && tes->gridCells) { uw.pendingChildWsCull = false; - int32_t culled = 0, total = 0; - for (const auto& waterParentPtr : (*uw.gWaterLOD)->GetChildren()) { - if (!waterParentPtr) - continue; - const auto waterParent = static_cast(waterParentPtr.get()); - auto [c, t] = CullWaterParentByGridCells(waterParent); - culled += c; - total += t; - } - logger::info("[Unified Water] [Cull] Deferred full cull on child WS entry: {}/{} tiles culled", culled, total); + CullAllWaterLODParents(*uw.gWaterLOD); } } } @@ -610,16 +583,7 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, if (tes && tes->gridCells) { auto& uw = globals::features::unifiedWater; uw.pendingChildWsCull = false; - int32_t culled = 0, total = 0; - for (const auto& waterParentPtr : (*uw.gWaterLOD)->GetChildren()) { - if (!waterParentPtr) - continue; - const auto waterParent = static_cast(waterParentPtr.get()); - auto [c, t] = CullWaterParentByGridCells(waterParent, tes); - culled += c; - total += t; - } - logger::info("[Unified Water] [Cull] Deferred cull (BSWaterShader): {}/{} tiles culled", culled, total); + CullAllWaterLODParents(*uw.gWaterLOD, tes); } } @@ -685,16 +649,7 @@ void UnifiedWater::TESWaterSystem_UpdateDisplacementMeshPosition::thunk(RE::TESW const auto tes = globals::game::tes; if (tes && tes->gridCells) { uw.pendingChildWsCull = false; - int32_t culled = 0, total = 0; - for (const auto& waterParentPtr : (*uw.gWaterLOD)->GetChildren()) { - if (!waterParentPtr) - continue; - const auto waterParent = static_cast(waterParentPtr.get()); - auto [c, t] = CullWaterParentByGridCells(waterParent); - culled += c; - total += t; - } - logger::info("[Unified Water] [Cull] Deferred full cull on child WS entry: {}/{} tiles culled", culled, total); + CullAllWaterLODParents(*uw.gWaterLOD); } } From 81f0f7742cf4672ca48486fcec9181a4c4d14d7a Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Sat, 28 Mar 2026 15:35:27 +0100 Subject: [PATCH 06/15] fix: update water culling logic and use atomic types for thread safety --- src/Features/UnifiedWater.cpp | 65 ++++++++++++++++++++++------------- src/Features/UnifiedWater.h | 8 +++-- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 56bc260aac..aabd8575ed 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -18,17 +18,18 @@ static bool IsChildWorldSpace(const RE::TESWorldSpace* ws) // Cull all tile children of waterParent based on tes->gridCells. // Pass an explicit tes when globals::game::tes is not yet populated (e.g., during TES_SetWorldSpace). -static void CullWaterParentByGridCells(RE::BSMultiBoundNode* waterParent, RE::TES* tes = nullptr) +static bool CullWaterParentByGridCells(RE::NiNode* waterParent, RE::TES* tes = nullptr) { if (!tes) tes = globals::game::tes; if (!tes || !tes->gridCells || !waterParent) - return; + return false; const auto& gridCells = tes->gridCells; const int32_t offsetX = tes->currentGridX - static_cast(gridCells->length >> 1); const int32_t offsetY = tes->currentGridY - static_cast(gridCells->length >> 1); const int32_t length = static_cast(gridCells->length); + bool foundAttachedCell = false; for (const auto& child : waterParent->GetChildren()) { if (!child) @@ -41,25 +42,35 @@ static void CullWaterParentByGridCells(RE::BSMultiBoundNode* waterParent, RE::TE if (x >= 0 && y >= 0 && x < length && y < length) { if (const auto cell = gridCells->GetCell(x, y); cell && cell->cellState.any( RE::TESObjectCELL::CellState::kAttached, - static_cast(6))) + static_cast(6))) { cull = true; + foundAttachedCell = true; + } } child->SetAppCulled(cull); } + + return foundAttachedCell; } // Cull every tile under all water LOD parent nodes. -static void CullAllWaterLODParents(RE::NiNode* waterLOD, RE::TES* tes = nullptr) +static bool CullAllWaterLODParents(RE::NiNode* waterLOD, RE::TES* tes = nullptr) { if (!waterLOD) - return; + return false; + + bool foundAttachedCell = false; for (const auto& waterParentPtr : waterLOD->GetChildren()) { if (!waterParentPtr) continue; - const auto waterParent = static_cast(waterParentPtr.get()); - CullWaterParentByGridCells(waterParent, tes); + const auto waterParent = waterParentPtr->AsNode(); + if (!waterParent) + continue; + foundAttachedCell = CullWaterParentByGridCells(waterParent, tes) || foundAttachedCell; } + + return foundAttachedCell; } void UnifiedWater::LoadSettings(json& o_json) @@ -373,10 +384,10 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor // Set currentPlayerWorldSpace BEFORE func so BGSTerrainNode_UpdateWaterMeshSubVisibility // sees the correct worldspace when it fires during cell attachment inside func. auto& uw = globals::features::unifiedWater; - uw.currentPlayerWorldSpace = worldSpace; - uw.cachedTes = tes; // globals::game::tes is null on the render thread; cache here for later use + uw.currentPlayerWorldSpace.store(worldSpace, std::memory_order_release); + uw.cachedTes.store(tes, std::memory_order_release); // globals::game::tes is null on the render thread; cache here for later use if (!enteringChild) - uw.pendingChildWsCull = false; // leaving child WS: discard any stale pending cull + uw.pendingChildWsCull.store(false, std::memory_order_release); // leaving child WS: discard any stale pending cull func(tes, worldSpace, isExterior); @@ -397,7 +408,7 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor // Always set the deferred flag: cells may not be kAttached until several frames later, // and deferred cull hooks will retry until the cull actually takes effect. - uw.pendingChildWsCull = true; + uw.pendingChildWsCull.store(true, std::memory_order_release); } } @@ -406,8 +417,8 @@ void UnifiedWater::TES_DestroySkyCell::thunk(RE::TES* tes) func(tes); auto& uw = globals::features::unifiedWater; - uw.currentPlayerWorldSpace = nullptr; - uw.pendingChildWsCull = false; + uw.currentPlayerWorldSpace.store(nullptr, std::memory_order_release); + uw.pendingChildWsCull.store(false, std::memory_order_release); uw.waterCache->SetCurrentWorldSpace(nullptr); } @@ -432,11 +443,13 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) // instead. Placed before instruction lookup so it fires even for blocks with no UW tiles. { auto& uw = globals::features::unifiedWater; - if (uw.pendingChildWsCull && IsChildWorldSpace(uw.currentPlayerWorldSpace) && uw.gWaterLOD && *uw.gWaterLOD) { + if (uw.pendingChildWsCull.load(std::memory_order_acquire) && + IsChildWorldSpace(uw.currentPlayerWorldSpace.load(std::memory_order_acquire)) && + uw.gWaterLOD && *uw.gWaterLOD) { const auto tes = globals::game::tes; if (tes && tes->gridCells) { - uw.pendingChildWsCull = false; - CullAllWaterLODParents(*uw.gWaterLOD); + if (CullAllWaterLODParents(*uw.gWaterLOD)) + uw.pendingChildWsCull.store(false, std::memory_order_release); } } } @@ -526,7 +539,7 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) // Cull new tiles immediately, and if a full cull pass is pending (transition case // where pre-existing LOD blocks weren't re-attached), do it now that tes/gridCells // are valid. - if (IsChildWorldSpace(singleton.currentPlayerWorldSpace)) { + if (IsChildWorldSpace(singleton.currentPlayerWorldSpace.load(std::memory_order_acquire))) { const auto tes = globals::game::tes; if (tes && tes->gridCells) { // Cull the new tiles for this block @@ -578,12 +591,14 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, // Use cachedTes (saved on the game thread where it's valid) instead of globals::game::tes // which is null on the render thread due to initialization ordering. // Keep retrying (pendingChildWsCull stays true) until gridCells is populated with kAttached cells. - if (singleton.pendingChildWsCull && IsChildWorldSpace(singleton.currentPlayerWorldSpace) && singleton.gWaterLOD && *singleton.gWaterLOD) { - const auto tes = singleton.cachedTes; + if (singleton.pendingChildWsCull.load(std::memory_order_acquire) && + IsChildWorldSpace(singleton.currentPlayerWorldSpace.load(std::memory_order_acquire)) && + singleton.gWaterLOD && *singleton.gWaterLOD) { + const auto tes = singleton.cachedTes.load(std::memory_order_acquire); if (tes && tes->gridCells) { auto& uw = globals::features::unifiedWater; - uw.pendingChildWsCull = false; - CullAllWaterLODParents(*uw.gWaterLOD, tes); + if (CullAllWaterLODParents(*uw.gWaterLOD, tes)) + uw.pendingChildWsCull.store(false, std::memory_order_release); } } @@ -645,11 +660,13 @@ void UnifiedWater::TESWaterSystem_UpdateDisplacementMeshPosition::thunk(RE::TESW // BGSTerrainBlock_Attach doesn't fire for already-attached LOD blocks when entering a child // worldspace, and BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces. // This hook runs on the game thread with valid tes/gridCells, so consume the pending cull here. - if (uw.pendingChildWsCull && IsChildWorldSpace(uw.currentPlayerWorldSpace) && uw.gWaterLOD && *uw.gWaterLOD) { + if (uw.pendingChildWsCull.load(std::memory_order_acquire) && + IsChildWorldSpace(uw.currentPlayerWorldSpace.load(std::memory_order_acquire)) && + uw.gWaterLOD && *uw.gWaterLOD) { const auto tes = globals::game::tes; if (tes && tes->gridCells) { - uw.pendingChildWsCull = false; - CullAllWaterLODParents(*uw.gWaterLOD); + if (CullAllWaterLODParents(*uw.gWaterLOD)) + uw.pendingChildWsCull.store(false, std::memory_order_release); } } diff --git a/src/Features/UnifiedWater.h b/src/Features/UnifiedWater.h index 9b20030c7a..c6c8afe3c0 100644 --- a/src/Features/UnifiedWater.h +++ b/src/Features/UnifiedWater.h @@ -3,6 +3,8 @@ #include "UnifiedWater/Flowmap.h" #include "UnifiedWater/WaterCache.h" +#include + struct UnifiedWater : OverlayFeature { virtual inline std::string GetName() override { return "Unified Water"; } @@ -111,11 +113,11 @@ struct UnifiedWater : OverlayFeature RE::NiPoint2* gDisplacementMeshPos = nullptr; RE::NiPoint2* gDisplacementMeshFlowCellOffset = nullptr; - RE::TESWorldSpace* currentPlayerWorldSpace = nullptr; - bool pendingChildWsCull = false; + std::atomic currentPlayerWorldSpace{ nullptr }; + std::atomic pendingChildWsCull{ false }; // Cached from TES_SetWorldSpace::thunk (game thread) for use on the render thread. // globals::game::tes is null on the render thread (cached before TES singleton existed). - RE::TES* cachedTes = nullptr; + std::atomic cachedTes{ nullptr }; void SetFlowmapTex() const; static bool LoadOrderChanged(); From be9be5908332fffc7a7d925154c7dd7a04bd24de Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:36:35 +0000 Subject: [PATCH 07/15] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/Features/UnifiedWater.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index aabd8575ed..fab2078668 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -42,7 +42,7 @@ static bool CullWaterParentByGridCells(RE::NiNode* waterParent, RE::TES* tes = n if (x >= 0 && y >= 0 && x < length && y < length) { if (const auto cell = gridCells->GetCell(x, y); cell && cell->cellState.any( RE::TESObjectCELL::CellState::kAttached, - static_cast(6))) { + static_cast(6))) { cull = true; foundAttachedCell = true; } From a05281f1d9703d919bfd4f65e14000c1ff52217c Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Sat, 28 Mar 2026 16:11:10 +0100 Subject: [PATCH 08/15] fix: address AI comments --- src/Features/UnifiedWater.cpp | 79 +++++++++++++++++------------------ src/Features/UnifiedWater.h | 3 -- 2 files changed, 38 insertions(+), 44 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index fab2078668..e045340ecf 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -16,20 +16,31 @@ static bool IsChildWorldSpace(const RE::TESWorldSpace* ws) ws->parentUseFlags.all(RE::TESWorldSpace::ParentUseFlag::kUseLODData); } +struct CullCompletionState +{ + bool foundAttachedCell = false; + bool hasPotentiallyAttachableChild = false; + + bool IsComplete() const + { + return foundAttachedCell && !hasPotentiallyAttachableChild; + } +}; + // Cull all tile children of waterParent based on tes->gridCells. // Pass an explicit tes when globals::game::tes is not yet populated (e.g., during TES_SetWorldSpace). -static bool CullWaterParentByGridCells(RE::NiNode* waterParent, RE::TES* tes = nullptr) +static CullCompletionState CullWaterParentByGridCells(RE::NiNode* waterParent, RE::TES* tes = nullptr) { if (!tes) tes = globals::game::tes; if (!tes || !tes->gridCells || !waterParent) - return false; + return {}; const auto& gridCells = tes->gridCells; const int32_t offsetX = tes->currentGridX - static_cast(gridCells->length >> 1); const int32_t offsetY = tes->currentGridY - static_cast(gridCells->length >> 1); const int32_t length = static_cast(gridCells->length); - bool foundAttachedCell = false; + CullCompletionState state; for (const auto& child : waterParent->GetChildren()) { if (!child) @@ -40,17 +51,20 @@ static bool CullWaterParentByGridCells(RE::NiNode* waterParent, RE::TES* tes = n y -= offsetY; bool cull = false; if (x >= 0 && y >= 0 && x < length && y < length) { - if (const auto cell = gridCells->GetCell(x, y); cell && cell->cellState.any( - RE::TESObjectCELL::CellState::kAttached, - static_cast(6))) { + const auto cell = gridCells->GetCell(x, y); + if (cell && cell->cellState.any( + RE::TESObjectCELL::CellState::kAttached, + static_cast(6))) { cull = true; - foundAttachedCell = true; + state.foundAttachedCell = true; + } else { + state.hasPotentiallyAttachableChild = true; } } child->SetAppCulled(cull); } - return foundAttachedCell; + return state; } // Cull every tile under all water LOD parent nodes. @@ -59,7 +73,7 @@ static bool CullAllWaterLODParents(RE::NiNode* waterLOD, RE::TES* tes = nullptr) if (!waterLOD) return false; - bool foundAttachedCell = false; + CullCompletionState aggregate; for (const auto& waterParentPtr : waterLOD->GetChildren()) { if (!waterParentPtr) @@ -67,10 +81,12 @@ static bool CullAllWaterLODParents(RE::NiNode* waterLOD, RE::TES* tes = nullptr) const auto waterParent = waterParentPtr->AsNode(); if (!waterParent) continue; - foundAttachedCell = CullWaterParentByGridCells(waterParent, tes) || foundAttachedCell; + const auto state = CullWaterParentByGridCells(waterParent, tes); + aggregate.foundAttachedCell = aggregate.foundAttachedCell || state.foundAttachedCell; + aggregate.hasPotentiallyAttachableChild = aggregate.hasPotentiallyAttachableChild || state.hasPotentiallyAttachableChild; } - return foundAttachedCell; + return aggregate.IsComplete(); } void UnifiedWater::LoadSettings(json& o_json) @@ -385,12 +401,16 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor // sees the correct worldspace when it fires during cell attachment inside func. auto& uw = globals::features::unifiedWater; uw.currentPlayerWorldSpace.store(worldSpace, std::memory_order_release); - uw.cachedTes.store(tes, std::memory_order_release); // globals::game::tes is null on the render thread; cache here for later use if (!enteringChild) uw.pendingChildWsCull.store(false, std::memory_order_release); // leaving child WS: discard any stale pending cull func(tes, worldSpace, isExterior); + if (!uw.waterCache) { + uw.pendingChildWsCull.store(false, std::memory_order_release); + return; + } + uw.waterCache->SetCurrentWorldSpace(worldSpace); if (enteringChild) { @@ -419,6 +439,9 @@ void UnifiedWater::TES_DestroySkyCell::thunk(RE::TES* tes) auto& uw = globals::features::unifiedWater; uw.currentPlayerWorldSpace.store(nullptr, std::memory_order_release); uw.pendingChildWsCull.store(false, std::memory_order_release); + if (!uw.waterCache) + return; + uw.waterCache->SetCurrentWorldSpace(nullptr); } @@ -438,20 +461,9 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) const auto waterSystem = RE::TESWaterSystem::GetSingleton(); const auto& singleton = globals::features::unifiedWater; - // Consume pending child WS cull on the first block attach after entering a child worldspace. - // BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces, so we cull here - // instead. Placed before instruction lookup so it fires even for blocks with no UW tiles. - { - auto& uw = globals::features::unifiedWater; - if (uw.pendingChildWsCull.load(std::memory_order_acquire) && - IsChildWorldSpace(uw.currentPlayerWorldSpace.load(std::memory_order_acquire)) && - uw.gWaterLOD && *uw.gWaterLOD) { - const auto tes = globals::game::tes; - if (tes && tes->gridCells) { - if (CullAllWaterLODParents(*uw.gWaterLOD)) - uw.pendingChildWsCull.store(false, std::memory_order_release); - } - } + if (!waterSystem || !singleton.waterCache || !singleton.gWaterLOD || !*singleton.gWaterLOD) { + func(block); + return; } std::vector> built; @@ -587,21 +599,6 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, { const auto& singleton = globals::features::unifiedWater; - // Deferred child-WS cull: cells are not kAttached immediately after TES_SetWorldSpace. - // Use cachedTes (saved on the game thread where it's valid) instead of globals::game::tes - // which is null on the render thread due to initialization ordering. - // Keep retrying (pendingChildWsCull stays true) until gridCells is populated with kAttached cells. - if (singleton.pendingChildWsCull.load(std::memory_order_acquire) && - IsChildWorldSpace(singleton.currentPlayerWorldSpace.load(std::memory_order_acquire)) && - singleton.gWaterLOD && *singleton.gWaterLOD) { - const auto tes = singleton.cachedTes.load(std::memory_order_acquire); - if (tes && tes->gridCells) { - auto& uw = globals::features::unifiedWater; - if (CullAllWaterLODParents(*uw.gWaterLOD, tes)) - uw.pendingChildWsCull.store(false, std::memory_order_release); - } - } - // Fix BSWaterShaderProperty.plane after interior->exterior transitions. // The plane feeds ReflectPlane in the PerGeometry cbuffer. When corrupted (e.g., plane.constant = 0 // or garbage), the shader's refractionPlaneMul calculation produces extreme values causing flickering. diff --git a/src/Features/UnifiedWater.h b/src/Features/UnifiedWater.h index c6c8afe3c0..6b0d883d06 100644 --- a/src/Features/UnifiedWater.h +++ b/src/Features/UnifiedWater.h @@ -115,9 +115,6 @@ struct UnifiedWater : OverlayFeature std::atomic currentPlayerWorldSpace{ nullptr }; std::atomic pendingChildWsCull{ false }; - // Cached from TES_SetWorldSpace::thunk (game thread) for use on the render thread. - // globals::game::tes is null on the render thread (cached before TES singleton existed). - std::atomic cachedTes{ nullptr }; void SetFlowmapTex() const; static bool LoadOrderChanged(); From c890c90c0c08a7c6734307fa7ead70356d5ac91a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:16:54 +0000 Subject: [PATCH 09/15] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/Features/UnifiedWater.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index e045340ecf..4087ce6b81 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -53,8 +53,8 @@ static CullCompletionState CullWaterParentByGridCells(RE::NiNode* waterParent, R if (x >= 0 && y >= 0 && x < length && y < length) { const auto cell = gridCells->GetCell(x, y); if (cell && cell->cellState.any( - RE::TESObjectCELL::CellState::kAttached, - static_cast(6))) { + RE::TESObjectCELL::CellState::kAttached, + static_cast(6))) { cull = true; state.foundAttachedCell = true; } else { From 34e681c403dfe8d61743125fb4070eb75f03de16 Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Sat, 28 Mar 2026 16:55:01 +0100 Subject: [PATCH 10/15] fix: re-fix the AI comments --- src/Features/UnifiedWater.cpp | 39 ++++++++++++++++++++++++++--------- src/Features/UnifiedWater.h | 4 ++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index e045340ecf..7a47106da9 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -89,6 +89,22 @@ static bool CullAllWaterLODParents(RE::NiNode* waterLOD, RE::TES* tes = nullptr) return aggregate.IsComplete(); } +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 = globals::game::tes; + if (!tes || !tes->gridCells) + return; + + if (CullAllWaterLODParents(*gWaterLOD, tes)) + pendingChildWsCull.store(false, std::memory_order_release); +} + void UnifiedWater::LoadSettings(json& o_json) { settings = o_json; @@ -401,6 +417,7 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor // sees the correct worldspace when it fires during cell attachment inside func. auto& uw = globals::features::unifiedWater; uw.currentPlayerWorldSpace.store(worldSpace, std::memory_order_release); + uw.cachedTes.store(tes, std::memory_order_release); if (!enteringChild) uw.pendingChildWsCull.store(false, std::memory_order_release); // leaving child WS: discard any stale pending cull @@ -439,6 +456,7 @@ void UnifiedWater::TES_DestroySkyCell::thunk(RE::TES* tes) auto& uw = globals::features::unifiedWater; uw.currentPlayerWorldSpace.store(nullptr, std::memory_order_release); uw.pendingChildWsCull.store(false, std::memory_order_release); + uw.cachedTes.store(nullptr, std::memory_order_release); if (!uw.waterCache) return; @@ -459,13 +477,16 @@ void UnifiedWater::BGSTerrainNode_UpdateWaterMeshSubVisibility::thunk(const RE:: void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) { const auto waterSystem = RE::TESWaterSystem::GetSingleton(); - const auto& singleton = globals::features::unifiedWater; + auto& singleton = globals::features::unifiedWater; if (!waterSystem || !singleton.waterCache || !singleton.gWaterLOD || !*singleton.gWaterLOD) { func(block); return; } + // Additional game-thread retry path for deferred child-WS cull completion. + singleton.TryCompleteDeferredChildWorldspaceCull(); + std::vector> built; bool attaching = false; @@ -599,6 +620,12 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, { const auto& singleton = globals::features::unifiedWater; + // Fallback deferred cull path for frames where game-thread retry points are not reached in time. + { + auto& uw = globals::features::unifiedWater; + uw.TryCompleteDeferredChildWorldspaceCull(singleton.cachedTes.load(std::memory_order_acquire)); + } + // Fix BSWaterShaderProperty.plane after interior->exterior transitions. // The plane feeds ReflectPlane in the PerGeometry cbuffer. When corrupted (e.g., plane.constant = 0 // or garbage), the shader's refractionPlaneMul calculation produces extreme values causing flickering. @@ -657,15 +684,7 @@ void UnifiedWater::TESWaterSystem_UpdateDisplacementMeshPosition::thunk(RE::TESW // BGSTerrainBlock_Attach doesn't fire for already-attached LOD blocks when entering a child // worldspace, and BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces. // This hook runs on the game thread with valid tes/gridCells, so consume the pending cull here. - if (uw.pendingChildWsCull.load(std::memory_order_acquire) && - IsChildWorldSpace(uw.currentPlayerWorldSpace.load(std::memory_order_acquire)) && - uw.gWaterLOD && *uw.gWaterLOD) { - const auto tes = globals::game::tes; - if (tes && tes->gridCells) { - if (CullAllWaterLODParents(*uw.gWaterLOD)) - uw.pendingChildWsCull.store(false, std::memory_order_release); - } - } + uw.TryCompleteDeferredChildWorldspaceCull(); if (!uw.flowmap) return; diff --git a/src/Features/UnifiedWater.h b/src/Features/UnifiedWater.h index 6b0d883d06..44602c94ef 100644 --- a/src/Features/UnifiedWater.h +++ b/src/Features/UnifiedWater.h @@ -115,6 +115,10 @@ struct UnifiedWater : OverlayFeature std::atomic currentPlayerWorldSpace{ nullptr }; std::atomic pendingChildWsCull{ false }; + // Cached from TES_SetWorldSpace::thunk (game thread) for deferred cull fallback. + std::atomic cachedTes{ nullptr }; + + void TryCompleteDeferredChildWorldspaceCull(RE::TES* tes = nullptr); void SetFlowmapTex() const; static bool LoadOrderChanged(); From 5f3bb69287b950537b93db53df50f1acd8a750df Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Sat, 28 Mar 2026 17:18:08 +0100 Subject: [PATCH 11/15] fix: AI nitpicks and less verbose comments --- src/Features/UnifiedWater.cpp | 108 +++++++++++++++++----------------- src/Features/UnifiedWater.h | 2 +- 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 33d8f4549d..004ddd6641 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -16,6 +16,36 @@ static bool IsChildWorldSpace(const RE::TESWorldSpace* ws) ws->parentUseFlags.all(RE::TESWorldSpace::ParentUseFlag::kUseLODData); } +// Engine transition state treated as attached for culling. +static constexpr auto kTransitionAttachedCellState = static_cast(6); + +static bool ShouldCullAtCell(const RE::TES* tes, int32_t cellX, int32_t cellY, bool* isInGrid = nullptr) +{ + if (isInGrid) + *isInGrid = false; + if (!tes || !tes->gridCells) + return false; + + const auto& gridCells = tes->gridCells; + const int32_t offsetX = tes->currentGridX - static_cast(gridCells->length >> 1); + const int32_t offsetY = tes->currentGridY - static_cast(gridCells->length >> 1); + const int32_t length = static_cast(gridCells->length); + + const int32_t x = cellX - offsetX; + const int32_t y = cellY - offsetY; + if (x < 0 || y < 0 || x >= length || y >= length) + return false; + + if (isInGrid) + *isInGrid = true; + + if (const auto cell = gridCells->GetCell(x, y)) { + return cell->cellState.any(RE::TESObjectCELL::CellState::kAttached, kTransitionAttachedCellState); + } + + return false; +} + struct CullCompletionState { bool foundAttachedCell = false; @@ -27,19 +57,15 @@ struct CullCompletionState } }; -// Cull all tile children of waterParent based on tes->gridCells. -// Pass an explicit tes when globals::game::tes is not yet populated (e.g., during TES_SetWorldSpace). +// Cull waterParent children using tes->gridCells attachment state. +// Pass tes explicitly when globals::game::tes is not ready (e.g., TES_SetWorldSpace). static CullCompletionState CullWaterParentByGridCells(RE::NiNode* waterParent, RE::TES* tes = nullptr) { if (!tes) tes = globals::game::tes; - if (!tes || !tes->gridCells || !waterParent) + if (!tes || !waterParent) return {}; - const auto& gridCells = tes->gridCells; - const int32_t offsetX = tes->currentGridX - static_cast(gridCells->length >> 1); - const int32_t offsetY = tes->currentGridY - static_cast(gridCells->length >> 1); - const int32_t length = static_cast(gridCells->length); CullCompletionState state; for (const auto& child : waterParent->GetChildren()) { @@ -47,27 +73,19 @@ static CullCompletionState CullWaterParentByGridCells(RE::NiNode* waterParent, R continue; int32_t x, y; Util::WorldToCell(child->world.translate, x, y); - x -= offsetX; - y -= offsetY; - bool cull = false; - if (x >= 0 && y >= 0 && x < length && y < length) { - const auto cell = gridCells->GetCell(x, y); - if (cell && cell->cellState.any( - RE::TESObjectCELL::CellState::kAttached, - static_cast(6))) { - cull = true; - state.foundAttachedCell = true; - } else { - state.hasPotentiallyAttachableChild = true; - } - } + bool isInGrid = false; + const bool cull = ShouldCullAtCell(tes, x, y, &isInGrid); + if (cull) + state.foundAttachedCell = true; + else if (isInGrid) + state.hasPotentiallyAttachableChild = true; child->SetAppCulled(cull); } return state; } -// Cull every tile under all water LOD parent nodes. +// Cull all tiles under every water LOD parent. static bool CullAllWaterLODParents(RE::NiNode* waterLOD, RE::TES* tes = nullptr) { if (!waterLOD) @@ -413,8 +431,7 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor { const bool enteringChild = IsChildWorldSpace(worldSpace); - // Set currentPlayerWorldSpace BEFORE func so BGSTerrainNode_UpdateWaterMeshSubVisibility - // sees the correct worldspace when it fires during cell attachment inside func. + // Set before func so attachment hooks fired inside func see the new worldspace. auto& uw = globals::features::unifiedWater; uw.currentPlayerWorldSpace.store(worldSpace, std::memory_order_release); uw.cachedTes.store(tes, std::memory_order_release); @@ -431,20 +448,17 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor uw.waterCache->SetCurrentWorldSpace(worldSpace); if (enteringChild) { - // BGSTerrainBlock_Attach calls waterSystem->Enable() when blocks attach. - // In child worldspaces, already-attached LOD blocks don't re-attach, so Enable() - // is never called after the transition — leaving LOD water tiles unanimated. + // BGSTerrainBlock_Attach calls Enable() on block attach. + // Child-worldspace transitions can keep old LOD blocks attached, so re-enable here. if (const auto waterSystem = RE::TESWaterSystem::GetSingleton()) waterSystem->Enable(); - // Try an immediate cull using the tes parameter (globals::game::tes may be null here). - // Cells just transitioned: they're likely not kAttached yet, so this usually culls 0 tiles. - // pendingChildWsCull is always set so deferred hooks can retry once cells load. + // Try an immediate cull with tes (globals::game::tes may still be null). + // Newly transitioned cells are often not attached yet, so deferred retries are still needed. if (uw.gWaterLOD && *uw.gWaterLOD && tes && tes->gridCells) CullAllWaterLODParents(*uw.gWaterLOD, tes); - // Always set the deferred flag: cells may not be kAttached until several frames later, - // and deferred cull hooks will retry until the cull actually takes effect. + // Keep deferred retries enabled until attached cells are observed and culled. uw.pendingChildWsCull.store(true, std::memory_order_release); } } @@ -569,27 +583,13 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) waterSystem->Enable(); // BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces. - // Cull new tiles immediately, and if a full cull pass is pending (transition case - // where pre-existing LOD blocks weren't re-attached), do it now that tes/gridCells - // are valid. + // Cull newly built tiles here; full deferred retries are handled by + // TryCompleteDeferredChildWorldspaceCull(). if (IsChildWorldSpace(singleton.currentPlayerWorldSpace.load(std::memory_order_acquire))) { const auto tes = globals::game::tes; if (tes && tes->gridCells) { - // Cull the new tiles for this block - const auto& gridCells = tes->gridCells; - const int32_t offsetX = tes->currentGridX - static_cast(gridCells->length >> 1); - const int32_t offsetY = tes->currentGridY - static_cast(gridCells->length >> 1); - const int32_t length = static_cast(gridCells->length); for (const auto& [shape, instruction] : built) { - const int32_t ix = instruction->x - offsetX; - const int32_t iy = instruction->y - offsetY; - bool cull = false; - if (ix >= 0 && iy >= 0 && ix < length && iy < length) { - if (const auto cell = gridCells->GetCell(ix, iy); cell && cell->cellState.any( - RE::TESObjectCELL::CellState::kAttached, - static_cast(6))) - cull = true; - } + const bool cull = ShouldCullAtCell(tes, instruction->x, instruction->y); shape->SetAppCulled(cull); } } @@ -620,7 +620,9 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, { const auto& singleton = globals::features::unifiedWater; - // Fallback deferred cull path for frames where game-thread retry points are not reached in time. + // Render-thread fallback for deferred child-worldspace cull completion. + // cachedTes/grid state can be stale while the game thread mutates terrain state. + // pendingChildWsCull keeps retrying, so stale reads only delay culling for a frame. { auto& uw = globals::features::unifiedWater; uw.TryCompleteDeferredChildWorldspaceCull(singleton.cachedTes.load(std::memory_order_acquire)); @@ -681,9 +683,9 @@ void UnifiedWater::TESWaterSystem_UpdateDisplacementMeshPosition::thunk(RE::TESW auto& uw = globals::features::unifiedWater; - // BGSTerrainBlock_Attach doesn't fire for already-attached LOD blocks when entering a child - // worldspace, and BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces. - // This hook runs on the game thread with valid tes/gridCells, so consume the pending cull here. + // Game-thread fallback for deferred child-worldspace cull completion. + // Needed when entering child worldspaces with already-attached LOD blocks, + // where BGSTerrainBlock_Attach/UpdateWaterMeshSubVisibility may not run. uw.TryCompleteDeferredChildWorldspaceCull(); if (!uw.flowmap) diff --git a/src/Features/UnifiedWater.h b/src/Features/UnifiedWater.h index 44602c94ef..06cc01e7b4 100644 --- a/src/Features/UnifiedWater.h +++ b/src/Features/UnifiedWater.h @@ -115,7 +115,7 @@ struct UnifiedWater : OverlayFeature std::atomic currentPlayerWorldSpace{ nullptr }; std::atomic pendingChildWsCull{ false }; - // Cached from TES_SetWorldSpace::thunk (game thread) for deferred cull fallback. + // Game-thread TES snapshot used by deferred child-worldspace cull fallbacks. std::atomic cachedTes{ nullptr }; void TryCompleteDeferredChildWorldspaceCull(RE::TES* tes = nullptr); From 607aefa8052df9d1a847b22a11cac652e0962cae Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Sat, 28 Mar 2026 19:37:31 +0100 Subject: [PATCH 12/15] fix: move to globals, improve styling --- src/Features/UnifiedWater.cpp | 40 +++++++++++++++++------------------ src/Globals.cpp | 3 +++ src/Globals.h | 1 + 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 004ddd6641..82c268339f 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -450,7 +450,7 @@ void UnifiedWater::TES_SetWorldSpace::thunk(RE::TES* tes, RE::TESWorldSpace* wor if (enteringChild) { // BGSTerrainBlock_Attach calls Enable() on block attach. // Child-worldspace transitions can keep old LOD blocks attached, so re-enable here. - if (const auto waterSystem = RE::TESWaterSystem::GetSingleton()) + if (const auto waterSystem = globals::game::waterSystem) waterSystem->Enable(); // Try an immediate cull with tes (globals::game::tes may still be null). @@ -490,16 +490,16 @@ void UnifiedWater::BGSTerrainNode_UpdateWaterMeshSubVisibility::thunk(const RE:: void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) { - const auto waterSystem = RE::TESWaterSystem::GetSingleton(); - auto& singleton = globals::features::unifiedWater; + const auto waterSystem = globals::game::waterSystem; + auto& uw = globals::features::unifiedWater; - if (!waterSystem || !singleton.waterCache || !singleton.gWaterLOD || !*singleton.gWaterLOD) { + if (!waterSystem || !uw.waterCache || !uw.gWaterLOD || !*uw.gWaterLOD) { func(block); return; } // Additional game-thread retry path for deferred child-WS cull completion. - singleton.TryCompleteDeferredChildWorldspaceCull(); + uw.TryCompleteDeferredChildWorldspaceCull(); std::vector> built; bool attaching = false; @@ -525,7 +525,7 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) const auto lodLevel = node->GetLODLevel(); const auto worldSpace = block->node->manager->worldSpace; - const auto instructions = singleton.waterCache->GetInstructions(worldSpace, lodLevel, node->baseCellX, node->baseCellY); + const auto instructions = uw.waterCache->GetInstructions(worldSpace, lodLevel, node->baseCellX, node->baseCellY); if (!instructions) { logger::warn("[Unified Water] No instructions found for {} chunk at {}, {}", worldSpace->GetFormEditorID(), node->baseCellX, node->baseCellY); func(block); @@ -538,7 +538,7 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) RE::NiCloningProcess cloningProcess; - const auto targetShape = lodLevel > 4 || singleton.settings.UseOptimisedMeshes ? singleton.optimisedWaterMesh : singleton.waterMesh; + 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; @@ -579,13 +579,13 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) } } - (*singleton.gWaterLOD)->AttachChild(block->water, true); + (*uw.gWaterLOD)->AttachChild(block->water, true); waterSystem->Enable(); // BGSTerrainNode_UpdateWaterMeshSubVisibility never fires in child worldspaces. // Cull newly built tiles here; full deferred retries are handled by // TryCompleteDeferredChildWorldspaceCull(). - if (IsChildWorldSpace(singleton.currentPlayerWorldSpace.load(std::memory_order_acquire))) { + if (IsChildWorldSpace(uw.currentPlayerWorldSpace.load(std::memory_order_acquire))) { const auto tes = globals::game::tes; if (tes && tes->gridCells) { for (const auto& [shape, instruction] : built) { @@ -598,6 +598,7 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) void UnifiedWater::BGSTerrainBlock_Detach::thunk(RE::BGSTerrainBlock* block) { + auto& uw = globals::features::unifiedWater; const auto water = block->water; block->water = nullptr; @@ -611,22 +612,19 @@ void UnifiedWater::BGSTerrainBlock_Detach::thunk(RE::BGSTerrainBlock* block) water->DetachChildAt(--count); } - (*globals::features::unifiedWater.gWaterLOD)->DetachChild(water); + (*uw.gWaterLOD)->DetachChild(water); block->waterAttached = false; } } void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, RE::BSRenderPass* pass) { - const auto& singleton = globals::features::unifiedWater; + auto& uw = globals::features::unifiedWater; // Render-thread fallback for deferred child-worldspace cull completion. // cachedTes/grid state can be stale while the game thread mutates terrain state. // pendingChildWsCull keeps retrying, so stale reads only delay culling for a frame. - { - auto& uw = globals::features::unifiedWater; - uw.TryCompleteDeferredChildWorldspaceCull(singleton.cachedTes.load(std::memory_order_acquire)); - } + uw.TryCompleteDeferredChildWorldspaceCull(uw.cachedTes.load(std::memory_order_acquire)); // Fix BSWaterShaderProperty.plane after interior->exterior transitions. // The plane feeds ReflectPlane in the PerGeometry cbuffer. When corrupted (e.g., plane.constant = 0 @@ -652,12 +650,12 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, } } - if (singleton.flowmap) { + if (uw.flowmap) { // ObjectUV.xyz below, xy contains width and height, z contains mesh scale // Previously flowmap size was in x, yz contained flowmap offset for water displacement mesh - *singleton.gFlowMapSize = singleton.flowmap->GetWidth(); // ObjectUV.x - singleton.gDisplacementMeshFlowCellOffset->x = static_cast(singleton.flowmap->GetHeight()); // ObjectUV.y - singleton.gDisplacementMeshFlowCellOffset->y = 1.0f - pass->geometry->local.scale; // ObjectUV.z (counters 1 - x in SetupGeometry) + *uw.gFlowMapSize = uw.flowmap->GetWidth(); // ObjectUV.x + uw.gDisplacementMeshFlowCellOffset->x = static_cast(uw.flowmap->GetHeight()); // ObjectUV.y + uw.gDisplacementMeshFlowCellOffset->y = 1.0f - pass->geometry->local.scale; // ObjectUV.z (counters 1 - x in SetupGeometry) if (const auto prop = pass->geometry->GetGeometryRuntimeData().shaderProperty.get(); prop && prop->GetRTTI() == globals::rtti::BSWaterShaderPropertyRTTI.get()) { const auto waterShaderProp = static_cast(prop); @@ -667,8 +665,8 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, // xy is world cell flowmap based (0,0 is corner of flow map), zw is world cell // Funky maths here to counter what's being done in SetupGeometry // Previously these values were relative to the 5x5 flow grid centered on the player - waterShaderProp->flowX = x + singleton.flowmap->GetOffsetX(); // CellTexCoordOffset.x - waterShaderProp->flowY = y + singleton.flowmap->GetOffsetY() + singleton.flowmap->GetWidth() - singleton.flowmap->GetHeight(); // CellTexCoordOffset.y + waterShaderProp->flowX = x + uw.flowmap->GetOffsetX(); // CellTexCoordOffset.x + waterShaderProp->flowY = y + uw.flowmap->GetOffsetY() + uw.flowmap->GetWidth() - uw.flowmap->GetHeight(); // CellTexCoordOffset.y waterShaderProp->cellX = x; // CellTexCoordOffset.z waterShaderProp->cellY = y; // CellTexCoordOffset.w } diff --git a/src/Globals.cpp b/src/Globals.cpp index e90c3bf4ce..a3c12569a9 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -97,6 +97,7 @@ namespace globals RE::BSGraphics::Renderer* renderer = nullptr; RE::BSShaderManager::State* smState = nullptr; RE::TES* tes = nullptr; + RE::TESWaterSystem* waterSystem = nullptr; bool isVR = false; RE::MemoryManager* memoryManager = nullptr; RE::INISettingCollection* iniSettingCollection = nullptr; @@ -167,6 +168,7 @@ namespace globals iniPrefSettingCollection = RE::INIPrefSettingCollection::GetSingleton(); gameSettingCollection = RE::GameSettingCollection::GetSingleton(); tes = RE::TES::GetSingleton(); + waterSystem = RE::TESWaterSystem::GetSingleton(); cameraNear = (float*)(REL::RelocationID(517032, 403540).address() + 0x40); cameraFar = (float*)(REL::RelocationID(517032, 403540).address() + 0x44); deltaTime = (float*)REL::RelocationID(523660, 410199).address(); @@ -204,6 +206,7 @@ namespace globals using namespace game; sky = RE::Sky::GetSingleton(); utilityShader = RE::BSUtilityShader::GetSingleton(); + waterSystem = RE::TESWaterSystem::GetSingleton(); bEnableLandFade = iniSettingCollection->GetSetting("bEnableLandFade:Display"); diff --git a/src/Globals.h b/src/Globals.h index fa96446891..194f61668f 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -211,6 +211,7 @@ namespace globals extern RE::BSGraphics::Renderer* renderer; extern RE::BSShaderManager::State* smState; extern RE::TES* tes; + extern RE::TESWaterSystem* waterSystem; extern bool isVR; extern RE::MemoryManager* memoryManager; extern RE::INISettingCollection* iniSettingCollection; From 1ea544eaf902c4dec3f86cc47aaa2c5ebd123f1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:38:03 +0000 Subject: [PATCH 13/15] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/Features/UnifiedWater.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 82c268339f..ed9291c8d4 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -655,7 +655,7 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, // Previously flowmap size was in x, yz contained flowmap offset for water displacement mesh *uw.gFlowMapSize = uw.flowmap->GetWidth(); // ObjectUV.x uw.gDisplacementMeshFlowCellOffset->x = static_cast(uw.flowmap->GetHeight()); // ObjectUV.y - uw.gDisplacementMeshFlowCellOffset->y = 1.0f - pass->geometry->local.scale; // ObjectUV.z (counters 1 - x in SetupGeometry) + uw.gDisplacementMeshFlowCellOffset->y = 1.0f - pass->geometry->local.scale; // ObjectUV.z (counters 1 - x in SetupGeometry) if (const auto prop = pass->geometry->GetGeometryRuntimeData().shaderProperty.get(); prop && prop->GetRTTI() == globals::rtti::BSWaterShaderPropertyRTTI.get()) { const auto waterShaderProp = static_cast(prop); @@ -665,10 +665,10 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, // xy is world cell flowmap based (0,0 is corner of flow map), zw is world cell // Funky maths here to counter what's being done in SetupGeometry // Previously these values were relative to the 5x5 flow grid centered on the player - waterShaderProp->flowX = x + uw.flowmap->GetOffsetX(); // CellTexCoordOffset.x + waterShaderProp->flowX = x + uw.flowmap->GetOffsetX(); // CellTexCoordOffset.x waterShaderProp->flowY = y + uw.flowmap->GetOffsetY() + uw.flowmap->GetWidth() - uw.flowmap->GetHeight(); // CellTexCoordOffset.y - waterShaderProp->cellX = x; // CellTexCoordOffset.z - waterShaderProp->cellY = y; // CellTexCoordOffset.w + waterShaderProp->cellX = x; // CellTexCoordOffset.z + waterShaderProp->cellY = y; // CellTexCoordOffset.w } } From e72e038421a28dbcc3fc49b9e139d4fe0df76695 Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Sat, 28 Mar 2026 20:18:24 +0100 Subject: [PATCH 14/15] fix: replace globals::game::tes with cachedTes for improved thread safety --- src/Features/UnifiedWater.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 82c268339f..b129b41d5d 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -115,7 +115,7 @@ void UnifiedWater::TryCompleteDeferredChildWorldspaceCull(RE::TES* tes) return; if (!tes) - tes = globals::game::tes; + tes = cachedTes.load(std::memory_order_acquire); if (!tes || !tes->gridCells) return; @@ -499,7 +499,7 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) } // Additional game-thread retry path for deferred child-WS cull completion. - uw.TryCompleteDeferredChildWorldspaceCull(); + uw.TryCompleteDeferredChildWorldspaceCull(uw.cachedTes.load(std::memory_order_acquire)); std::vector> built; bool attaching = false; @@ -586,7 +586,7 @@ void UnifiedWater::BGSTerrainBlock_Attach::thunk(RE::BGSTerrainBlock* block) // Cull newly built tiles here; full deferred retries are handled by // TryCompleteDeferredChildWorldspaceCull(). if (IsChildWorldSpace(uw.currentPlayerWorldSpace.load(std::memory_order_acquire))) { - const auto tes = globals::game::tes; + const auto tes = uw.cachedTes.load(std::memory_order_acquire); if (tes && tes->gridCells) { for (const auto& [shape, instruction] : built) { const bool cull = ShouldCullAtCell(tes, instruction->x, instruction->y); @@ -621,11 +621,6 @@ void UnifiedWater::BSWaterShader_SetupGeometry::thunk(RE::BSShader* waterShader, { auto& uw = globals::features::unifiedWater; - // Render-thread fallback for deferred child-worldspace cull completion. - // cachedTes/grid state can be stale while the game thread mutates terrain state. - // pendingChildWsCull keeps retrying, so stale reads only delay culling for a frame. - uw.TryCompleteDeferredChildWorldspaceCull(uw.cachedTes.load(std::memory_order_acquire)); - // Fix BSWaterShaderProperty.plane after interior->exterior transitions. // The plane feeds ReflectPlane in the PerGeometry cbuffer. When corrupted (e.g., plane.constant = 0 // or garbage), the shader's refractionPlaneMul calculation produces extreme values causing flickering. @@ -684,7 +679,7 @@ void UnifiedWater::TESWaterSystem_UpdateDisplacementMeshPosition::thunk(RE::TESW // Game-thread fallback for deferred child-worldspace cull completion. // Needed when entering child worldspaces with already-attached LOD blocks, // where BGSTerrainBlock_Attach/UpdateWaterMeshSubVisibility may not run. - uw.TryCompleteDeferredChildWorldspaceCull(); + uw.TryCompleteDeferredChildWorldspaceCull(uw.cachedTes.load(std::memory_order_acquire)); if (!uw.flowmap) return; From a527741b19174622b8ca5c2bb1df7ddc0167b202 Mon Sep 17 00:00:00 2001 From: SkrubbySkrubInAShrub Date: Sat, 28 Mar 2026 20:27:04 +0100 Subject: [PATCH 15/15] fix: address AI comments --- src/Features/UnifiedWater.cpp | 2 +- src/Globals.h | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 02d280ec84..ca11bb74ec 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -16,7 +16,7 @@ static bool IsChildWorldSpace(const RE::TESWorldSpace* ws) ws->parentUseFlags.all(RE::TESWorldSpace::ParentUseFlag::kUseLODData); } -// Engine transition state treated as attached for culling. +// Engine behavior: CellState value 6 is the transition/attached state. static constexpr auto kTransitionAttachedCellState = static_cast(6); static bool ShouldCullAtCell(const RE::TES* tes, int32_t cellX, int32_t cellY, bool* isInGrid = nullptr) diff --git a/src/Globals.h b/src/Globals.h index 194f61668f..d7c85eff14 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -217,7 +217,6 @@ namespace globals extern RE::INISettingCollection* iniSettingCollection; extern RE::INIPrefSettingCollection* iniPrefSettingCollection; extern RE::GameSettingCollection* gameSettingCollection; - extern RE::TES* tes; extern float* cameraNear; extern float* cameraFar; extern float* deltaTime;