Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion features/Terrain Helper/Shaders/Features/TerrainHelper.ini
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[Info]
Version = 1-0-1
AuditVersion = false

[Nexus]
nexusmodid = 143149
nexusfilegroupid = 000000
nexusfilename = Terrain Helper
autoupload = false
autoupload = true
10 changes: 8 additions & 2 deletions package/Shaders/ISWaterBlend.hlsl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "Common/DummyVSTexCoord.hlsl"
#include "Common/FrameBuffer.hlsli"
#include "Common/Math.hlsli"
#include "Common/VR.hlsli"

typedef VS_OUTPUT PS_INPUT;
Expand Down Expand Up @@ -75,11 +76,16 @@ PS_OUTPUT main(PS_INPUT input)
0.1, 0.95);
historyFactor = NearFar_Menu_DistanceFactor.w * (distanceFactor * (waterMask * -0.85 + 0.95));
}
// Un-premultiply history so bilinear filtering against cleared pixels does not darken water edges
float3 historyColor = waterHistory.xyz / max(waterHistory.w, EPSILON_DIVISION);

historyFactor *= waterHistory.w;
finalColor = lerp(sourceColor, waterHistory.xyz, historyFactor);
finalColor = lerp(sourceColor, historyColor, historyFactor);
}

psout.Color1 = float4(finalColor, WaterBlend::GetWaterCoverage(waterMask));
float waterCoverage = WaterBlend::GetWaterCoverage(waterMask);
// Store premultiplied history so transparent clears filter without dark outlines
psout.Color1 = float4(finalColor * waterCoverage, waterCoverage);
psout.Color = finalColor;

return psout;
Expand Down
17 changes: 11 additions & 6 deletions package/Shaders/Lighting.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -1108,12 +1108,17 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace)
if (SharedData::extendedMaterialSettings.EnableComplexMaterial) {
const float kMaskEpsilon = (4.0 / 255.0);

complexMaterial = TexEnvMaskSampler.SampleLevel(SampEnvMaskSampler, uv, 15).w < (1.0 - kMaskEpsilon);

// Detect texture saved in the wrong format
if ((abs(envMaskSample.x - envMaskSample.y) < kMaskEpsilon) &&
(abs(envMaskSample.x - envMaskSample.z) < kMaskEpsilon) &&
(abs(envMaskSample.y - envMaskSample.z) < kMaskEpsilon))
const float4 mipSample = TexEnvMaskSampler.SampleLevel(SampEnvMaskSampler, uv, 15);
complexMaterial = mipSample.w < (1.0 - kMaskEpsilon);

const bool grayscaleMask = (abs(mipSample.x - mipSample.y) < kMaskEpsilon) &&
(abs(mipSample.x - mipSample.z) < kMaskEpsilon) &&
(abs(mipSample.y - mipSample.z) < kMaskEpsilon);
// Preserve height-only masks while rejecting grayscale environment masks
const bool solidBlackHeightMask = all(mipSample.xyz < kMaskEpsilon) &&
mipSample.w > kMaskEpsilon &&
mipSample.w < (1.0 - kMaskEpsilon);
if (grayscaleMask && !solidBlackHeightMask)
complexMaterial = false;

if (complexMaterial) {
Expand Down
6 changes: 4 additions & 2 deletions src/Features/UnifiedWater.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,10 @@ void UnifiedWater::BGSTerrainNode_UpdateWaterMeshSubVisibility::thunk(const RE::

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<RE::TESObjectCELL::CellState>(6)))
cull = true;
if (const auto cell = gridCells->GetCell(x, y); cell && cell->cellState.any(RE::TESObjectCELL::CellState::kAttached, static_cast<RE::TESObjectCELL::CellState>(6))) {
// Keep LOD visible when a loaded dry cell has no active water to replace it
cull = cell->cellFlags.any(RE::TESObjectCELL::Flag::kHasWater);
}
}

child->SetAppCulled(cull);
Expand Down
48 changes: 48 additions & 0 deletions src/Hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,28 @@ namespace GrassExtensions
};
}

namespace WaterBlendHistory
{
struct BSImagespaceShader_Render
{
static void thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param)
{
GET_INSTANCE_MEMBER(renderTargets, globals::game::shadowState)

// Clear stale coverage left by discarded non-water pixels
const float clearColor[4] = { 0.f, 0.f, 0.f, 0.f };
const auto target = renderTargets[1];
globals::d3d::context->ClearRenderTargetView(
globals::game::renderer->GetRuntimeData().renderTargets[target].RTV,
clearColor);

func(imageSpaceShader, shape, param);
}

static inline REL::Relocation<decltype(thunk)> func;
};
}

struct IDXGISwapChain_Present
{
static HRESULT WINAPI thunk(IDXGISwapChain* This, UINT SyncInterval, UINT Flags)
Expand Down Expand Up @@ -559,6 +581,29 @@ namespace Hooks
static inline REL::Relocation<decltype(thunk)> func;
};

// kSNOW / kSNOW_SWAP are created at R8G8B8A8_UNORM by vanilla; the snow shader
// writes accumulated wetness/sparkle values that exceed the 8-bit range and
// quantize into visible banding on tessellated snow. Promote to fp16 for headroom.
struct CreateRenderTarget_Snow
{
static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties)
{
a_properties->format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT);
func(This, a_target, a_properties);
}
static inline REL::Relocation<decltype(thunk)> func;
};

struct CreateRenderTarget_SnowSwap
{
static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties)
{
a_properties->format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT);
func(This, a_target, a_properties);
}
static inline REL::Relocation<decltype(thunk)> func;
};

// kNORMAL_TAAMASK_SSRMASK and its swap need UAV bind because DeferredCompositeCS
// writes vanilla-encoded normals through UAV1 (`normals.UAV` in Deferred::DeferredPasses),
// which feeds the post-pass vanilla SSAO chain (ISSAORawAO -> ISSAOComposite). Without
Expand Down Expand Up @@ -958,6 +1003,8 @@ namespace Hooks

logger::info("Hooking BSShaderRenderTargets::Create::CreateRenderTarget(s)");
stl::write_thunk_call<CreateRenderTarget_Main>(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x3F0, 0x3F3, 0x548));
stl::write_thunk_call<CreateRenderTarget_Snow>(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x406, 0x409, 0x55E));
stl::write_thunk_call<CreateRenderTarget_SnowSwap>(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x41C, 0x41F, 0x574));
stl::write_thunk_call<CreateRenderTarget_Normals>(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x458, 0x45B, 0x5B0));
stl::write_thunk_call<CreateRenderTarget_NormalsSwap>(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x46B, 0x46E, 0x5C3));
stl::write_thunk_call<CreateRenderTarget_MotionVectors>(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x4F0, 0x4EF, 0x64E));
Expand All @@ -977,6 +1024,7 @@ namespace Hooks

logger::info("Hooking BSImagespaceShader");
stl::detour_thunk<CSShadersSupport::BSImagespaceShader_DispatchComputeShader>(REL::RelocationID(100952, 107734));
stl::write_vfunc<0x1, WaterBlendHistory::BSImagespaceShader_Render>(RE::VTABLE_BSImagespaceShaderISWaterBlend[3]);

logger::info("Hooking BSComputeShader");
stl::write_vfunc<0x02, CSShadersSupport::BSComputeShader_Dispatch>(RE::VTABLE_BSComputeShader[0]);
Expand Down
17 changes: 16 additions & 1 deletion tools/feature_version_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def get_feature_ini_metadata(feature_dir_or_ini_path):
if not sections:
sections = ['Info'] if parser.has_section('Info') else []

metadata = {'auto_upload': False}
metadata = {'auto_upload': False, 'audit_version': True}
for section in sections:
if not parser.has_section(section):
continue
Expand All @@ -196,6 +196,15 @@ def get_feature_ini_metadata(feature_dir_or_ini_path):
if auto_upload_str is not None:
metadata['auto_upload'] = str(auto_upload_str).strip().lower() not in ('false', '0', 'no', 'off', '')

# Activation-only features keep all code in the core mod; their toggle
# .ini opts out of version auditing/bumping with `AuditVersion = false`.
# Auditing is on by default, so a blank value must NOT exclude the ini:
# only explicit falsy values opt out (unlike auto_upload, which is opt-in
# and treats blank as off).
audit_version_str = section_items.get('auditversion') or section_items.get('audit_version')
if audit_version_str is not None:
metadata['audit_version'] = str(audit_version_str).strip().lower() not in ('false', '0', 'no', 'off')

section_metadata = {
'mod_id': section_items.get('nexusmodid') or section_items.get('nexus_mod_id') or section_items.get('mod_id'),
'mod_filename': section_items.get('nexusfilename') or section_items.get('nexus_filename') or section_items.get('nexusmodfilename') or section_items.get('nexus_mod_filename') or section_items.get('mod_filename') or section_items.get('modname') or section_items.get('name'),
Expand Down Expand Up @@ -630,6 +639,12 @@ def get_feature_key(feature_dir, feature_meta_map):

meta = feature_meta_map.get(feature_key)
ini_path = get_feature_ini(feature_dir)
# Activation-only features (e.g. Terrain Helper) opt out of version
# auditing via `AuditVersion = false` in their .ini: all their code lives
# in the core mod, so bumping the toggle .ini on every edit is meaningless
# churn. Skip them entirely from bump suggestions and PR-check failures.
if ini_path and not get_feature_ini_metadata(ini_path).get('audit_version', True):
continue
# Use last release tag (version_ref) as the baseline for version proposals so that
# multiple PRs between releases don't accumulate spurious bumps.
prior_ver = get_prior_version(ini_path, version_ref) if ini_path else None
Expand Down
Loading