From 4625f5be575e5a350a8da131ed17cb4bd7aec7e2 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 20 May 2026 01:36:40 -0700 Subject: [PATCH 01/24] chore: rebrand as Open Shaders (#22) Co-authored-by: Claude Opus 4.7 --- .claude/CLAUDE.md | 17 +- .github/copilot-instructions.md | 4 +- .github/workflows/maint-cleanup-releases.yaml | 2 +- .github/workflows/maint-update-wiki.yaml | 8 + .github/workflows/nexus-upload.yaml | 194 +++++++---- .github/workflows/pr-checks.yaml | 4 +- .github/workflows/release-build.yaml | 2 +- .github/workflows/release-hotfix.yaml | 8 +- AI-INSTRUCTIONS.md | 8 +- CMakeLists.txt | 306 ++++++++++++++++-- Dockerfile | 6 +- README.md | 62 ++-- containerbuild.ps1 | 2 +- docs/new-feature-template/NewFeatureReadme.md | 2 +- .../Icons/Action Icons/discord.png | Bin 156016 -> 0 bytes .../Monochrome/cs-logo.png | Bin 23006 -> 0 bytes .../Icons/Community Shaders Logo/cs-logo.png | Bin 20676 -> 0 bytes .../CommunityShaders/Overrides/README.md | 12 +- .../Themes/DragonBlood/discord.png | Bin 105950 -> 0 bytes .../Themes/DwemerBronze/discord.png | Bin 407478 -> 0 bytes .../Themes/HighContrast/discord.png | Bin 73661 -> 0 bytes .../CommunityShaders/Themes/Light/cs-logo.png | Bin 25838 -> 0 bytes .../CommunityShaders/Themes/Light/discord.png | Bin 73661 -> 0 bytes .../Themes/NordicFrost/discord.png | Bin 121874 -> 0 bytes src/Feature.cpp | 2 +- src/FeatureIssues.cpp | 10 +- src/Features/GrassLighting.cpp | 2 +- src/Features/PerformanceOverlay.cpp | 2 +- src/Features/RenderDoc.cpp | 4 +- src/Features/VR.cpp | 2 +- src/Features/VR.h | 4 +- src/Features/VR/Input.cpp | 4 +- src/Features/VR/SettingsUI.cpp | 14 +- src/Menu.cpp | 9 +- src/Menu.h | 29 +- src/Menu/HomePageRenderer.cpp | 115 ++----- src/Menu/HomePageRenderer.h | 7 - src/Menu/IconLoader.cpp | 1 - src/Menu/MenuHeaderRenderer.cpp | 2 +- src/Menu/OverlayRenderer.cpp | 6 +- src/Menu/SettingsTabRenderer.cpp | 4 +- src/Menu/ThemeManager.h | 2 +- src/SettingsOverrideManager.h | 2 +- src/ShaderCache.cpp | 4 +- src/State.h | 4 +- src/XSEPlugin.cpp | 2 +- tools/feature_version_audit.py | 4 +- 47 files changed, 603 insertions(+), 269 deletions(-) delete mode 100644 package/Interface/CommunityShaders/Icons/Action Icons/discord.png delete mode 100644 package/Interface/CommunityShaders/Icons/Community Shaders Logo/Monochrome/cs-logo.png delete mode 100644 package/Interface/CommunityShaders/Icons/Community Shaders Logo/cs-logo.png delete mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood/discord.png delete mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze/discord.png delete mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/HighContrast/discord.png delete mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/Light/cs-logo.png delete mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/Light/discord.png delete mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost/discord.png diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 4212954429..08f5eb4993 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -2,6 +2,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Fork identity + +This repository is **Open Shaders** ([alandtse/open-shaders](https://github.com/alandtse/open-shaders)), a fork of [Community Shaders](https://github.com/community-shaders/skyrim-community-shaders) ([Nexus mod 180419](https://www.nexusmods.com/skyrimspecialedition/mods/180419)). The public/display name is "Open Shaders"; the runtime identity is intentionally kept as upstream Community Shaders so user installs are drop-in compatible: + +- **Keep as `CommunityShaders`** (do NOT rename): the CMake `PROJECT_NAME`, the DLL filename, the `SKSE/Plugins/CommunityShaders/` runtime directory, the `CommunityShaders.log` log file, the ImGui window ID after `###`, asset paths under `package/Interface/CommunityShaders/`, and any HLSL include paths. +- **Use "Open Shaders"**: in-game menu titles, README/AI-INSTRUCTIONS public-facing copy, the AIO Nexus mod filename, the GitHub release name, the in-game Welcome / FAQ / About text. +- **Link "Community Shaders" explicitly to upstream**: when the text or comment refers to the upstream project (its Nexus page is 180419, its repo is `community-shaders/skyrim-community-shaders`, its wiki lives on that repo). Never link `doodlum/skyrim-community-shaders` — that path is dead. + +The AIO bundle ships only features whose `Shaders/Features/*.ini` has `autoupload = true` (CORE features always included). The `AIO_INCLUDE_NON_AUTOUPLOAD=ON` CMake option overrides for local dev builds. The Nexus upload workflow ships only the AIO archive — there is no per-feature matrix distribution. See `.github/workflows/nexus-upload.yaml`. + +**Logo absence is intentional.** The upstream Community Shaders logo is non-GPL, not trademark-licensed, and may not be redistributed by forks — so `cs-logo.png` is not present in this repo. The icon loader's load path is null-safe at every consumer (`IconLoader.cpp` retries the colored fallback; `Menu.cpp` derives `showLogo` from `texture != nullptr`; `MenuHeaderRenderer` and `HomePageRenderer` gate all logo draws on the null check). Missing logo → one `logger::warn`, menu renders headers without the logo image, layout adjusts via the `showLogo` flag. Do not "fix" the missing file or restore the upstream asset. + ## Build Commands ### WSL/Linux Environment Note @@ -435,6 +447,9 @@ Feature versions are automatically extracted from `.ini` files and compiled into - **Complete Solutions**: Provide fully functional code with proper error handling and resource management - **Performance Conscious**: Always consider GPU workload and user experience impact - **Documentation**: Include Doxygen comments for public methods, especially graphics-related functions +- **Concise Comments**: Comments explain _why_, not _what_. Skip restating code in prose. A 1-line "why this hack" beats a 4-line block paraphrasing the next 4 lines. Block comments at the top of a function/section are fine when they capture non-obvious context (invariants, gotchas, history); avoid mid-function tutorial paragraphs. +- **Minimal Churn**: PRs touch only what the change requires. Don't reformat unrelated lines, rename adjacent variables, or "clean up" code outside the PR's scope. If you spot something worth fixing nearby, open a follow-up PR or surface it in the description rather than expanding the diff. Auto-format/lint touching unrelated lines is acceptable only when it's the linter's own commit; mixing with logic changes obscures review. +- **Comments describe present code, not absent code**: Don't add comments that describe code that used to be in the file but isn't now ("the Discord banner was removed", "this constant was renamed from X"). The reader sees only the present file; the deletion isn't visible. Past-tense framing of present behavior is fine ("if someone landed a commit during the release, abort"); the rule is specifically about describing code that no longer exists. Exception: a regression-risk warning that names the removed code so a future maintainer doesn't restore it ("do not re-add the Discord banner — the upstream invite isn't a fork channel") is load-bearing and stays. Commit messages, PR descriptions, and CHANGELOG entries are the right place for "what changed" — code comments are not. ## Development Best Practices (Learned from Codebase) @@ -488,7 +503,7 @@ Conventional commits drive semantic-release. `feat:` triggers a minor bump, `fix - PR a feature branch directly into `main`. - Run `Release: Semantic Version` on `hotfix/X.Y.x` for the current line — it will fail with `cannot be published as it is out of range` because the maintenance contract requires the hotfix line to be strictly older than `main`. Use `ff_target` into `main` instead. -Full details: [Developers wiki — Patch Release Process](https://github.com/community-shaders/skyrim-community-shaders/wiki/Developers#patch-release-process-any-line). +Full details: [Open Shaders developer wiki — Patch Release Process](https://github.com/alandtse/open-shaders/wiki/Developers#patch-release-process-any-line). The fork now maintains its own wiki (transferred from upstream) at `alandtse/open-shaders/wiki`; the `maint-update-wiki.yaml` workflow auto-publishes buffer documentation there on every push to `dev`. Upstream Community Shaders maintains its own copy at `community-shaders/skyrim-community-shaders/wiki` — link to whichever is appropriate for the audience. ### Code Organization and Refactoring Patterns diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c0c3b0e730..d8c15c1265 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -39,8 +39,8 @@ SKSE64 plugin providing modular DirectX 11 graphics enhancements for Skyrim SE/A ### Essential Repository Setup ```bash -git clone https://github.com/doodlum/skyrim-community-shaders.git --recursive -cd skyrim-community-shaders +git clone https://github.com/alandtse/open-shaders.git --recursive +cd open-shaders git submodule update --init --recursive # If not cloned with --recursive ``` diff --git a/.github/workflows/maint-cleanup-releases.yaml b/.github/workflows/maint-cleanup-releases.yaml index e934cf7085..76bc6a8b5e 100644 --- a/.github/workflows/maint-cleanup-releases.yaml +++ b/.github/workflows/maint-cleanup-releases.yaml @@ -150,7 +150,7 @@ jobs: tag_name=$(echo "$line" | cut -d' ' -f1) reason=$(echo "$line" | cut -d'(' -f2- | sed 's/)$//') echo " • $tag_name - $reason" - echo " 🔗 https://github.com/doodlum/skyrim-community-shaders/releases/tag/$tag_name" + echo " 🔗 ${{ github.server_url }}/${{ github.repository }}/releases/tag/$tag_name" fi done echo "" diff --git a/.github/workflows/maint-update-wiki.yaml b/.github/workflows/maint-update-wiki.yaml index 8a1f980d15..87ddc3315c 100644 --- a/.github/workflows/maint-update-wiki.yaml +++ b/.github/workflows/maint-update-wiki.yaml @@ -17,6 +17,14 @@ permissions: jobs: update-wiki: name: Update Buffer Documentation + # Publish buffer documentation to the wiki of any repo that maintains + # its own copy. The fork (alandtse/open-shaders) maintains a wiki + # transferred from upstream; upstream (community-shaders/skyrim- + # community-shaders) also maintains one. Forks that don't want a + # wiki of their own should drop their entry from this list. + if: > + github.repository == 'alandtse/open-shaders' || + github.repository == 'community-shaders/skyrim-community-shaders' runs-on: windows-2022 steps: - name: Checkout code diff --git a/.github/workflows/nexus-upload.yaml b/.github/workflows/nexus-upload.yaml index 9e97d21d98..13641ae496 100644 --- a/.github/workflows/nexus-upload.yaml +++ b/.github/workflows/nexus-upload.yaml @@ -1,8 +1,21 @@ name: "Nexus: Upload Release" +# `mode=aio` (default) uploads only the AIO to the fork's mod page. +# `mode=matrix` runs the upstream per-feature fan-out (`feature_version_audit.py` builds the matrix). +# Only `prepare-nexus-matrix` branches on mode; downstream jobs are +# matrix-shape-agnostic. + on: workflow_dispatch: inputs: + mode: + description: "Upload mode: 'aio' uploads only the AIO to a single mod page (fork default); 'matrix' fans out per-feature to their respective Nexus pages (upstream behavior)." + required: false + type: choice + default: "aio" + options: + - "aio" + - "matrix" tag: description: "Release tag to upload to Nexus" required: true @@ -11,17 +24,17 @@ on: required: false default: "skyrimspecialedition" nexus_mod_id: - description: "Nexus mod ID" + description: "Target Nexus mod ID. In `aio` mode this is the fork's single mod page. In `matrix` mode this seeds the CORE row of the upstream matrix (defaults to 86492 if empty)." required: false - default: "86492" + default: "" artifact_pattern: - description: "Artifact glob pattern to select the package to upload" + description: "Artifact glob pattern. Defaults: `CommunityShaders_AIO-*.7z` in aio mode, `CommunityShaders-*.7z` (core .7z) in matrix mode." required: false - default: "CommunityShaders-*.7z" + default: "" mod_filename: - description: "Filename used for the Nexus mod upload" + description: "Nexus mod filename. Defaults: `Open Shaders` in aio mode, `Community Shaders` in matrix mode." required: false - default: "Community Shaders" + default: "" dry_run: description: "If true, do not upload to Nexus; only report the planned upload" required: false @@ -35,6 +48,10 @@ on: required: false workflow_call: inputs: + mode: + required: false + type: string + default: "aio" tag: description: "Release tag to upload to Nexus" required: true @@ -46,15 +63,15 @@ on: nexus_mod_id: required: false type: string - default: "86492" + default: "" artifact_pattern: required: false type: string - default: "CommunityShaders-*.7z" + default: "" mod_filename: required: false type: string - default: "Community Shaders" + default: "" dry_run: required: false type: string @@ -79,6 +96,7 @@ jobs: version: ${{ steps.resolve.outputs.version }} dry_run: ${{ steps.dryrun.outputs.dry_run }} has_uploads: ${{ steps.generate.outputs.has_uploads }} + mode: ${{ steps.resolve.outputs.mode }} steps: - name: Checkout repository uses: actions/checkout@v6 @@ -90,7 +108,7 @@ jobs: with: python-version: "3.11" - - name: Resolve release tag + - name: Resolve release tag and mode id: resolve run: | TAG="$INPUT_TAG" @@ -109,10 +127,17 @@ jobs: exit 1 fi VERSION="${TAG#v}" - echo "tag=$TAG" >> $GITHUB_OUTPUT + MODE="${INPUT_MODE:-aio}" + if [[ "$MODE" != "aio" && "$MODE" != "matrix" ]]; then + echo "ERROR: mode must be 'aio' or 'matrix' (got '$MODE')" >&2 + exit 1 + fi + echo "tag=$TAG" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "mode=$MODE" >> $GITHUB_OUTPUT env: INPUT_TAG: ${{ inputs.tag || '' }} + INPUT_MODE: ${{ inputs.mode || '' }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Compute dry-run mode @@ -129,44 +154,86 @@ jobs: - name: Generate Nexus upload matrix id: generate run: | - ARGS=( --export-nexus-matrix --matrix-output nexus-matrix-raw.json ) - PREVIOUS_TAG=$(git tag --merged "$RELEASE_TAG" --list 'v*.*.*' --sort=-v:refname 2>/dev/null \ - | grep -v -- '-' \ - | awk -v current="$RELEASE_TAG" '$0 != current { print; exit }') - if [ -n "$PREVIOUS_TAG" ]; then - ARGS+=( --base "$PREVIOUS_TAG" ) + set -euo pipefail + MODE='${{ steps.resolve.outputs.mode }}' + + # Workflow inputs default to empty so matrix mode keeps + # its upstream defaults below; aio mode uses fork defaults. + if [ "$MODE" = "aio" ]; then + EFFECTIVE_MOD_ID="${INPUT_NEXUS_MOD_ID}" + EFFECTIVE_ARTIFACT="${INPUT_ARTIFACT_PATTERN:-CommunityShaders_AIO-*.7z}" + EFFECTIVE_FILENAME="${INPUT_MOD_FILENAME:-Open Shaders}" else - ARGS+=( --all-features ) + EFFECTIVE_MOD_ID="${INPUT_NEXUS_MOD_ID:-86492}" + EFFECTIVE_ARTIFACT="${INPUT_ARTIFACT_PATTERN:-CommunityShaders-*.7z}" + EFFECTIVE_FILENAME="${INPUT_MOD_FILENAME:-Community Shaders}" fi - if [ -n "$INPUT_NEXUS_MOD_ID" ]; then - ARGS+=( --core-mod-id "$INPUT_NEXUS_MOD_ID" ) - fi - if [ -n "$INPUT_MOD_FILENAME" ]; then - ARGS+=( --core-filename "$INPUT_MOD_FILENAME" ) - fi - if [ -n "$INPUT_ARTIFACT_PATTERN" ]; then - ARGS+=( --core-artifact-pattern "$INPUT_ARTIFACT_PATTERN" ) - fi - # Strip the leading 'v' so we get e.g. "1.5.2" — the value - # is rendered into Nexus file descriptions, where the - # bare semver is what users see in the UI. - RELEASE_VER="${RELEASE_TAG#v}" - ARGS+=( --release-version "$RELEASE_VER" ) - python tools/feature_version_audit.py "${ARGS[@]}" - - # Fetch the specific release by tag — avoids pagination issues with - # the releases list endpoint (default page size is 30). + export EFFECTIVE_MOD_ID EFFECTIVE_ARTIFACT EFFECTIVE_FILENAME + + # Fetch the release body once — both modes use it as the + # changelog for the CORE/AIO row. gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" \ 2>/dev/null > release.json || echo '{}' > release.json - python -c " + + if [ "$MODE" = "aio" ]; then + if [ -z "$EFFECTIVE_MOD_ID" ] && [ "$DRY_RUN" != "true" ]; then + echo "::error::aio mode requires nexus_mod_id for a real upload. Set it via the workflow input or update the default once the Open Shaders Nexus page exists." + exit 1 + fi + if [ -z "$EFFECTIVE_MOD_ID" ]; then + echo "::warning::nexus_mod_id is empty — dry-run continues, but a real upload would fail." + fi + RELEASE_VER="${RELEASE_TAG#v}" + export RELEASE_VER + python3 << 'PYEOF' + import json, os, re + with open('release.json') as f: + release = json.load(f) + if not isinstance(release, dict): + release = {} + body = release.get('body') or '' + body = re.sub(r'\n---\n+# Feature Version Audit\b.*', '', body, flags=re.DOTALL).strip() + release_ver = os.environ['RELEASE_VER'] + row = { + 'name': 'core', + 'is_core': True, + 'nexus_mod_id': os.environ['EFFECTIVE_MOD_ID'], + 'artifact_pattern': os.environ['EFFECTIVE_ARTIFACT'], + 'mod_filename': os.environ['EFFECTIVE_FILENAME'], + 'auto_upload': True, + 'changelog': body, + 'file_description': f'Open Shaders {release_ver} AIO. See changelog for included features.', + } + with open('nexus-matrix-raw.json', 'w') as f: + json.dump([row], f) + PYEOF + else + # Matrix mode: upstream behavior — the audit tool emits + # the full per-feature matrix. + ARGS=( --export-nexus-matrix --matrix-output nexus-matrix-raw.json ) + PREVIOUS_TAG=$(git tag --merged "$RELEASE_TAG" --list 'v*.*.*' --sort=-v:refname 2>/dev/null \ + | grep -v -- '-' \ + | awk -v current="$RELEASE_TAG" '$0 != current { print; exit }') + if [ -n "$PREVIOUS_TAG" ]; then + ARGS+=( --base "$PREVIOUS_TAG" ) + else + ARGS+=( --all-features ) + fi + ARGS+=( --core-mod-id "$EFFECTIVE_MOD_ID" ) + ARGS+=( --core-filename "$EFFECTIVE_FILENAME" ) + ARGS+=( --core-artifact-pattern "$EFFECTIVE_ARTIFACT" ) + RELEASE_VER="${RELEASE_TAG#v}" + ARGS+=( --release-version "$RELEASE_VER" ) + python tools/feature_version_audit.py "${ARGS[@]}" + + # Splice the GitHub release body into the CORE row's changelog. + python3 << 'PYEOF' import json, os, re with open('release.json') as f: release = json.load(f) if not isinstance(release, dict): release = {} body = release.get('body') or '' - # Strip feature audit appendix — Nexus changelog should only - # contain human-readable release notes, not the audit table. body = re.sub(r'\n---\n+# Feature Version Audit\b.*', '', body, flags=re.DOTALL).strip() with open('nexus-matrix-raw.json') as f: data = json.load(f) @@ -176,14 +243,16 @@ jobs: break with open('nexus-matrix-raw.json', 'w') as f: json.dump(data, f) - " + PYEOF + fi - python -c " + # Common post-processing: build the upload-eligible matrix + # (filter on auto_upload + artifact presence in the release). + # Single-row AIO matrix trivially passes this filter. + python3 << 'PYEOF' import json, os, fnmatch with open('nexus-matrix-raw.json') as f: data = json.load(f) - # Load asset names from the release to skip uploads where the - # artifact was not included — applies to all rows including core. try: with open('release.json') as f: release = json.load(f) @@ -195,16 +264,8 @@ jobs: def artifact_in_release(row): pat = row.get('artifact_pattern', '') return bool(pat) and any(fnmatch.fnmatch(n, pat) for n in asset_names) - # Full matrix for artifact prep; upload matrix excludes auto_upload=false - # and features whose standalone artifact is absent from the release. upload_data = [row for row in data if row.get('auto_upload', True) and artifact_in_release(row)] - # Drift detector: a feature opted in to auto_upload but its - # artifact pattern doesn't match anything on the release. - # This is almost always a metadata bug (folder name vs - # nexusfilename mismatch — see HDR Display in v1.5.2). - # Surface it loudly via GHA warning + step summary so it - # cannot silently drop a feature from a release again. missing = [row for row in data if row.get('auto_upload') is True and not row.get('is_core') @@ -219,7 +280,6 @@ jobs: pat = row.get('artifact_pattern', '?') mod_id = row.get('nexus_mod_id', '?') lines.append(f'| `{name}` | `{pat}` | {mod_id} |') - # Annotation appears inline in the run UI. print(f'::warning title=Nexus auto-upload missing artifact::' f'{name} (mod {mod_id}) marked autoupload=true but no ' f'release asset matches pattern {pat!r}. Likely cause: ' @@ -241,7 +301,8 @@ jobs: json.dump({'include': upload_data or [{'name': '_empty', 'skip': True}]}, f) with open('nexus-upload-state.json', 'w') as f: json.dump({'has_uploads': has_uploads}, f) - " + PYEOF + DELIM=$(openssl rand -hex 8) { printf 'matrix<<%s\n' "$DELIM" @@ -251,13 +312,14 @@ jobs: cat nexus-upload-matrix.json printf '\n%s\n' "$DELIM" } >> "$GITHUB_OUTPUT" - HAS_UPLOADS=$(python -c 'import json; d=json.load(open("nexus-upload-state.json")); print("true" if d.get("has_uploads") else "false")') + HAS_UPLOADS=$(python3 -c 'import json; d=json.load(open("nexus-upload-state.json")); print("true" if d.get("has_uploads") else "false")') echo "has_uploads=${HAS_UPLOADS}" >> "$GITHUB_OUTPUT" env: RELEASE_TAG: ${{ steps.resolve.outputs.tag }} INPUT_NEXUS_MOD_ID: ${{ inputs.nexus_mod_id || '' }} INPUT_MOD_FILENAME: ${{ inputs.mod_filename || '' }} INPUT_ARTIFACT_PATTERN: ${{ inputs.artifact_pattern || '' }} + DRY_RUN: ${{ steps.dryrun.outputs.dry_run }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} prepare-artifacts: @@ -339,6 +401,7 @@ jobs: version = os.environ.get('VERSION', '') game_id = os.environ.get('NEXUS_GAME_ID', 'skyrimspecialedition') api_key = os.environ.get('UNEX_APIKEY', '') + ua = 'open-shaders-ci/1.0' if os.environ.get('MODE') == 'aio' else 'community-shaders-ci/1.0' summary = open(os.environ['GITHUB_STEP_SUMMARY'], 'a') def w(line=''): @@ -359,8 +422,8 @@ jobs: mod_id = str(row.get('nexus_mod_id', '')) planned = row.get('mod_version') or version label = row.get('mod_filename', name) - url = f'https://www.nexusmods.com/{game_id}/mods/{mod_id}' - link = f'[{label}]({url})' + url = f'https://www.nexusmods.com/{game_id}/mods/{mod_id}' if mod_id else '' + link = f'[{label}]({url})' if url else label if not api_key or not mod_id: w(f'| {link} | `{planned}` | ⚠️ skipped | — |') @@ -369,7 +432,7 @@ jobs: api_url = f'https://api.nexusmods.com/v1/games/{game_id}/mods/{mod_id}/files.json' req = urllib.request.Request(api_url, headers={ 'apikey': api_key, - 'User-Agent': 'community-shaders-ci/1.0', + 'User-Agent': ua, 'Accept': 'application/json', }) try: @@ -405,6 +468,7 @@ jobs: UPLOAD_MATRIX: ${{ needs.prepare-nexus-matrix.outputs.upload_matrix }} VERSION: ${{ needs.prepare-nexus-matrix.outputs.version }} NEXUS_GAME_ID: ${{ inputs.nexus_game_id || 'skyrimspecialedition' }} + MODE: ${{ needs.prepare-nexus-matrix.outputs.mode }} UNEX_APIKEY: ${{ secrets.UNEX_APIKEY }} upload-to-nexus: @@ -428,10 +492,7 @@ jobs: mod_version: ${{ matrix.mod_version || needs.prepare-nexus-matrix.outputs.version }} mod_filename: ${{ matrix.mod_filename }} changelog: ${{ inputs.changelog || matrix.changelog || '' }} - # file_description anchors each .7z to the CS release it shipped - # with. Empty string falls back to the upstream default - # ("See mod description for details.") so non-release dispatches - # without an audit-tool-built matrix still work. + # file_description anchors each .7z to the release it shipped with. file_description: ${{ matrix.file_description || '' }} check_existing: true secrets: @@ -450,6 +511,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "**Workflow:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY echo "**Tag:** ${{ needs.prepare-nexus-matrix.outputs.tag }}" >> $GITHUB_STEP_SUMMARY + echo "**Mode:** ${{ needs.prepare-nexus-matrix.outputs.mode }}" >> $GITHUB_STEP_SUMMARY echo "**Dry run:** ${{ needs.prepare-nexus-matrix.outputs.dry_run }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -473,6 +535,7 @@ jobs: version = os.environ.get('VERSION', '') game_id = os.environ.get('NEXUS_GAME_ID', 'skyrimspecialedition') api_key = os.environ.get('UNEX_APIKEY', '') + ua = 'open-shaders-ci/1.0' if os.environ.get('MODE') == 'aio' else 'community-shaders-ci/1.0' summary = open(os.environ['GITHUB_STEP_SUMMARY'], 'a') def w(line=''): @@ -484,7 +547,7 @@ jobs: url = f'https://api.nexusmods.com/v1/games/{game_id}/mods/{mod_id}/files.json' req = urllib.request.Request(url, headers={ 'apikey': api_key, - 'User-Agent': 'community-shaders-ci/1.0', + 'User-Agent': ua, 'Accept': 'application/json', }) try: @@ -508,7 +571,7 @@ jobs: mod_id = str(row.get('nexus_mod_id', '')) planned = row.get('mod_version') or version label = row.get('mod_filename', name) - url = f'https://www.nexusmods.com/{game_id}/mods/{mod_id}' + url = f'https://www.nexusmods.com/{game_id}/mods/{mod_id}' if mod_id else '' artifact = f'nexus-upload-{name}' versions = nexus_versions(mod_id) if versions is None: @@ -536,7 +599,9 @@ jobs: w('|--------|---------|---------|-------|----------|') for icon, label, planned, url, artifact, status in rows_status: art = f'`{artifact}`' if status in ('failed', 'unknown') else '—' - w(f'| {icon} | [{label}]({url}) | `{planned}` | [View]({url}) | {art} |') + link = f'[{label}]({url})' if url else label + view = f'[View]({url})' if url else '—' + w(f'| {icon} | {link} | `{planned}` | {view} | {art} |') if failed or unknowns: w() w('Re-run is safe: already-uploaded versions will be automatically skipped.') @@ -547,4 +612,5 @@ jobs: UPLOAD_MATRIX: ${{ needs.prepare-nexus-matrix.outputs.upload_matrix }} VERSION: ${{ needs.prepare-nexus-matrix.outputs.version }} NEXUS_GAME_ID: ${{ inputs.nexus_game_id || 'skyrimspecialedition' }} + MODE: ${{ needs.prepare-nexus-matrix.outputs.mode }} UNEX_APIKEY: ${{ secrets.UNEX_APIKEY }} diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index 27f3509377..683e73ef5c 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -187,7 +187,7 @@ jobs: if: steps.gen_notes.outputs.notes_generated == 'true' uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1 with: - name: "Community Shaders ${{ needs.build.outputs.version }} PR #${{ github.event.pull_request.number }}" + name: "Open Shaders ${{ needs.build.outputs.version }} PR #${{ github.event.pull_request.number }}" tag: v${{ needs.build.outputs.version }}-pr${{ github.event.pull_request.number }} prerelease: true artifacts: "${{ github.workspace }}/dist/CommunityShaders_AIO-*.7z" @@ -200,7 +200,7 @@ jobs: if: steps.gen_notes.outputs.notes_generated != 'true' uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1 with: - name: "Community Shaders ${{ needs.build.outputs.version }} PR #${{ github.event.pull_request.number }}" + name: "Open Shaders ${{ needs.build.outputs.version }} PR #${{ github.event.pull_request.number }}" tag: v${{ needs.build.outputs.version }}-pr${{ github.event.pull_request.number }} prerelease: true artifacts: "${{ github.workspace }}/dist/CommunityShaders_AIO-*.7z" diff --git a/.github/workflows/release-build.yaml b/.github/workflows/release-build.yaml index 98fd52d763..244c9f4187 100644 --- a/.github/workflows/release-build.yaml +++ b/.github/workflows/release-build.yaml @@ -175,7 +175,7 @@ jobs: id: create_release uses: ncipollo/release-action@v1 with: - name: "Community Shaders ${{ steps.combined_notes.outputs.release_tag }}" + name: "Open Shaders ${{ steps.combined_notes.outputs.release_tag }}" draft: true omitDraftDuringUpdate: true tag: ${{ steps.combined_notes.outputs.release_tag }} diff --git a/.github/workflows/release-hotfix.yaml b/.github/workflows/release-hotfix.yaml index 5879f76f20..2827a38754 100644 --- a/.github/workflows/release-hotfix.yaml +++ b/.github/workflows/release-hotfix.yaml @@ -317,7 +317,13 @@ jobs: echo "Resolve by checking out \`${STAGING}\` locally, cherry-picking the conflicted commits manually, and pushing." fi echo "" - echo "See: [Hotfix Release Process](https://github.com/${{ github.repository }}/wiki/Developers#hotfix-release-process)" + # Link to the running repo's own wiki. Both upstream + # (community-shaders/skyrim-community-shaders) and the + # Open Shaders fork (alandtse/open-shaders) maintain + # wiki copies; `maint-update-wiki.yaml` keeps them in + # sync from buffer scans, so `${{ github.repository }}` + # resolves correctly in either context. + echo "See: [Hotfix Release Process](${{ github.server_url }}/${{ github.repository }}/wiki/Developers#hotfix-release-process)" } > /tmp/pr-body.md # Best-effort: ensure the hotfix label exists so --label diff --git a/AI-INSTRUCTIONS.md b/AI-INSTRUCTIONS.md index 56b9ddfe66..daa956cfd7 100644 --- a/AI-INSTRUCTIONS.md +++ b/AI-INSTRUCTIONS.md @@ -1,6 +1,6 @@ # AI Development Instructions -This file provides guidance for AI assistants working with the Skyrim Community Shaders codebase. +This file provides guidance for AI assistants working with the Open Shaders codebase — a fork of [Community Shaders](https://github.com/community-shaders/skyrim-community-shaders) ([Nexus](https://www.nexusmods.com/skyrimspecialedition/mods/180419)). The runtime layout (DLL name, settings path, log file) intentionally matches upstream Community Shaders so users can switch without losing settings; only the public display name and in-game branding are "Open Shaders". ## Primary Documentation @@ -52,4 +52,10 @@ For full details about manual packaging targets (Package-Core, Package-AIO-Manua **Key Focus**: Performance impact awareness, runtime compatibility (SE/AE/VR), complete working solutions, DirectX/HLSL best practices. +**Style directives** (see `.claude/CLAUDE.md` "Code Quality Expectations" for full text): + +- **Concise comments**: explain _why_, not _what_. Don't paraphrase the next 4 lines in 4 lines of comment. +- **Minimal churn**: PRs touch only what the change requires. Out-of-scope cleanups go in a follow-up, not the current diff. +- **No comments about absent code**: don't describe code that used to be in the file but isn't now ("X was removed", "Y was renamed from Z"). The deletion isn't visible. Past-tense framing of present behavior is fine. Exception: a regression-risk warning that names the removed code to prevent re-adding it. + For detailed explanations, examples, and comprehensive guidance, refer to `.claude/CLAUDE.md`. diff --git a/CMakeLists.txt b/CMakeLists.txt index c05cbf4f1b..2350126edc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,11 +44,17 @@ option( "Build shader unit tests (runs automatically before packaging)" ON ) +option( + AIO_INCLUDE_NON_AUTOUPLOAD + "Include features whose .ini has autoupload != true in the AIO. Off by default — the AIO ships only release-ready features. Turn on for local dev builds that want every feature regardless of release status." + OFF +) message("\tAuto plugin deployment: ${AUTO_PLUGIN_DEPLOYMENT}") message("\tZip to dist: ${ZIP_TO_DIST}") message("\tAIO Zip to dist: ${AIO_ZIP_TO_DIST}") message("\tTracy profiler: ${TRACY_SUPPORT}") message("\tShader tests: ${BUILD_SHADER_TESTS}") +message("\tAIO include non-autoupload features: ${AIO_INCLUDE_NON_AUTOUPLOAD}") # ####################################################################################################################### # # Build version info from git @@ -352,6 +358,117 @@ string(TIMESTAMP UTC_NOW "%Y-%m-%dT%H-%MZ" UTC) # Set AIO directory path used by multiple targets below set(AIO_DIR "${CMAKE_CURRENT_BINARY_DIR}/aio") +# TRUE iff the feature's .ini sets `autoupload = true` (case-insensitive) +# or the feature is CORE (always included). Result in ${out_var}. +function(feature_is_autoupload feature_path out_var) + if(EXISTS "${feature_path}/CORE") + set(${out_var} TRUE PARENT_SCOPE) + return() + endif() + file(GLOB _ini_candidates "${feature_path}/Shaders/Features/*.ini") + foreach(_ini IN LISTS _ini_candidates) + file(STRINGS "${_ini}" _ini_lines REGEX "^[ \t]*autoupload[ \t]*=") + foreach(_line IN LISTS _ini_lines) + # Strip "key =" to extract the value, lowercase it. + string( + REGEX REPLACE "^[ \t]*autoupload[ \t]*=[ \t]*" + "" + _val + "${_line}" + ) + string(STRIP "${_val}" _val) + string(TOLOWER "${_val}" _val) + # Treat unset / falsey values as off; only explicit truthy values count. + if( + _val STREQUAL "true" + OR _val STREQUAL "1" + OR _val STREQUAL "yes" + OR _val STREQUAL "on" + ) + set(${out_var} TRUE PARENT_SCOPE) + return() + endif() + endforeach() + endforeach() + set(${out_var} FALSE PARENT_SCOPE) +endfunction() + +# Partition features into AIO-included / -excluded sets. +# AIO_INCLUDE_NON_AUTOUPLOAD=ON includes everything (local dev override). +set(AIO_INCLUDED_FEATURE_PATHS "") +set(AIO_EXCLUDED_FEATURE_PATHS "") +foreach(_fpath IN LISTS FEATURE_PATHS) + if(NOT IS_DIRECTORY "${_fpath}") + continue() + endif() + if(AIO_INCLUDE_NON_AUTOUPLOAD) + list(APPEND AIO_INCLUDED_FEATURE_PATHS "${_fpath}") + else() + feature_is_autoupload("${_fpath}" _ok) + if(_ok) + list(APPEND AIO_INCLUDED_FEATURE_PATHS "${_fpath}") + else() + list(APPEND AIO_EXCLUDED_FEATURE_PATHS "${_fpath}") + endif() + endif() +endforeach() +list(LENGTH AIO_INCLUDED_FEATURE_PATHS _aio_in_count) +list(LENGTH AIO_EXCLUDED_FEATURE_PATHS _aio_out_count) +message("\tAIO features included: ${_aio_in_count}") + +# Build the paths (relative to AIO root) that the archive step deletes +# from the stage. We derive them from each feature's on-disk structure +# because the source folder, inner shader dir, and .ini basename can +# all differ (e.g. IBL ships ImageBasedLighting.ini under +# Shaders/Features, with shader code under Shaders/IBL/). +set(AIO_EXCLUDED_FEATURE_NAMES "") +set(AIO_EXCLUDED_STAGE_RELPATHS "") +foreach(_fpath IN LISTS AIO_EXCLUDED_FEATURE_PATHS) + get_filename_component(_fname "${_fpath}" NAME) + list(APPEND AIO_EXCLUDED_FEATURE_NAMES "${_fname}") + + if(EXISTS "${_fpath}/Shaders") + file( + GLOB _shader_subdirs + LIST_DIRECTORIES TRUE + RELATIVE "${_fpath}/Shaders" + "${_fpath}/Shaders/*" + ) + foreach(_sd IN LISTS _shader_subdirs) + if(_sd STREQUAL "Features") + continue() + endif() + if(IS_DIRECTORY "${_fpath}/Shaders/${_sd}") + list(APPEND AIO_EXCLUDED_STAGE_RELPATHS "Shaders/${_sd}") + endif() + endforeach() + endif() + + if(EXISTS "${_fpath}/Shaders/Features") + file( + GLOB _ini_files + RELATIVE "${_fpath}/Shaders" + "${_fpath}/Shaders/Features/*.ini" + ) + foreach(_ini IN LISTS _ini_files) + list(APPEND AIO_EXCLUDED_STAGE_RELPATHS "Shaders/${_ini}") + endforeach() + endif() +endforeach() +list(REMOVE_DUPLICATES AIO_EXCLUDED_STAGE_RELPATHS) + +if(_aio_out_count GREATER 0) + message( + "\tAIO features excluded from release archive (autoupload != true, set AIO_INCLUDE_NON_AUTOUPLOAD=ON to override): ${_aio_out_count}" + ) + foreach(_fname IN LISTS AIO_EXCLUDED_FEATURE_NAMES) + message("\t\t- ${_fname}") + endforeach() + message( + "\tNote: excluded features still build and validate; they are stripped only at AIO archive time." + ) +endif() + # Robocopy wrapper for Windows incremental file copy (used by deployment targets). # We invoke through `cmd /c ""` rather than the bare wrapper path because # modern Windows refuses to execute scripts from the current directory without an explicit @@ -371,7 +488,9 @@ endif() # # CMake install() infrastructure for manual packaging # ####################################################################################################################### -# Append a '/' to the end of each feature path for installation all its contents but not itself +# Append a '/' to install contents but not the directory itself. Uses +# the full FEATURE_PATHS (not the autoupload filter) so cross-feature +# shader #includes still resolve at build time. set(FEATURE_PATHS_SLASH ${FEATURE_PATHS}) list(TRANSFORM FEATURE_PATHS_SLASH APPEND /) @@ -481,9 +600,9 @@ if(AUTO_PLUGIN_DEPLOYMENT OR AIO_ZIP_TO_DIST) list(FILTER _AIO_PACKAGE_SOURCE_FILES EXCLUDE REGEX "/Shaders/") append_copy_if_different(_prepare_aio_cmds _AIO_PACKAGE_SOURCE_FILES "${CMAKE_SOURCE_DIR}/package" "${AIO_DIR}") - # Copy feature folders (only files, preserve existing files in AIO). - # Shader files are excluded - copy_shaders.stamp owns the Shaders/ subdir - # so the two custom-build rules don't race on the same destinations. + # Copy feature folders (files only; copy_shaders.stamp owns Shaders/ + # to avoid racing). All features copied — autoupload filter applies + # only at archive time. foreach(_fpath IN LISTS FEATURE_PATHS) if(EXISTS "${_fpath}") file( @@ -569,6 +688,8 @@ if(AUTO_PLUGIN_DEPLOYMENT OR AIO_ZIP_TO_DIST) list(FILTER _package_shaders EXCLUDE REGEX "/Tests/") set(_shader_copy_cmds) + # All features' shaders are copied — cross-feature #includes need the + # full tree at compile time (e.g. DistantTree.hlsl pulls IBL/IBL.hlsli). set(_feature_shader_paths) foreach(_fpath IN LISTS FEATURE_PATHS) if(EXISTS "${_fpath}/Shaders") @@ -596,7 +717,6 @@ if(AUTO_PLUGIN_DEPLOYMENT OR AIO_ZIP_TO_DIST) append_copy_if_different(_shader_copy_cmds _package_shaders "${CMAKE_SOURCE_DIR}/package/Shaders" "${AIO_DIR}/Shaders") - # feature shader folders foreach(_fpath IN LISTS FEATURE_PATHS) if(EXISTS "${_fpath}/Shaders") file( @@ -915,15 +1035,92 @@ if(AIO_ZIP_TO_DIST) set(TARGET_AIO_ZIP "${PROJECT_NAME}_AIO-${UTC_NOW}.7z") set(AIO_ARCHIVE "${CMAKE_SOURCE_DIR}/dist/${TARGET_AIO_ZIP}") set(AIO_ZIP_STAMP "${CMAKE_CURRENT_BINARY_DIR}/aio_package.stamp") + set(AIO_STAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/aio_stage") message("Zipping ${AIO_DIR} to ${AIO_ARCHIVE}") + # Fast path: tar AIO_DIR directly. Slow path (excluded features): + # copy → stage, strip excluded relpaths, tar the stage. AIO_DIR stays + # complete so AUTO_PLUGIN_DEPLOYMENT delivers the full tree. + set(_aio_zip_cmds + COMMAND + ${CMAKE_COMMAND} + -E + make_directory + "${CMAKE_SOURCE_DIR}/dist" + ) + if(AIO_EXCLUDED_FEATURE_NAMES) + list( + APPEND _aio_zip_cmds + COMMAND + ${CMAKE_COMMAND} + -E + rm + -rf + "${AIO_STAGE_DIR}" + COMMAND + ${CMAKE_COMMAND} + -E + copy_directory + "${AIO_DIR}" + "${AIO_STAGE_DIR}" + ) + foreach(_relpath IN LISTS AIO_EXCLUDED_STAGE_RELPATHS) + list( + APPEND _aio_zip_cmds + COMMAND + ${CMAKE_COMMAND} + -E + rm + -rf + "${AIO_STAGE_DIR}/${_relpath}" + ) + endforeach() + list( + APPEND _aio_zip_cmds + COMMAND + ${CMAKE_COMMAND} + -E + chdir + "${AIO_STAGE_DIR}" + ${CMAKE_COMMAND} + -E + tar + cf + "${AIO_ARCHIVE}" + --format=7zip + -- + . + ) + else() + list( + APPEND _aio_zip_cmds + COMMAND + ${CMAKE_COMMAND} + -E + chdir + "${AIO_DIR}" + ${CMAKE_COMMAND} + -E + tar + cf + "${AIO_ARCHIVE}" + --format=7zip + -- + . + ) + endif() + list( + APPEND _aio_zip_cmds + COMMAND + ${CMAKE_COMMAND} + -E + touch + ${AIO_ZIP_STAMP} + ) + add_custom_command( - OUTPUT ${AIO_ZIP_STAMP} - COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_SOURCE_DIR}/dist" - COMMAND ${CMAKE_COMMAND} -E tar cf ${AIO_ARCHIVE} --format=7zip -- . - COMMAND ${CMAKE_COMMAND} -E touch ${AIO_ZIP_STAMP} - WORKING_DIRECTORY ${AIO_DIR} + OUTPUT ${AIO_ZIP_STAMP} ${_aio_zip_cmds} DEPENDS PREPARE_AIO ${CMAKE_CURRENT_BINARY_DIR}/copy_shaders.stamp COMMENT "Creating AIO archive ${AIO_ARCHIVE}" ) @@ -1021,16 +1218,89 @@ add_custom_command( ) add_custom_target("AIO" DEPENDS ${AIO_DIR}/SKSE/Plugins/${PROJECT_NAME}.dll) -# Manual AIO package target +# Manual AIO package target. Strips non-autoupload features at archive +# time (mirroring AIO_ZIP_PACKAGE) so the manual path produces the same +# release-ready bundle as the automated path. set(AIO_PACKAGE "${DIST_PATH}/${PROJECT_NAME}_AIO-${UTC_NOW}.7z") +set(AIO_MANUAL_STAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/aio_manual_stage") +set(_aio_manual_cmds + COMMAND + ${CMAKE_COMMAND} + -E + make_directory + ${AIO_DIR} + COMMAND + ${CMAKE_COMMAND} + --install + ${CMAKE_BINARY_DIR} + --prefix + ${AIO_DIR} +) +if(AIO_EXCLUDED_FEATURE_NAMES) + list( + APPEND _aio_manual_cmds + COMMAND + ${CMAKE_COMMAND} + -E + rm + -rf + "${AIO_MANUAL_STAGE_DIR}" + COMMAND + ${CMAKE_COMMAND} + -E + copy_directory + "${AIO_DIR}" + "${AIO_MANUAL_STAGE_DIR}" + ) + foreach(_relpath IN LISTS AIO_EXCLUDED_STAGE_RELPATHS) + list( + APPEND _aio_manual_cmds + COMMAND + ${CMAKE_COMMAND} + -E + rm + -rf + "${AIO_MANUAL_STAGE_DIR}/${_relpath}" + ) + endforeach() + list( + APPEND _aio_manual_cmds + COMMAND + ${CMAKE_COMMAND} + -E + chdir + "${AIO_MANUAL_STAGE_DIR}" + ${CMAKE_COMMAND} + -E + tar + cfv + ${AIO_PACKAGE} + --format=7zip + -- + . + ) +else() + list( + APPEND _aio_manual_cmds + COMMAND + ${CMAKE_COMMAND} + -E + chdir + ${AIO_DIR} + ${CMAKE_COMMAND} + -E + tar + cfv + ${AIO_PACKAGE} + --format=7zip + -- + . + ) +endif() + add_custom_command( OUTPUT ${AIO_PACKAGE} - DEPENDS ${CORE_SOURCES} - COMMAND ${CMAKE_COMMAND} -E make_directory ${AIO_DIR} - COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${AIO_DIR} - COMMAND - ${CMAKE_COMMAND} -E chdir ${AIO_DIR} ${CMAKE_COMMAND} -E tar cfv - ${AIO_PACKAGE} --format=7zip -- . + DEPENDS ${CORE_SOURCES} ${_aio_manual_cmds} COMMENT "Creating AIO zip package (manual)" ) add_custom_target("Package-AIO-Manual" DEPENDS ${AIO_PACKAGE}) @@ -1082,7 +1352,7 @@ if(BUILD_SHADER_TESTS) endif() message("*************************************************************") -message("Community Shaders configuration complete") +message("Open Shaders configuration complete (fork of Community Shaders)") message("To prepare a ZIP package of AIO, Core, or Features") message(" Build cmake targets:") message(" - Package-Core: Core package") diff --git a/Dockerfile b/Dockerfile index af2eac944d..927ca80a41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,8 +24,8 @@ RUN git clone https://github.com/microsoft/vcpkg.git C:/vcpkg && \ cd C:/vcpkg && \ bootstrap-vcpkg.bat -RUN setx /M VCPKG_ROOT "C:/vcpkg" && mkdir C:\skyrim-community-shaders +RUN setx /M VCPKG_ROOT "C:/vcpkg" && mkdir C:\open-shaders -WORKDIR C:/skyrim-community-shaders +WORKDIR C:/open-shaders -ENTRYPOINT ["powershell", "-File", "C:/skyrim-community-shaders/containerbuild.ps1"] +ENTRYPOINT ["powershell", "-File", "C:/open-shaders/containerbuild.ps1"] diff --git a/README.md b/README.md index 9a8128f28d..e90af5f4e3 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,37 @@ -[![Latest Release](https://img.shields.io/github/v/release/doodlum/skyrim-community-shaders)](https://github.com/doodlum/skyrim-community-shaders/releases) -[![License](https://img.shields.io/github/license/doodlum/skyrim-community-shaders)](./LICENSE) -[![Last Commit](https://img.shields.io/github/last-commit/doodlum/skyrim-community-shaders)](https://github.com/doodlum/skyrim-community-shaders/commits) -[![Build Status](https://img.shields.io/github/actions/workflow/status/doodlum/skyrim-community-shaders/release-build.yaml?branch=dev)](https://github.com/doodlum/skyrim-community-shaders/actions) -[![Discord](https://img.shields.io/discord/1080142797870485606?label=discord&logo=discord&color=5865F2)](https://discord.com/invite/nkrQybAsyy) -[![Open Issues](https://img.shields.io/github/issues/doodlum/skyrim-community-shaders)](https://github.com/doodlum/skyrim-community-shaders/issues) -[![Contributors](https://img.shields.io/github/contributors/doodlum/skyrim-community-shaders)](https://github.com/doodlum/skyrim-community-shaders/graphs/contributors) -[![Stars](https://img.shields.io/github/stars/doodlum/skyrim-community-shaders?style=social)](https://github.com/doodlum/skyrim-community-shaders/stargazers) +[![Latest Release](https://img.shields.io/github/v/release/alandtse/open-shaders)](https://github.com/alandtse/open-shaders/releases) +[![License](https://img.shields.io/github/license/alandtse/open-shaders)](./LICENSE) +[![Last Commit](https://img.shields.io/github/last-commit/alandtse/open-shaders)](https://github.com/alandtse/open-shaders/commits) +[![Build Status](https://img.shields.io/github/actions/workflow/status/alandtse/open-shaders/release-build.yaml?branch=dev)](https://github.com/alandtse/open-shaders/actions) +[![Open Issues](https://img.shields.io/github/issues/alandtse/open-shaders)](https://github.com/alandtse/open-shaders/issues) +[![Contributors](https://img.shields.io/github/contributors/alandtse/open-shaders)](https://github.com/alandtse/open-shaders/graphs/contributors) +[![Stars](https://img.shields.io/github/stars/alandtse/open-shaders?style=social)](https://github.com/alandtse/open-shaders/stargazers) -[![Pre-commit CI](https://results.pre-commit.ci/badge/github/doodlum/skyrim-community-shaders/dev.svg)](https://results.pre-commit.ci/latest/github/doodlum/skyrim-community-shaders/dev) -![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/doodlum/skyrim-community-shaders?utm_source=oss&utm_medium=github&utm_campaign=doodlum%2Fskyrim-community-shaders&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) +[![Pre-commit CI](https://results.pre-commit.ci/badge/github/alandtse/open-shaders/dev.svg)](https://results.pre-commit.ci/latest/github/alandtse/open-shaders/dev) +![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/alandtse/open-shaders?utm_source=oss&utm_medium=github&utm_campaign=alandtse%2Fopen-shaders&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/doodlum/skyrim-community-shaders) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/alandtse/open-shaders) -# Skyrim Community Shaders +# Open Shaders -SKSE core plugin for community-driven advanced graphics modifications. +SKSE core plugin for advanced graphics modifications for Skyrim and fork of Community Shaders. -[Nexus](https://www.nexusmods.com/skyrimspecialedition/mods/86492) -[User Wiki](https://modding.wiki/en/skyrim/developers/community-shaders) +[Open Shaders developer wiki](https://github.com/alandtse/open-shaders/wiki) · [Upstream Community Shaders on Nexus](https://www.nexusmods.com/skyrimspecialedition/mods/180419) · [Upstream source](https://github.com/community-shaders/skyrim-community-shaders) · [Upstream developer wiki](https://github.com/community-shaders/skyrim-community-shaders/wiki) + +## About this fork + +**Open Shaders is a fork of [Community Shaders](https://github.com/community-shaders/skyrim-community-shaders).** All of the architecture, the shader pipeline, the feature framework, and the vast majority of the code in this repository originated upstream and is the work of the upstream Community Shaders authors and contributors. This fork inherits the upstream [GPL-3.0-or-later license with the Modding and Linking exceptions](./COPYING) — copyrights, authorship, and the modding exceptions are preserved unchanged. See the upstream [contributors page](https://github.com/community-shaders/skyrim-community-shaders/graphs/contributors) for the team behind the project. + +**Naming convention used throughout this repo and the in-game UI:** + +| Term | Refers to | +| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| Community Shaders | The upstream project (`community-shaders/skyrim-community-shaders`, Nexus mod 180419) | +| Open Shaders | This fork (`alandtse/open-shaders`) | +| `CommunityShaders` (as a path / filename / identifier in source) | Runtime-compat identifier; intentionally kept identical to upstream so settings, themes, and SKSE plugin discovery work without migration | + +The upstream branding (logo, Nexus icon, typography) is non-GPL and not redistributed by this fork — see the "Icons" section under [License](#license) below. + +An Open Shaders Nexus mod page does not exist yet; for now, install from [GitHub releases](https://github.com/alandtse/open-shaders/releases) or build from source. ## Requirements @@ -60,13 +74,15 @@ Install them manually only if you want them in everywhere. To clone the repository with all submodules, run the following command in your terminal: ```bash -git clone https://github.com/doodlum/skyrim-community-shaders.git --recursive -cd skyrim-community-shaders +git clone https://github.com/alandtse/open-shaders.git --recursive +cd open-shaders ``` +> The DLL filename is `CommunityShaders.dll` and the SKSE plugin directory is `SKSE/Plugins/CommunityShaders/` — identical to upstream Community Shaders, so user settings, themes, and mod-manager profiles are drop-in compatible. Only the public name and in-game branding are "Open Shaders". + ### Visual Studio build -To build the project, just open `./skyrim-community-shaders` with Visual Studio's "Open Folder" feature. (Ensure you have `CMake Tools for Windows` selected when installing VS) +To build the project, just open `./open-shaders` with Visual Studio's "Open Folder" feature. (Ensure you have `CMake Tools for Windows` selected when installing VS) Follow the prompts to `Configure` and `Build` the project. It should generate the AIO package in the `./build/ALL/aio` folder by default. @@ -118,6 +134,8 @@ cmake --build ./build/ALL --config Release --target Package-Core cmake --build ./build/ALL --config Release --target Package-GrassLighting ``` +The AIO bundles only features marked `autoupload = true` in their feature `.ini` — features not yet ready for release are built but excluded from the AIO. To include everything in a local build, see the `AIO_INCLUDE_NON_AUTOUPLOAD` CMake option. + For more details about packaging targets, options, and the difference between automated and manual packaging, see the "Manual packaging targets (detailed)" section in `.claude/CLAUDE.md`. #### CMAKE Options (optional) @@ -149,13 +167,13 @@ For those who prefer to not install Visual Studio or other build dependencies on ```pwsh & 'C:\Program Files\Docker\Docker\DockerCli.exe' -SwitchWindowsEngine; ` -docker build -t skyrim-community-shaders . +docker build -t open-shaders . ``` 3. Then run the build: ```pwsh -docker run -it --rm -v .:C:/skyrim-community-shaders skyrim-community-shaders:latest +docker run -it --rm -v .:C:/open-shaders open-shaders:latest ``` 4. Retrieve the generated build files from the `build/aio` folder. @@ -166,7 +184,7 @@ docker run -it --rm -v .:C:/skyrim-community-shaders skyrim-community-shaders:la If you run into `Access violation` build errors during step 3, you can try adding [`--isolation=process`](https://learn.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/hyperv-container): ```pwsh -docker run -it --rm --isolation=process -v .:C:/skyrim-community-shaders skyrim-community-shaders:latest +docker run -it --rm --isolation=process -v .:C:/open-shaders open-shaders:latest ``` ## Debugging @@ -218,4 +236,4 @@ See LICENSE within each directory; if none, it's [Default](#default) ### Icons -- [Community Shaders Logo](package/Interface/CommunityShaders/Icons/Community%20Shaders%20Logo/) is not covered by the GPL-3.0 license. It is provided solely for personal use (e.g., building from source) and may only be used in unmodified form. There is no license for any other purpose or to distribute the logo. No trademark license is granted for the logo. Any use not expressly permitted is prohibited without the express written consent of the Community Shaders team. +Open Shaders does not ship the upstream Community Shaders logo. The upstream logo is non-GPL, not trademark-licensed, and may only be used in unmodified form with the Community Shaders team's permission — none of which extends to forks. Action icons and category icons are bundled as before; the upstream Discord banner has been removed since the fork has no affiliated Discord channel. The menu renders without a logo image when none is present (the load path is null-safe). diff --git a/containerbuild.ps1 b/containerbuild.ps1 index 80390fbb43..8294082ed0 100644 --- a/containerbuild.ps1 +++ b/containerbuild.ps1 @@ -9,7 +9,7 @@ Write-Host "Starting build..." if (-Not (Test-Path -Path "CMakeUserPresets.json")) { Copy-Item -Path "CMakeUserPresets.json.template" -Destination "CMakeUserPresets.json" - (Get-Content -Path "CMakeUserPresets.json") -replace 'F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/SkyrimVR/Data;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data', 'C:/skyrim-community-shaders/build' | Set-Content -Path "CMakeUserPresets.json" + (Get-Content -Path "CMakeUserPresets.json") -replace 'F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/SkyrimVR/Data;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data', 'C:/open-shaders/build' | Set-Content -Path "CMakeUserPresets.json" Write-Host "CMakeUserPresets.json created and modified." } else { Write-Host "CMakeUserPresets.json already exists. No action taken." diff --git a/docs/new-feature-template/NewFeatureReadme.md b/docs/new-feature-template/NewFeatureReadme.md index 85c06059fa..2abe7e6b4d 100644 --- a/docs/new-feature-template/NewFeatureReadme.md +++ b/docs/new-feature-template/NewFeatureReadme.md @@ -1,6 +1,6 @@ # New Feature Development Reference -Quick reference for creating new graphics features in Community Shaders. +Quick reference for creating new graphics features in Open Shaders (and upstream Community Shaders — the feature model is shared). ## File Structure diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/discord.png b/package/Interface/CommunityShaders/Icons/Action Icons/discord.png deleted file mode 100644 index 3654718739d64db56466d1890c11b86d13e86a4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156016 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 diff --git a/package/Interface/CommunityShaders/Icons/Community Shaders Logo/Monochrome/cs-logo.png b/package/Interface/CommunityShaders/Icons/Community Shaders Logo/Monochrome/cs-logo.png deleted file mode 100644 index 56b3d908e462abeec7bf01c9b66479e6456ec069..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23006 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 diff --git a/package/Interface/CommunityShaders/Icons/Community Shaders Logo/cs-logo.png b/package/Interface/CommunityShaders/Icons/Community Shaders Logo/cs-logo.png deleted file mode 100644 index a3b86eff27f658a6dd35667d88eded23986b65a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20676 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 Iqx@6<3mq>?0RR91 diff --git a/package/SKSE/Plugins/CommunityShaders/Overrides/README.md b/package/SKSE/Plugins/CommunityShaders/Overrides/README.md index a39e0cc4aa..114197a73d 100644 --- a/package/SKSE/Plugins/CommunityShaders/Overrides/README.md +++ b/package/SKSE/Plugins/CommunityShaders/Overrides/README.md @@ -1,6 +1,6 @@ -# Community Shaders - Settings Override System +# Open Shaders - Settings Override System -The Settings Override System allows mods to provide custom configuration overrides for Community Shaders features without modifying the main settings file. This enables better mod compatibility and allows multiple mods to adjust different settings independently. +The Settings Override System allows mods to provide custom configuration overrides for Open Shaders features without modifying the main settings file. This enables better mod compatibility and allows multiple mods to adjust different settings independently. ## Directory Structure @@ -121,7 +121,7 @@ To create feature-specific overrides, you need to use the correct feature short ## How It Works -1. **Discovery**: Override files are automatically discovered when Community Shaders loads +1. **Discovery**: Override files are automatically discovered when Open Shaders loads 2. **Priority**: Overrides are applied after the main settings are loaded but before features initialize 3. **Merging**: Override values are merged into the existing settings, overwriting only the specified values 4. **Global vs Feature**: Global overrides affect the main settings structure, while feature-specific overrides only affect individual features @@ -130,7 +130,7 @@ To create feature-specific overrides, you need to use the correct feature short ### In-Game UI -- Navigate to the "Overrides" tab in the Community Shaders menu +- Navigate to the "Overrides" tab in the Open Shaders menu - View all discovered override files - Enable/disable individual overrides - Refresh to discover new override files @@ -159,7 +159,7 @@ To create feature-specific overrides, you need to use the correct feature short - Verify JSON syntax is valid - Ensure feature short name is correct - Check that override system is enabled in the UI -- Look for errors in the Community Shaders log +- Look for errors in the Open Shaders log (CommunityShaders.log) ### JSON Validation @@ -171,7 +171,7 @@ Use a JSON validator to ensure your override files have valid syntax: ### Log Messages -Community Shaders logs override discovery and application: +Open Shaders logs override discovery and application: - Check `CommunityShaders.log` for override-related messages - Look for "Discovered X override files" and "Applied X override(s)" messages diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood/discord.png b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood/discord.png deleted file mode 100644 index 666cb18c9b6a23b33690a8fcef419ae169c038bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105950 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

Q}Pt{ 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�M|HC#FVK^_f7aKG2<^P5;tVc$4{d|r%et>pbEm-sS&-!2jAXtLAhd>lBT!(>crYj83~ zdeJmG2r9oq(KXP!b?=aLM6>mN{ms$8`Sl{B(E7zk96Ky9L_`cjFh<(i)(t^PLb(CJ z;o#<&Od}JlxT|1ZozG8nb>tJq-y&ef{Z5T%<_HXvDE7hJPoh2Xa&4Ro?cp}?xJcNXO!`YX)I}0ND65jh3PI-zm zZY_UmO4KN@zKJ`wa+~C|7*O>{pyt?LH|PSBjtCPi3_oBB#*yIb_PyHj4!-n6G&8E*=K?XUbc0?y!Rz9vnRmI);`-fHplJWuD_e3RC`#Kl_9ZHJ9ny1}*^F0*iH@2hh=~2YhlJQnCIW zR_K2KZxJpG2x7yT5ZFr*Oc#NiD)YM|(|Z05xt;qE_bBl~=CWnjEig){IX~o%&zT4p z8nRXj8D+uB;q>O{jq$(JA&2g(_Zyw;(0W=B1q5sX6br(c!Z;*hCp%2dg9P(J- znQyXI+ZZK|a;bqE(%{@-aMb5*i+0~FgrF|6zznYD*4Xnu@ERjw`q<3ve?xF=B^)x* zj$skW@H>Y@HQ|J7qt%5DiI zc;lLD+9G&ys_wgm)oF+l$zX(aG@vURLhl2Vuwg^$jCG)B!^d1=ZEK1Dv0jt;rk7d? zSf2r2M*1$}{=I9zL9hM&lj++x6Fquz;1ma7aS|2Bs-?#~{#bgzknCV%eH5xuor4P@FP*Z;o}u+rEDlUHpOb>DZ^9NW1N}8~yaX52CMq_Hx>? zlgckGUmQ2ZQDSu*u+{3A;JN;4`p3V$m>&M9BWoSyn2m3IesY9OPWFdCGR}`b zx&-jtbrX4CKXU##)v;Ytadw?GInsXmPV>%`I?SQ6$1X&BZi-!FN2Qtab-%mDTg#S8 zFv6a@W_~CR*2lpc)~nKAz5o0gz>$_`eu7kvL@CEsZ(Nt3jUIkmB__3l6Z^-2%msK>wD*5vlx zHfzxCZP(L{+qU^}3?-GIh(b@iiZRR%P(&=tuJD&wLYmxX7p8r8X)@{7?`EfJ*$JW? zxv|^EwK%iM^b&+}uifWQ@4MS18+Xcm_|c8q=>LA`0y^?HPN=l^(1#sHH+=E(sg5=t z4}RohJ@2{awxELDH#Qmh(1|AY*!Pa~rqJJg-~!h(n2OBBBWsq@zg)YGuAH3Xhu*o> zh4%mJfAwTA2oNr_jg0a#h_Sqone^*G|hk+y~$-W8jW6lrgd6PD+1@F8!y;@qVw(7I-yfn`FYy zlW;2hP;}E>H}aabmnWZ__^;Gk^uP%xoKShU%-hx}4c`BdI6lR6XyK&?KkRVdZ3pFU z;pHN~&A$8W|Gk*#`KP+v{iiB-4XzB|xFYbV@ZNk5AwDOunnsz=*&Yb;8F{H3$#R#S zBi~>D`MLD;XP#Jj`Z13^lFmN!jH*L&n?x5}aDfLY75#nZomyXS_|7*yPtg%YpOqbk zcA8{L(ZdJciDs#kM;J;!9`TDurnXA5-P-I01=_xiKK%X*=$NOTPy_l(kVXjO7U-gHI9lZfwd)QqXnfh?Yv%S?}eDFQoaPN3ILm}*UtY(1oJFGt5nyA z(T|38J64>wE+LOTo0~o=*S{F;KZ~2^6gBNGyD9L0tzO-wvlBHWu zhF_<~^(^d$0g9c;bdIaSF;JlYZGb8BvUIp? zS<`H#%^3)M!Cu_(9%JKJw*ZqNXvXgTY{n}btV8|$++K*t^SaDqUKrN@=d10J)?*pGObBJzeue|!0Tyi7htVd3 z?Ke!hBQN7ZxRsw2G`tCA@C;DeCc*-6nb0Z78GRC)ckPIFDIs03uR)tW*)~G3GDV0q z2bf|27Trv#vomCrMGSb`&J_V6Ei7`G!>r+3pX#3hkl<|y`2@o-0BcDt_efqgT##+i z*We8NG4+n7Bve$m5j?@bA7&imTn#6zboHpP5D+Yb)#5^gV)vHB95BA_!m=0ctdRdEMNq|r}v%q41A}>keaw)YR^*O1%;8|(uZlG9KZ^LDZWz;xd9?tAaI@? zmG;AeCT!soVQ)tO#Ryvsi|~V^FzSFwA3kTORVc$nF>cENAyOSUlop&@-IY#?oYfsa zvY~{Pq%WAT^bSXHd1MQ%cyH9*5eV}bJ`*|^=z~>x_s5;IeEux~tUKD_U@&jaEC6b6X^{>Kbk#kuTg zi#hdpl6Di1AHkrFbkD~Q`zawK>X?O0cRZw(^qE8tSmAC2XSDLdl!G@Y0!plnye&4= zMPa;KUrfg31TrL8o^UAR8jV)CPv)3+riJPyT{5Sq=||JS67pTr6VEIz^Fme#ncg!8 zAg9V(+midu8LrVIzcYnKZF47$LNM=)(z5?>yoN5Bak0zR#L(0Q}L~{0<#M}_=?e~uhTIJK5~2mEiI8lq_D-K01lA^{Hb?VJU=kG&I>IeoJQoC zR)Nm-r*v&6PUsgcJgebV?TM667BH|t#}se2i}4AOzYS^B(`2I2YVf~>v+Pj&|MnU{ ztUGTo>9-y^sAu7}aR+jiuqVb5WDS8VW(dxZ9Hdd?p9gHHUhu$;`kOK4CJdqlPvL`%G8bf#l#I>;-d=&#S|uzaeE;~v=j16)^<8-^Kw2i_JWmGX*XM`b)i zo8I?Mrywk@*H+m7z03`oZ1vc2e1drh@}h7QnbET834vns_C@EVxgSJ%5z&Lxl(q$$ zw`;dQ9b5^n7Ni^@3>2w#nyl4Qd+dF{f9cqiFMsh$dckiW@4*|zsk_&%{hABHg*Y?V z$>k81G5Ylz`f7X>h4l?!n-PLHmiRyIykWGJ^f9dExpK(K?YQZ?I2LxDKA-=`OXzPe zy|^9^Qk({5d;DXMZGi*D0e{NN&!!g~e^hfK%baHw9RKd7$cwrA=f%NM@W0N5MdPop ze)$WYrv#ULW^!t6UEz=`&J+vYK)G?*-(6AzIIND(efn~`^CqbQA(m|G^AbQ(95={5 zpZ}-J+>ursrKjIY>PVvmh&eP%`F%Gdy_&rd{J5-r^6#>Z{=a9-CzL%mY$3mA8bu^)*^C&U*9y4+#fj!F!>r z;?Ud;-aIcT*|IfuoG1>qAKcQ~TN>mt#(VEVq5l^ku-!r|c%&rTM!%Hb9fL!FrTQ)y z+wo#2LzaK+Z!W0;93>rh+;Q~gKRH7KIEa2h9na{XY|ggOk*+n>PQ2-c7_a^JZ}P3# znxTh%oltN*g8yTklzc~>lAO*rwcri!=~VaUK6&|!&hJ)va+#YC+|0Cr`#EK#!XNkC z(j4t3Hq6m&Y5TagthmP(p30j{oVQZi*nih1KTKuyd2R<=VX3hJZO9dKla!}R#o=th z8*Z0%snFyqdJQW@^~*vtohtY5fBL(NYXC<{Wz63CPnXjJH7LjK7uq-azw1toX1@NV zE9kmwzEOi)R;9~6`vtng3+xk~@8`{vd37jHa+p84XJ)4>NH z?75zC+);Jcj*{-PIZ$D{ZxmT(eK}xL3En{M%r%hh?z^<0nG)Es=T56eZS{S8jsTND zY`=J4)vv+<|MtZz{7%{R=#wA6oId-1E~c-2=}P+iCoikD71?)>U2!*^wW}^Qf8d?x z&~H5VR1XaKkI!6AyX#my=_yU-6uo17U3RLuCj>~L+%3UD1oj5*z*6MQrA6-UjFKrb z7g&luvShn<3FN@MYv~8ub>TpO(*jV6O>&o=L;simMIL@;!r>z(Sz7wDY4cXPY5O+X zynz-|3EnF_39C1=Jh$Fn#zQ%emni{F@bYypwHFEv7CCS5T5yKtz1MwPzA|P-pWSfV zHoEgpi(t_17QC@a5mA$oJFQ!rUZP~H2j4R=A4T{oaCFdD3$jlL?0;%O8x~x!#1kIa z-0^&&qjHixB5CL*uAMde6zMrNmm3cDS;WEEGzfn1y0P3 zfNKI39N!u%Dis9G<+=Xo?Ef|tI{!xua<|Sv*|2U+x&m9-93b}1P|HwMA(0UK5r=$rd) z66u^2d5b9XuF+&*lDhTeKCNe>jMwNb(4n?0YeE=6Cj4SzE1(LMYM(Es$?IAAfUnUTLnfK_Ex`_CtXkA z#wN*Uqi-w@6v7hWL?LHmhKeGwGrI`W@-dm!#;7gOi;j6<$95}`nQ#c$1E*Oq#-auY z6b`Xt zp>UNDX+nnc!V%Bw@QDa#xD5fRExYD}zAH3QT9C_MOF!?g-)B zCjf9>fI(<);Eoenra7qp2 zsL#g;h>#{RJ(xnVZT*Je;E*2z&;vcEXj!5eGHhXzD9$M-gA1NFm~l*GCVWu$;X-&a zjvzTLfSiD*M5r@V2aa9Dt{{H;aw=EcF-}HnAP*I0q~#^M$mpE~oo4fCr3Jb0l6aud zN6USeE-&eJ^=A8I`4Qzn8Yv^8+%`Scf;bm~H~fa&fqXL=$z56TqpqlvMyc)vr0}A6 zl{`)oI=>ebPM_M}v7=EOX5`VnyiMLh7Rk&Iat9m1Z{-7@q(H|6`j`SiDJC?D&Vc@E z-IG(V*xzEmMf6B5boK=twlJyE24pXx2ChJINsrKQ0P`_^QGb= ztHI1#ZT;4X`_L=A);R(bQR`&a)uY&b3*u<99(UJ>pbggF;(;FyBjh({t#tzBAfPK{ zh&O#~t|f6zG`FXcbk34G_(AIrWT;I99L)XLY6pz9{2S%N-2Trs$m~fgH@`cGBeo4e2~`CJ%fqRCeZdBxhxR=3$eF31rTEgzBBoAp;Wq9UH3IP?|D zVb9Mn7$8qgPXJ{@S*_7_Ibwz?Mw(MHJMG8a4j$%Ye}%ouz+N#QfUfF&c0Eaw$3ozn z0LLRy*JePBa&DK+yZx7fH$L?K3+T14IlUgAQc`gk?73^^f9e7`S~9(u@nU(fgpVTU zzSta2W@}QJK3u1az@I5NE8Fj_?vDRC!#NDx^`tEv7Kg{3CdXcJYTWX}tLU`fIk~>M zTO3iv0a1>C*uG_dT6g2sv~I=D0YSIC_&Bw@u(nbh>+zPVhxOE*KP(l8NO>c5`zW&= zENUI=EC8gn#9B{Z$4%QCTJ7!MlKjoxonwp`#QjY-Hph0sM@iqirR=al3$m;2z9FDE zeJ;+KzWbEvo#-o*^Y5gm9bE$&>|Fablgv?H#u0ZjShKh+%g+*wuo;35Q0~eKI7-^Q zH5`QI@VcW^9D`+{{q}Kk+z<2G%ker%K9kJGz@+cqu%m`KecM~rCij#0nI+dcYtVDa zQG#sBSlw%KNS45-v(7w&#)+rOkvm3X_QrOh@$%NXNJnF!yT0vVkPc9=frwu^$gm-C^2o z{px3TDaXdFqa+Y~yrt_Ut7PY9OMK`Z=hkCW?xBwS5`^%DueTtl{dUG(irDiQAMd2c zlO$bCvHt%vo7wIxjz_~eJDf{A?m8Xi@Gw3%?YDbqG^033|$I_)A zIG^70#xv46E9%k z+K(%Di(V{pw*+x+SZ5tJE!-d~_R@_vwB2oroUS__-MTHTB9|v&BbX%W?Ua(X;4V2D zWVm%BQFzUCjcEFJBLsfcpor^M2XO4H=Mgx!$TDN)K!%*qKDKowljQ|hB`D^{Ni`tx z+Y^p87Muw*GsHzHu(=3mS>B>_v= z%S8KFS0wT8kk6uH8#XF~{7>^BCVi2$$$5FTAi5%BR$4mrc+~|u( zlU3|_?D%7}&(sQw^&=ctduGt+xG~V}Qd+qKM}3VQULINS$IA#2YfEIDsk8rLsP#(b=xyxK3w96VHsMGBUXP9<*R3dK0h)54AuW0b(jSZR=&5ggGvtP9aK|40m*gU++9SZN_44qqzZb zd^|ce6ISDlrJ8$&!JxlSdoHX!}Ix`rGu1#Z+qJ4MTtrX}T%M)38=?QTK8sYgG8yY#3YcsfMu6KW6Nky6 zjw+WU;DPj8#<)&{Dnh9EeD@^^-VXi2AzvL%@t&B26CQg%vy)%(th#hcMSPQvp#T+fFLZ?_q!De6Qgw(H+S z09JnRdP4payn;p`wv7kodWe9waYAZAAHCf{08Y|pot$?WJ<9A}kfX|bqaCfc!6h!2 z0c{G4z)%a^r<6GT4gT-4fvkiMLhWwr!kYmC{9kvg&?2)VPgR|leB}+t!DKA!oJR8x z#yb*$_tZ(h+5_KalIHT4?8uY`Wz*-}cHEG>Q-^3DDs6K(VMp(4Yu?BvOE}Z*dS4`W znmLajj!~+G2y4#Qx$mho>U5S_^O0R;kP^`<``;F0Om=t62{|F13gr$NzmavHtwP7O z(K2(cr)@vV^~_#lOdbNY0&=7E1!-B^u{w1pmyrJy1IcD%1+MeAIF51t-|MOQJr2HP zgnO>TvOcn(c`?m`F?!rLnljx&h#1#bR(=5alq_nF$`}<-H>V2SZM-g3?&|Qk4sNoz zwc<-Uj)#~B89y}gm9A^;x9=>49R{07^w<=aks>plmTiNKQ>)eRE4sZ`Z%Z^d$n}N* zleK_v_WKd=fpCgbn89iRO=cq!qwI>Z<$ihl@}*~=|DO)tc+=!$dGnv1>A%Yxkh`ay z2W(6&Kt~d+ks}Wk)_^7!NtX}!&Dsl_fZPUiQD)xFj{iyXxwv!Y$`aHUV~6=X8O`hR z*|dSD&*a>loDJK*^#!`DyxskTwi8EjMt|gk7tsD&+MIo!#BG$ch5;#=p-a!5E@J&} zsh-=%*qVN+jaEr45)X@Y%xm7bt{pF+#eF=MfcVjEQ=Xk^u_G4aBH*Btr4BEoo{mJ} z+PpE}?{Qe}4QtfvyNb9y$FAGTA3HW^6`U@&Dbvj&a@+`rXLqy{h$VAImLQ$=%Vd^X zU4_3>5mT!oi~)- zIaFs7!5Dy}4eTYmSJI=Obc_dPlvH+@C_6!vAcuW-cJ-MWFMTX&lgVjqiwpuWIC^;I zsW~TQ<)HL|oJ$pr@cn0hdPY4ygM+Po}?l|9inspEnLFjIQ|9 zW%RjEUPkvnSHRvfI;!#lE&1pPgWN zxb7-c0!se+{TBq>#>64zA>r@i zCHsQ4zO;^u-yz{#rbq(>NOMj5VLh!BQsmgRKn`=xdlZvRc7W*ng>zeUs>=vsj#DEr zBpO93QMY|ddum6d488!*v`%-D7-1qSnrGC>J-$s~mu;ZPCwoYO!(eZeiHnl>HpCkv zoAVqQLZ5)-HO6jzAddmC@{KmdabOftCs*9DMmJtAfThpSY7V9VbtoVp8C7K8kT{bv zB9Fp24o6D}!N8B*SK4SRIUpPXkE$3d9*q7tc2L1LK8>iQ$Bo{PE#jjey`rnMr1XQ^ z+~Ih(+dX%-0kmZ?-vPuGzZibQDwYgLCW9S!YXJgcJ0n937T6?jz`~ zC>K4i!4ST9Y_uxAYeoGx2F}M(607b2^3|fXz2cPhO!``LUU+TeWBtWvB;>RxP_9B~ zc(gk8o_*@J_x0K6J98L5NJ3soS2uxu}`Xf2BC`PQ;MI-Pg`*Z6r>O(KpFzou1nm#kSTaf8y z6ZvoMaFFnV7IX{>26kA%#!Erd%$YJxKgx{P!g*`7npq6`FOqRLYb1BngA7S>dmwmN z@s;Na+6#^`aIH6I$>4v49qfMs{&(5}yLAzK4*pTC*Yvs#gH}5b>VdHTZRSyAXPM(z zP{!z;3ujM}$?Mb>FKgxlh28Bt;t7#05-U|Y3aFE*@oD@$5{Le)s3{ZIR%wHW;kYZk zSFYTKfTL8@{wKq?0Dj%h4*CllQ7aUVP`}XYP}{Fkk7K{ND93P9N{qgGe)SQY?4=qOXpj0d9YN+ zOj7Io*X{9OG{srh4q=_z9>-W{DIY(%1ZL>Y5h zS=g=KQLTgLzfu0U4cwlWpoby7NgyBxTUoQ4Gu;q~&6xMMETcabin&a-qcjjpZ;fM& zbMP&|0rfZWi0u(A1$}sa&#QAo3fW`%Z3Nqqj0P9GP3Zlx-vYcgeRkQk1#kS+f#DeW zNfHKcl;J2xc5L04ol4ICw7~xzj6@yB2q~=sKD0K=Ak+(uIqwB(C3qL|+ge5@m5`}P zIhpnY`)99x_owTwy;}J(3u3bc%bV}tz2++H9o=vn&Fu%u-SKtixiAB(#3Ij{dHG66is}@nD4r|!C^ZX?9e#IeM7i9V z>Cd;W4f^>>b^^)u>{K*+)YuI)KS(@6a4tUyg zPo+yfaK84P6%W%m=rJlK7-YxI+lVOS9_oKPR>6(~+TpT)-sk=Z(q&YM_jDC9F(9;0 z;V5FaC1>SzJZ*k6r%T%ve3@k3o}bwWkcYzbowv{MoEKqSLsY2#4;yC@6O0$88(Yr5 z|Iqn#)U!{eU8b=swD*=TUr7&nWOK5gfByM?bkE)DjzKYZD1qY#ZjQA_S>S&`BWk48 z9ZY9a(o|y7&VZxcy&twX$S^IRp9A_hz`W&0UfO)s=M0DW}j+l9<K5WFekM=G4L;+>y zUuBci+P7~0?>e6)p}_mKf@N`h>}XwY8gZBkL)#(?rcA80jkU+%9{Cw!@^9myh|zn0g2w)z4%Y`<)c=Zw@OsSYK1`4BYPKE=RB z*%-Wm85VC0l|hZp_$Ej=Pn_Fe2gx_Oj)7tSw_wzB`hwd+NOul%C8j1k8VDiW+8Bhe z^85s_A@Oz}OEI|*LF)I6K@>l>RSyd6+4vw4j!HWKmv8yZAh_WzmUV24((ybQzO8FM zS|q@j-Va`ZV|UBJeaD2}05K{y84!xIoud2U96>r0z}xC%2~$?0%vK5kN)jiTM$B{$NFNl9Sh`9cC2SDME+QCq}f85 zstzA-%is6gNryYyC*b9-lf4*5rO0?+q%(9SEmVS9i*XQ1104?c)nl~;S1a2#Gf#L< zIB#|08@L1uir5GHjGDOy3}}6VaFQwg_J(;|oVB{h)vwGXk@ipJh4zrB!+(%C1FED8 z08l*FzKs!-F+zZbNWi4ON0V)`3h(WAJ{o{27BwkP%(aVU;Q@6~>aW$S0VRe}<8KQb z>2WNzUn7|_*~YTnI|__DFRC@U!(j)Sqk7j0gRUl7BmP5g;F`aLr<$d zsJed0y58k5@{jSK(trm-3gUtiE{%T>2|{yPvIRg%G&+}w_M0S+qhYPRWsIXtGuTFe z$G5fbKu`Ej_~_`Bi(`Li_HDIs^~0gV%vsp%e2}u5pKJah?EhhKUGLwVWgK(10a9Lv zb#Qu%HUV~uv2drSkivq5*pF|FUz9~y$jcgeGWXi0)nj*!-z}^#cE`y)#+2;~Xq9^Y zciV!R{`VtFs5yShB4_#Rs2{3t0JIO&?Ng28)R!G3`XSmr-7DvKw~y4fq{{u< z?IZ1bc2D!$v(}b<+M?Zg9?l^od1Ds~vT; za?GpH%qXSSp4k`6g3)LQd6GdsN=N4%y*ZuD?f)Jemj*x}(jt&3(V2vN#&VX%PGt1K z`M#9wJj(D7b4seiaSVYjHm*^NSFQD%-mrOo@YG;)R96$`yrvma9DkD60vbOB%{02v zKB}(l0~=TBf9A~15_$VYNPt-ew%dXZjCpZyE{b13Sjt$FTRXZt;1YAH|gL zq_RW=Y$%mMX3taLXHyB@c-?<4c%y9necDS-_TY`O_dmojZz{WZT1Y;AO5|xcRi^cYaUA^!YRd$36SFD0 zCJ3uv3ob857SP#{Zg#CK44~I(3-qvi+FI$Wa1=;+BfYEv?y<9eHm&_x&&y*x=e^Yf z@zpO~;n&6S8RaANwZ3e|%RNccpB2xH2XET8tvWYLDvr$u9~#Fe*cKW6!;Q z0s8;g-(2F4%69!f^)m;7egHn>D7^Q6cdG#$Z@K7Fdh8RAb-=&+r92L!Q0OkOHGX>H z_dbB5;Y?QG$0!M95oVyd{$HwxWF;lpT&Go&dj`uw2 zdcxZ5usXSI0YtFWitAS7<3@X5_;d>rDzx`YM;%v#IZCpfU&`(qI}K4N>{9h=bnE}l z^Pt}s`H`~Bnc3M|hsigi;a{ETbWc6v=;_BDMQ5CGM!hcd`O8l^k!~5I-a?}Pro3!H z3`B4Yq}vcJFr?HQWyR-YbJ&GeJI!drmgcUK-F~cq=LTf^|%4G?@IO!!Xrx(BKYDS+go4Flug#IH3l$Oo&J61la0qpuu%ylD!X-+ovrY5*G=<# zyX{14CiIc4fHc8GKTfW3oMi+N$4A^ZzvD3-fm}e_r6R-cz{T+XwU@020o1V%*&TBAgk`V6ROv zp`9c>4H!vr%za!}yMBxQo6RzC&`hhClU-J;6`&_J>XXI!-?A8)=Uru=%mO$Jww|Ev zMGz0v7|cl0i3?+ySl}AE`xsOa&V0?Q0(zhb3}MYVO+d^gA0O8jQU&t*2jT!{5pbC(BE9i!T^P`#>Q9B9R4%_|w+GG?5U*EyZ*1 zVP4>DZOE*nLB=Ma5YKVnj26USzxWu7eK5Ndn&(R?D`qRJLuwU3_DpAeUs^rryu|?w zPHFiJC;Dszmm3s|MMhN{&TcR%0$e9sgkllWE|{U~O7pqD4Y)atiZ38Hei;&emhfw^ zi4lx1ypDiUnPJiU1i0=efI^{2W5JkWky&}dgF6`O&h;28?t~&>K!9_CIMHuJEYE3p zG04gK>4pSROpZB7e9VjU*56ntA8E%9#f5!Zrzji?1bE53LX;QOjjvli@i8qI;aJe$ zR=+v>6#q_}j!S}o8iKkd@mQjRbUz71%9>l|_gqLApDpQ(pw+%?<5}@MOJizv1s{Th z7FsFGT>nGeEx*8mHV6_DSRmC3$PCItgKO)5`!#6C?Ct>a#(sy65HL^)WUkp|v4~cy z9U@?WIb+N7O5SzI+G%xUh=bQS@mviAcB6$mv~qk4t<+6R&M%a+!DnO8p)F>+oFSKs z(FUjSG$!fW_pS9E2Sh0t41P1{&(d$S3mqpM_(J>-IDTAqe9+JhjyeVkrzIYP!xZue z?Ol_<7M$%ICX#3ixwF}Ywo;h;>+R^cAw3d7Krcf;iQ|ZpU1W~f`ZXABW&Uq<$pZO+ zeeW5;{>QPvVnHF4>6h9rGYh$tW!qw-wBUpqOi`l#?LN1liQB0Ka@;w{t?TYdh3|Z-}sboP$=e zhZU1xMA`y?!`F8^FEnAPOT~p+ZZt%SBD#u0Ba`O#^p=^>kXldH~GWx7J*B1 z2=efuBCH>Knq?j)3?jp=Km%hcKRTEK?4ZS*1nj(kgP`7it_4MBH&U=2FX*7oo^hLa zj1Z|9@6b=9?;>aRRj0owp4rZDDVU<`j|Vs zoo<7UEl_Rr52-CqzRTPwsYbidnf+|C~N zWb=>Uf2I61jEGlanAfCWT!PGvPf!AcF7NUuXeg8A0{`dR(Wt1>3BEF@gx|6`wU94(~< zb`R%`{x&;cpZyP@$2~EQp)eTM4_Pu77)l)`4^jW`Jvs9p^Z0okC79r&e;JOk6HYjR zZu1vLIE3r;dHk=R==m`&7b5jTSI3QcamUU4$N;uWu` z*TvyzPRA`9bp@HPUnaylXH)M@(F(1>3pp>-7`j3-sMI8pG&oQ#=>(&ulolU z+Gs}*!i%U2b(oLfrW@io36ov+-0~c;AiI37wFW(SUzW2d@2+}UbCB5q^RkrmjF~aE zE8btfy9Egq+A9G+C850AYv&$damIH0fXL^z4h~VaN7sa%yg=B>VYQ8F?LKOf>5o#m ze%`rnp)Y>@>bg_JHCMNwq36Hg1+?R?`zvoWx%M|e!(~UBTgR~YTHWpQ_@|sme{#{K z^ovhA)^)h7fIR~1Ic+XdcBvt~Z~033hrzKWsjo`qzwz8tX^(wlhmaD;agWVJ_uDef z)4me{QDuz2a~s_eLOtEI?RxtBry@A)iAT?m$Jw>+=+TJcv!A?tqVtfuMef~oW0SQd zcw={y!PDo|7oJK7-}8Waz5C9Tj{9hS0_Txpy+h?(Nkz^*>B%Qj<8mDlvw64$ zZ@ltP&hs5K_ML3F>u+s%`|Q*wlh37J1@Bcpqy0bCbktLxa%@w!ltk{;w4r z7Hveboj|aVU0&SLV1dB`HtxEK{raFukNwuOPN=`j=g!7^E5_UAxU!NU%pP7#et`bh zeo5-5_?*yx8~>BQ8m01oW@j*F!vE(xq|~2a<@)%AEO9B|YmV)=KgD%Tc-h)t{8CsI z0+7d14)tiPV|9IjXATlb?%Dhw(XBEK*L&NVrP%;kpPT)kTVHMeqb@}FX6j%j(6aq+ zr$ppdTe2fd2->3bA|U`G^I3*!sJ1hPAljp1Kl02T>e>$K0<#L3m-^4J-Y3qanV*A> zhJn1X+KdHbaHgf84_qyTdSL*ZMj49+o{ctIdo;5{(^xV=KXX1y!&2c!Q|!B8_$~=# z3oYtEw15jnLn(ISa70nm;S0y*T3DU+wphfz*c!nVl;9{h7+^OKGvHd?0Z+S+Z~f@N z16)C8k_IC)sSZq14q6TzKK)`)%yZ&iyFceDT=ez6bUcCi2q(@Zzr;XBAEPgIoH$3B z^^9thaO|TCCF$@XQPBHpXQkqEtz(Jwe4I&xFcgMipfr||xAk>Mz0`aXhIF7y68&k2 zh2CqTDHg%z6E0pa`QMMLE1ZN2 zL3KwNw2&?ctM=QDOwpC@SiGmMJOpnf zsN!$*E%T4W5VR|ahqleX|@Yi#kdiR^V~OFVhU))cwN19c+Gal*ED;%uWG50NFZVK%n&>&OD+{20NI(5c15s}*+^)8=#?wK#i@0G-gRzlJa96{4Ncc{M|6 z*Rwd%urC|+TOYF%Mkp=Tgs|uu6%d}<3S;|}$&6zjn(d|~D_fE>FnK1PTatpnxz5Zk z)q7quJWGMeL~~mf>N9&O%2TKBvYhc_zogrV4s1-f|0(d{fg6v3Ex=5RbZmUCrsPaUzwtGvyThaea-8?R zkI-YDa18BZ+mlZP%ke5t{;gB#jpu*FOBUyvrSjJ4b=O|y4x;iFZrLUtx${ZC@hp1k zvtK|)5nmaz;&#u@AoIT<^zer@x%;viAXm_W!T{=Bf1PUycBfi!Z*om0wR_59VZi>DTzJ zbo13Ow>Qcy_{36qL%TVvRCa>h%G^5yVyq1xi*wI%f1JbHXRU1v%P(5+lKx+AN&zc7 z-%QTFH%3syg%@5(TlU-^>MFa!>_)%ys5P!bP?TkpZy7SRiU%jz2m}#HnfA%cJQ&}0ZXX=hsJIH0uD)Y0vqoX$LOvbQHG1_ z_RbU~b3nZ2cTc8oee)`B_rw49lk}2TpG6OP*x|Hgw~00wua(I2K{j7;sEC{90 z=^vkQCVh1R<@>ibI)C50fX-i5=zM?gWud8Oz3An1#1rRqzU7XNDx%Q;D>H!gG#^_| zyKKy!a&v~q3x~W=zg;fZV`E*O{X6WsQ=Hpnoa(dbQ~2)H=Uqg5?z zKiZ&ur;5xffg;C0?^HVOIo)TH{bh%Z_IZKt22xUi|JP26qf^c}qwR`y%Z=OU^)GG5 zQWRc& zo;rOBxwi`uBFFvj8u5l(kaFRt^RCg7d;%U!$A)WiaOMUl@ z()Zu{{nO}MHxqsDM-4ge062?&xbp-j23?e$gid<#%e*|w(>oCTv1YTBFPUUa-@2yZ zv5b2ido5D}SzBFFgX*l`g+`t|(SE#Z)h-@b^vR^hPMUE0+Y`;mDmyO08S8&*r6NDR z_04bi?<^aL?ls{V=Lrl~Vv~Fd-L2+!LdAg5{z9e+$4B3nE&lqXPpV z8dD7d?HDmhC>$eBM|^>QY(XOUW`{qfYzTqeBE~)~olmE2Mud<#nX;9{io1w-wcf>F zBXW15P!N63;L&#mArnhvON(v-Lan%otv%WiCB?&!;K|**K_N<;EO=eqN zh~ohWJwyx77DM70;*lwB+uV(=v%VM7`yFn3Wrhvq^TLp7fU*0sgxO``p>%9HOUp`7x!`?1(dSvL#rPGa9GH;eon$M)SRf1^~FBmy!D1c~T(Hh$P;ne)Hk z!m}=>mqp*9>6Ag=Jdqu(b?#^x7qCZvOR@DS7RLL9eToHZA<9N@$RPn8Vi>4*=6jwu z8E3dO8Sl8Yeg+2pP0A5>(`Y&~?`F{jw0IS;5Uw2(93ct(8ovqkNrFAeWNF}6JXdZX z`yohQ2B%josh-OJG$x&c{%n)UB3_~hI4eORB;8G@_B2RCItMCK{MN~l;WO*+VwVh#@1~og z97*i@WXS?{O*VMa*T)=u=ty7^$}UjZnH5N%@l?j2BV6Y zFO4D|q-5vYR{h1~0TP_#f~eggL1PNn;+lX9aPfZETfsny3o4dPc5=wd*y5j5N4659 z=WhUr9-C6-x62!-Ju?AGrmnbBeP(Q-*=HT>oiq7Yv`iE@%sIRAoS#cS<_LeE*tlw5 zuj}y+KqYg}lgDGBFDlcUnI%pIWkD^rtOdEXu*;O|%%mJ8>oK8jJ`-3;UG|qIVyYbIdf#Lqx&C*)=PVP0M_XnLWX!n&M{oeE_bb`| z3`L+$+#y6}gaBhfux26v43?4Lv5;Lv{V`nGZf$wT?%taDN%_3;GCLT}HfwoDI3}gl zKc>1&YT3QeCjxX&N$yxVvzNv5a%$J0E#J?6$(i&Ek2{8b()8(Rq5TEF{Y-bhmTj!} z+%;?&Lwg(N%>FapH2xQq11+Oawr8!95q?_`TAz{(Qkob${;UplKe8&uZ!WwXSfHDD zUiw{jBslH)$I~lbcV2bil$|(U_{y_styGQ-IrGfsWZS&ap`!P{=bReoQPQQCUP_<- z#O3t;Z(UV`CQ1; z#8F2bRjK7Kjf=}ud#DOeXx&b|ZhdoVrr3CGrBYCiOT zK2hJiE`bT!(>?1hKHxe9LskXb+Q&Tnn@HZc(W1re~rzm4poTi`u)IZXze)kkI``^+9?>L8! zKdJFW*}297AxiL=Wpou_{BMZoY8*QMm%xjYpLPtL{f_rKY)3xvxcXU@3Y{N+{PF&K z^M=;eyWa3xI((wH669fYZbum47$BXOV6811Xe}~#9IeWe?XIOH*_Q6oppVw^>a9R< zcSw;n@=X7){HKr8b(4%K{9fp>$gXdG^~y>|2S4PniiZ;9agTc+v{-(5TEv}gv5Gf% zHGfjpE&u-IE9jty9PSSIA6$DiZQX4v-F<@Vxcl$h>XO94^jGgbhmLvnsa~V)R#1*l z5#)a}MqU4hPB&A-|2e_5QmM-FfQ&KYsKQdg4>tPD;kZUzm9KwUvj7zWRkL>2s6KaQ{ON zuX3W~`T9f$C3vU=NtL?oc^Wm9^Pl^~^lXwb?xl zAY(YF@NkzVNlH==--&-F2!Dyj%i^aU!!u zDuIFznrO{nE%L0$hGO#^bHt&vG`Y07Qk(yQ(b)yxNj4?x%L}aYe;!QUXq}qJtq=!4 z>h$0C%<6)EdO7HRlc|#O7)eGuww9@L(Ml5RiWO~?n8q#WnI+Z3@y%duIUqt{e=R3B z`|A2%Zo}}C7dn3QoFqky46ocQzl zfAkvcaj^ear2!kqo9&)$F$Oy$kfd$LhLq2k#1>i`Gu0pp0*28~Y+V+Pfpp}GT}$k+ z;O%+)0(CIO`MSoW>~PyhoAUcvycqoCQi&Ias=)_}K)|`-N&H^z@4(Pk+f#Fr69sdXs!S+cuHb^AHto{t2kIMkWyh_MPjO73eXZ?4oABfxNWAIu;aVSbl zfp6-*wXpbQgqCMY;M)@}16NC!emI0oVf%CtIN)*Fox z(gRSBkOps%8_k$}v2bZ-A_=;*U}>NxKKVi&xy8g&fk>0Yk}Y3T{5z)^>{Fklrzt*oo*b2 z{OrULa4E3(FIu_u{3?b8a$3O$rWc4z9NZC8b+XzYIEi?ViXM!!3g z2LgBHhJn*0iM_cv4hFl2n-7-SgYFM&QM$DPs>0UUk>_P-a2k&6CF2r8l^ zkD{d}Q3Q2`S1B`{*mk^VGPE6O!gY6zwo3-5BT(%4l=8+|+p&W*ulOz;c0YF4V6WTi zDfQOlT1510Xm-6Mt?Lao)$6UVG0E>mQ{uLCUm!CR@}}9_taYOtLv#}hEiJq;@G)Y3 z4iVnRdO|mgm~)MTP|dCZ_)}@K!(`s~Fb$CQdB!_7p^*-%baEE*(gGfQ7m^i=Ty@u- zA@?7T?J~P0Q*!26r|Po9rWy-APX$nhuDtUV<&W!phWw6t)6vG4s{4Oz{SUH~a$_>1 znRrYp?|Rv7DR!ZQXKZ{EmVubfrgw1H3aeL$z;+7VGs3QLZVTcLa%NlgwlEx@5_1G` zeTFs{YAd%h363+d&j>rH84fBe5WpaT4?9-H>l{&r5bPpH-fw@dc?9pl?`Qw+e=d0A zJ(E-5#HSzQ!5h0zPMp0r+lwN`H_HEGr;DA=?0U}s@tkHn-0*@*Z^<6aT<7u?D}xuD zXo>$_#>V(EM_qVC9;V{FKH;dx(1jOVKx?IKlSAUQuYD~&<}r`)dk1WZK>H88`&|0m zr!V(s4>|NOdd%dcx8RKzP0qG!)Y+2yPV?>*$4+tZ6sJgWw4)QGIKYoS`e^@Mwj{T8 zK}+TwD&Hk2d-LWSucw!scohBR`x-uqQ|o6ZNAAx)_%L^H6-Vf? z#~kI(qXNU)yOW~Eqb936dMS>puY4(X19<4e4y$j_+U^^-Y)5@UG~Yfw%NJ~Obz*6@fAo@*={(&DqoguE<<08yZ8(0{dFRkkM;%Q!O@`0cZ)mz| zEgXYnowEmmR>tqSZ4O;Q)|8 zZ@*~e7oY@NYW42OycP<{j!H7U^TyvlWr8twU%CGwht=SXlFHGE-@7e>DvS?5th=m~ zR00KT9PhS^HNq4QKFLIvyF+1{iR+gCZ2HE{LRbAr=+RF)mL9D;Q55(r0HM%X4M3Q# z-}A;Ze0QhkPY!a~eSep0CHUZXo^}-d>ld5cEOcA&a?kr7w33Faj-NVGc24-n`$Gm8 z58tl47nXV3|9r_Q)p1`^!THbs;*oUtqg(LBJ16<|$`_rovXAec;QpU4KDp|kl1ebg z5mSH5Q7#7Co6df%2NBxNHmk&^+t{km3r;S5f9xQI!C`xC@Qg+w%LcK;zNtvSAM60oE^Z$3e@yxW_QPIa{ zhZH{Bc5BQ3t%)a3e#S9QgJp+~5{Ov>P%M9u#VCg8#YWe-5;{%x=3&g{W{Vc|i_?MeFTFt@?3) z8&UHAF{QPl@cU9azN+8Rzxs`BJwfflIQ4-OuvWfZv-?$9C!q{b8xAPh^5kj`F+ez)ZZk@)(kt9 zb)L7(l`{KnbU<$J^z;9qQ|N;%Sh}8{6CvZ;LIOnj$!QH?qR`e74B@okF%cBa5dqCY z_pSarN_^pq?mx@*)qlB)zIXKx>U9KVgdxQ6ZH)!;GINXeVf36WPhKFfym`^YhXN)s z_~zFLZP;&HB(SZ0jc{;FVmq4*w*>YwT1Q!+8z~lH##9gMw@0m?8zT4H-A zSK$&bGlwb0n@|X7@eyf*I+|h5V}t@Cy`C8PG81VZ7igW5@Swz$HYU#CJRef5+v{MA zq{DpHLkiouOil*zAdUcw6>FU8_WG7+OJK2=DHeYKA+{hapcvX?2I!WPjbS*OVo?t| znl9MsSW-J_lG86VF$hubP6+1S+2yT4J;`a-h{ ze(d()?apvPFis)taiJdZw3rIs?G0tPo#m_e&>J1(MdaW|Us#pcp(E|aL3weN7+ih~ zhd!CZP`_Ic(K=eUt_2gVo34j->uLS^=3HF2Ze6{%Zv8sn`oGv{AU;*54gh#rlF@cNnGv~0_P9VBaKgRDJJ9fn4 zwZk?hn9`0Rqxc)^(zPW>02Fk<@Qv3#W<=DpFbO5ujEmMmAMGIqbGUucb_uOp{p)?mGzt_rD8@t9?1^*^F&g^fo2mp?~ z@$u361bb@yprx9eX5S%^IFFmxNtqqPrf0{#>k{N6Xa}$vW7m;)PY);;+db6%Sr(XX z?sdl!14}plekIYi4-WPGFz=V0OW$lQN_x}p&Dv`9d)Wk==4GP(O{rBW>`@&&d|SPv zli4*}4KawEo@rf*Pex(G)YaPclL++wJP*;*xZ}0v$FU7Dn&hk1ZZMd?q8>5*2u-18U$!IReGr>Mf2hJ3;{+#`|qy+I^FE#{2ZM4}T2Z>w$;W@+E&M zS9aql$9k1|@3!Ywy5g&?&b@b4$IVpsthZlOoiruwyYIgAQ+GRn4t-d_Qu1GZ`Q`Ov zsRZKOb?4Zz^{n$Qs?MI0_s~NRrMpjc90GNG;R|1=I}%_W`|QfJ_hyqo#-A-n{f1o+ zphq2XB;D_R_p8sB%9r4cyKMKo(DBy7}Qzv4>Ty0OU)l#~zoZ%5X#DyfW9 zX=~4k9=2=@-2D9H@O{-A&a2nOX<2Be9JjOIu8aVI`s3;(OY46`pkb|dx=VDb=!SdT=iteCyoEmgjW$;0P5S%p z*4o%(-^NSdn%el-KVMPXICKwtb-4j@_n*06Yh&B!Z9MR%L6;df47=PFLD4Fl@VRdiJHO zs_Z#%f`89V>~-(5d27v69H}4sVv~IbPUODV&MnVR-TQv^SdA-wAQk6jhm3=E13Opw zbB}%QMtl71VRXNT1dJC=Fn<4s+iIDg-I}^h2R-y~I{my06YZ6J2XAr8b6bLdF9nSD zg{fB1l#b7LwiA8lR{F71furma@q|MTs< zfpy2HX$*_(xLj?8{kOz%Y^CnalWe@_Bu8)9UUu+kuA(jbKG{$K ztLd_mckf;1@+6O;S4O7)eSLKxJ58kJ!=W-!%mfdz%}JER3o*3A3V#GBQ5*vu-Ai`u zz(I>K^D;XGe3q&+?YGy-wjlty{X8f%tUzFUd7jp<9|eqvKLSiF+5ZN)z#Mk$^*>RE zLA3s2ELWw-N1k`!2r@NTYz>NFY?<@gl^s=+HeAr}b<;&TWutr-|8L>N!XJ~mY}eH6 z-?CHQdhVXtZyl3$Rwq&3^ntP-avM%XQ?X-2`&I+-q}{V)h|6`EgWNurm!X<{zg^mk zP`8Wz9^0|SZL2+7KDR>{OnbNAP&u+ z5D9tge#^u9R!NsnSnCu0=G;bIbf5&Mbg;L+N}jX}LGOde@LpUf1Nya%vAERm1)6Ro zW8xN*z$`lz9&D4bdCrw_KhEuMqm2+SK`$1S77N+~Lm3j!mnJ9EjkM4oo!uQWwMOUXSk($lQBvo7`UN?T#ox$9l+57GkE%5zkd3xYxirw#`LLg zjBCCcJ*t5lL#>l?5QpQ_98{rys*;}tVd%96ZdhOkQ3M~5j5(j{tl}wZVCyF$R0Qw?uu2BTVjY?3C zGosin#PS$E$Y?KMMdQ<*eF|UOtUzXbF#FA`pCuie+ENMZ_60lpSbtm}fxZrhN5hY6 zZ)Tc1J+yu9WW56cIs#`?By*jF)u2j+aWm&oCsJYI4+93YPSZ`Cg9G}>PSf+;<%9b5 zo4fny&`SYU9c_gUpUv$~Z*5t5r}fq)%E2C3R-u!OU$Z}PG0@wg zkj;R$%tOp+TnJzaJtnUAGHisu;+_v_C-7`MtU8eexuU!hC5S06Kv=G7KX@*q|9EEg zwOtHsXSxQCO_Cc-c_9H=g9-ZsywS^!4in|cNWJY4&m*tVdk(^gUMfu_u>*^R)M(K3 zZMJbjE~B}*inpRo`7!SEEJtcXi24AKM4vLRrfeJQ0|YgPS(SA|p7+JIEztBjNoXzl zg?pUg#lEC_G?$}^c6fe&HizM*a-9^Et}gERijX*{mjfxcSie=-*_@izNVE2rV6~Pd z->c1t+x9uA0%dKUV}-u6a_vcP?+*j&JIrIuCLRds*q43|gv%>RkUXLalEj=z>{pPmx9=Og5QX4Jl!&I$Q^nNP~6 zEO373M?yE;+7>RC(&p+6XWDf`2HM84EPP&jqtFj-$>qwzY;k~Zog8Ri{(if+&*li= zIP+~efMb^pWvg+bZ{5`1ipEsNa^KDEI0)=ZspA0;i2#lQV+k_3_NLM6*=-Q{XR7F5>~C@$xC<33 zs`U9sx6k{0fI42s5RV3~(x)X(OmD+f>a}s*Z*$m{%X6r#(Mz8HP`}&G)gffEQQ^Zf zQ@_fXeO>co07rXnTC>m_(e<}A+UwKK>)P%pyVT=GR4+{Fwkhvde%SJq@!NM-EY;=_ zrM|CTFSGW|E-`sm0uX+<;CwE$Q_5UDIY)2ZK_0--r|)*Da$a{9c^&B>>{*?h>*dWz^-@wK|J4{GdAzo~M-m z+QduWxurcnY|kNMMH~9(?~ZkxY8$)9T2ur*3SIu~hNL|vr}Tjuz#*#dOOBf+C+U}d z*y_Qw+s2mv`x6ZRenW%f;rk51a8n)E-7NGKrMG)Q00)A4{cS>5wD)J}URyN}=#k5= z3wk|hx2&UV{(sBVg)6S78H{`G)B+zIpsDP0|KM!;?@u^3(cZmwEkO*Z++=~!noP1A z+4dwNu>Z^D)f4S}vy=CCluCfX!CPB%|8X5oeoi}FfT&|0A?JGC79SI9f z9=O?@_N}i)CVXdt;fLGjZ57#B0yoO8H`!)xsnY&!k$;~qfTIM6{KquDJ&%<>l-*2z zG>yeKRY%-=OS!MsP!(A`)ji4dTbtbKQ`vcAzlpat4Z#0Yc2sG2DX9f;V5uDZ{?!RD zH>$qab*k^DH&dEVWxKAmQRMCqZ(D|`1R3mWa+jcz&c3(0m=Le+d6DVgyJb$NmP*|v z__FBm1RLqRS9R(t{ZPiVjL{yOA_%)A>+AIszOPmMl>m_HP19In#Te->T{U? zZ&w?jZ@yR1GI)9}lLBk#GI(QyD zt(F_kCtyxc^DhAk^>A1J(){0XOmV6d&K5gp3SA zi)_&=zj+mm-Rc<_1#SLN5`?tE7?d>LLP00C;Oo1lfbtN6F%e2{5qhs3pFB5K2?as~ ztSkpvw2VV1r?!+4EzPMV=?h4s?8O=g+e{%OiU9N8a_qab|g}ydrLdBTmw884AY;ZDW9j@4g}3cG_bXXd)Tz_`s-wqQZ$@ z(&OEtB~sa(&*4mUG1<$|XQVF-W+A5?FDH>NRFUAwC3s8>%=uqtqG#acKnkPXkh;)E zq~-3~^d;&n!3x+Mzp9_5hshlExlcCPWmmX?+cdecH|`g9Ky!*`N_D5_DBmcm$UA5( z$SkXaLq=hsfyq?6#%*M+-Kc{Tye`uOL@f z>;TjOE>F6#A)4coWUl|&j{`Nh5wbo|k&<5QZE@6d1Z8oNy76!vOUbDo_IeX3QYY_q zp0+r~Aj9Xk7x->w`(zr^J6;D?Ae6aKfT+!0$+leR3ZwV2Yb))dDbcPF%8*>2x3BWO z1a2Jj69#XTV-QY!+R^l7J(8j<9`3)jEeyJSvljnbj}mTx|I>Z#(+d9Qj{m*kPQJuC z1i>5>68EGKFdF~Qc4q7DA-$UpV#@8=_08McTct*?Wr20`PUJ6trIrMESU23>uqiKjFK#X!vYo>EG_GXb;ZFk=l^nf#zFwcPv5D*dy_h=O1|QJ zDNc=^wPviQ{8<)xZ`|GjI!dPExGN5nbxKUlmT7BL6&+3=`g{z^cAiijGxqqP#%IMT zxP3>1X#F6~Dd+(o3H9^qUJRrj;z;EucEJ?mb4zA-{aJXp?{PrD< zN9r4uxDT52lCCptE(7va@28%2 zdeZ9#n}2DoEOk>!5gqfKtjFK()DI9Ftx=#pgxVa zV8ATU%=EQwJ9RMT>i6950Q%EQGVPUoKerpDx{^IExoz(cNs<^zqjd8!TFf`Uhjj?e zZDI5snf)JFCHS5Bx$?5UEnU)vi4k^L2l$(lNWE2pG^Sy%Z<22u8sC_0!>YH+4l1`R zgB9M`bzSZtb9~n_Bkb@t-6%5swjENBt}JuKP3ziGF-a=(B8bsX&kI4L)(h_DCOr2U zrRUU+*VwKO$&$P1#+}z=Lw6vnQp);8^zHUSr#r^Tv+Kl98yqiLt_jE`Ohy0Ru6EWs z9n*C~-#k-q@M3>?1!Cn6`cul@vO|MFx!IG2mw3hcU(+Y#Hsw%t3UxM~;;P#bI0XYp z1P)}Kwl@65_OiengEiwP^S>cKk3-h~wAj4m$>o}xoz+GNv^2Gw2UEdQ9Q~zWBkGxBe*YJZTPLFb-;D1VTGY^p5 z0q^!xOYX|YIu-;{a0G35@P_7Xff`M2mK`_NX%I#Y=HS?QWBs5(9MtTj62wvJvga5W zg1ddx;T!^5eDTcv`xLum*zOrEU?YMt@OS^c9fCYs`BvYK5rRX6QV>Vo^B-p)_Ex4%j4Wua4VctPVLPu>{DmFV&$XXG#Kss6Ojtw_A$)mN;IAzvvvayQ zQw=6q6-$hbr|4C4v^$b(6c!yIiY z#|Z(EMi59TG#-yr362A~=gHV6nD*@@DYbq7rtFTusH}S)kCe_OZ+mQKuS=Cd00Z{{Ig71P zHkdf>Ax?-A&shC5&K(<3f?S}<{7I6>crQOE@3Sa>nd45}Kxq7lM`hR>bj~k`2R4bO z@Kt<6{xbGM02?xgwOt}~-q!xL)#MgrQ5F8!WIvIQT@yhr091bnp)*m&2+oM02xw0r z=s<*)ul<#^P%+-WhR4t z1a(F^3vQeDAq5i(bvh$E(Y!YzvEx$T4=ZWH%XP;HwL*oIX>3V0)@u(s5}mNqQV*++`i$TpD~#CM&vnfks_cCq#12qp&XVVE_iVd{M34 z9AN&Rc$Beiq-An>7_^mPB7M@&#kd2@>s!$O&Wu(UF^nDMc^kuXCQ6PcDT_HHg+#YL z5+QL343WtI@2dz%ZvWpt-y;zV&Af5^{^x`qWzFd!2g)h z`vdwRRo3yp(I`omnIJ+aKcL^Ikwxj-8xKecufoX->Fsg zZ&_$=Z|6$F0j^6COL{rWa@O5jEWeBQxQ9HUCF=L4X+?Xd0kH=2w`7AQ$q?|sy`T%p zc`5e^?aT-URQLq#o2V~r8+JXLhZEb#Hk9{FN0Sc^mic2U+>B^4)ekuPOS{4HCy(zRD#O$XlhAbQEG&Z^H#K+*dyyr5pU z9e{8y4zdFj&y1cs{m|><4pFaT_@W6_b**Z2S4<1 zI_VD^?OiwN!*ZNXNn6(&ezI-VP3N>flC(;!i4fpI5~nzvlsTwhQ%v<3uYyb)p*okN zqP(Z}|JcE7X~RHW@Kx6X0o4N#mon=CC!-!=Wb0ie)2{2=bH?CH7eGj3Aj{Y~Bz2dK zO#>0nS_AGav^^^ zR7uZT9tM2R*O(LI(Ofpg^PP+B(3cUSSOh(I5c8zy~f=$O!;t>oUiN51KbqFLDZ>zOe?{Ta~lDFj@$#_;C zPCeqd#~p7OOvPTme(`2EiLj+{(kcj$e~+KxV)~6@#^`gXE0OV0@4cCS{?}1~>7%8{4ku%57=8 zXgCpCIgN53jAFRu8?^FK;jM!^W^|f?So$_{3kXqvpubVx2wTX5E^Gm+aUenG$nFHY z$Bo4a1TqMR;AfByN%)BcmSm=CxJssw9c$bv({?k68HtL3c)9`#F{eJwi5z>+JXgY= zjleib%EBJU{_45`m+?7-l~A{0E3~UIC4R6`JfJ;qcNv3S(QS8G2w!Y zm+AyHIrSLK6q8Sr+CYcZ8OS!l7jKkJh(Mhxdy({-91`6cY+&ts6z7oeJ0Yk ziO3atqZOMRvxnGLFfF9hVQ>7MZb1K>=f@_$Z4rPa^o+hBMacnAl#WNiPwcuO6pL}% zLlq+<@`$5UW{N-f0%VWQ{Dkxvnhu8Ypl#R@zip?DhJOe!5yQXYMabz#DDyI<`*RW1 z#wK*ok`p6_L43@;Jx7&bH|w8KGdlkXj0b`TSHZyxz#2YP4wBf|V@;sDB=a;H)*-TS zF%-_AChH%o*p9xFEZ8%BKb)|a2cDYXx2#94OKfu*#2JCSPx-k7fx0@&Pzc6aZ!j)S_j*9`Zy|~RNAY`hq!&rn=MijZ6qSK7 z-4Z$q7Boq~UgTs~MHP8m(lbJV!AbU}Ja`N2qE6*$H62bUXx!wC;}OdNylL@68%ZY& ztqfXL?7-XDSg$u3Lm##n$9-F5j?B*a+5u78wlS6()IFLLs6G_8{dyT@JAIhUt=Tzn zgZ+sif*UUH+J%7pF`eL8Z4Gv#$^_Dodie|4C@6wyzTlhnyWclOL|tD@pGf9Ff$@le zfF!&?myip~L(g#h60OM+g~s#HV^O*5%uafYoyp0GJn4%q-9koCSLS>bd3|r30osAn}LVRbI-~mpwuGzqp06SESn{peXo(SYG zw2_wTPw*>Tz3z-M>^w0F;)>j#H75NUvQY?N5Uf+^m}j2)69;d6`HNT5iBCVe?!Zw} z**1HRUGm~^?pr2{gb9$ZvvP#^(y=vsdX72&*J@$@k2KtV%M8ADW_9qt>5sYa63T)L z>f#P6ugY{~&}h#Ng1`__1f_cA)-jr6iBc|QP0FfSb!T6k0AB(+e}VtA6D)&}WD?YA zO*%X%|I=VTcE#7_}Kbz0QayRga7)>$s({^}~+{x~8K0gWUgj3%N zgiQ7M9L@E=)mzfQz1|P{!(G5k!?)j|AI#oFN)r%l=v1>TjEI+@btdZgIQTJO&is&Q zyJMAkKBIf;=sves(&L_ZEIs|Xr_OTx;mc3=ZSBjk0ahyEC(-j9;=BtZJaMHOWr@0A zW5Q=a_aq^=hJjqScpX3^O6Bb(CWd^@i=65f7$1WO7}@Jbd@j{PJ`V!?3_(qEwH|g! zx}7Hg-ta(R|AGu?G{Gy3l58^%{g2T8{grQKEs z1cs=WJpvFJpBt7X^^UuF1P3!Ky=068J{KEYN+xnSBh7Uw&`*+^UG=amr7V}%s*_z$ z{b<_!r?bm}Q~Kk@xRc-HeFzx(Ip)&*KLvUudI2~9{@%Vb*=F-MhfWQLek~}k7fEVq zmHtPz?)87jRfTPUj==ms@(NTcxj6=ZN}>mvwaBvxp#5|S?qlitL|}~(#wG=0Sp7qn zM+>pb_m(r+*6gan)q&r$HaMs&={urMUM%$;GsN0DF^ruka2@}nit?<&SE-eeLIL1^fJb;vGsv3;0}2A18zKy`J^=d7>7xB{UByAm-Zp%dG!l_? za!6}wH^!xIdy8a2*#!OR#8c=v!G31`NBD{I`v0o5e(6!&^2E0pWoL*17{mh|jA(6< zhLI$L@jw(Q!u2=XqLpMg4bPnO8+>-p0;0>pqy<})C*`^vpVHuNz={!*H26oYt2*>- zYaFql4Gc~Q!S6&wjo|IPVqB^W0Zcw&D?d;vId+WKK)E0}^@@rX2u%bK=6D8RB_dM8 zLZGayEUSc(KBF5jD4XEyNLn}r8$?_E@7x<(bKtIjl75F2wO|&X&VnLpG=oC1l^Lmb z1k44BNq7m%6_|>gw)+LTeo>|0eM6~^W=6X)F-6~T8z^ft$u%~|dpM;7bm#~4$AKiC z;XhEq=^pO7k$BZYj=S<{tgy{=jki||rM~X9?uUe-)Mm`7q!Y^KfC7^bEpH!_kXz*q z-X`bRn;?d5Kl1=llQGm8Y3BIkt}o#b^;QMJ5zIAL@Dz)tOE_&j9m=e-j@QNxpVi&+ z4$cFpbWjePtZ4Td45Ln{@SamPFN_rh|Jxh0!E3x?{7cP)!mU(Szl*aL&O|j)SNc`_f9$_SQ{g!F+v_RT2C@vK>t&gcQ^s`{9StpgK=SB4)v15 zy--(_=OhCdV5)ub>&E{L);_R8kNNP)4|lRv%Ov1f?^CLi6S|wQ%=S?v#|Fr^ka^_x zlR#b6(by}mjrzo%I>QD&?3fXZr?FcE*ubdYnQh@WoVJ4$qChae8U2Dw7Y>!S$(gb(Z`QT#W#I3fbcIR@}*l)5zTtM2Bqh+w(~$&*pj!VjKFNF?v_C(Bfr8kK&r&IZ7sz6~Y97!#jyV)VGkOSe zNo(Rvu;#7$QY4Ls2a}-wr@4d3`=vK)pmwKwEFcl>ICemSHo{P3uhgB8{wNWz(}Br& z+hr$AI(x2jF_W{%pUQio&@%i9JmX83*9FG0!nFeo{~13~1ZA{(#<*uNP~hJJ!Y$at z`h@a80GW0@QarbZrH*5tc`6vZd;VI;!yylRWK0h!?={|!nQzyK*S@rSfxp4ane9yfOGhi^cM3)l z<8cbQHW{CkOX$g#1$*zc&901xkrpud+l#z5ZCPCd=>LSKWS6#{ozaM@7*?%zL^~*{F_1lgU$?q;ycZ!6vohdlrVsxv9&|N|7c_3e3Vlu(sm6$ zk_80KuY28V{L_Inr$x#SO;2WN5>7VKJ`s+yl{FEE{f2Ua{bF+c?{wt=C2k-KVzX;j zS(Mn`b%N!K1}%6_Oz4$u5ggmf185Cegw%Z-&O zLM8J=}BbjcsGs*t%IF_JWjQ0tK4}_u*E2DJLTFnMFTUz!1DD(9{EY0w@ zRl_vXU5sO%`;I!raP0xDjFt55OM;>CI3>BgG+H+QU()9WKiamTOZtqI_5VOPEYAOi z>sV)1J_h|C`jlwiuO%2M*%UFRCc{lyT9C5%wGDCfi|1u1^uO1uKO$e?sNd-WzmYP% zdz=I%&0h}p+|)}vyk^{DYAJiimz*A<9E0p$M^8JFs-;v{_!(^;SMzlI&v2D=q3X#x zN=%vmQ|l%l^gismPOuA6?k6e#X;=Frk|_KfEi0_lSAm`BHu0JhbLM}vo3Wk1g1}Yi zW}n}3`1!0X$^-8B-|h+p4we#M^rHLEF!YVjT}40q<`11oOCy7WW(yuc7lBgJ z%0=3nWR8>o)!7FKCZ09q$C;2VZ`%!{-L#!CM7wH^L_dFK`nN+7+0MYxGin5`weB7M ztU3KnFqP|Zv}hfDu8eDC4b3AcJ?rNPNFGN)x}`88OfX31cSwtTm*?J%~=ltq(3e^i)+3{yDCnPr6E_{OwA z9d0rylRMrel&2Yal?Ik?P4IH7n2ADZkX`px_MT| z!X=rLXelLnAOd3~3pNxa5v`r`S-Dy2?lIi&sT&vB(wjb>*V>NuTb+Sy#|uZf$k%W@ z=riAG$YdYKL~-D#$DUgqZb-m=3ns&fqqpt!p@!DLz)y4JaDM|5;!6axG00wVWn;!R z>M2|3wFM%f?weVU7?F^{5(}BBF2_Cl*c3-`#nB{4hN1l)hC!W}1Fc&(ed_OZdad8< z*3~n`OPQLV*&UzexNNxR&YSb}_5G0Nj`i7S1h{*A3&PDv-v?R<8- zB;RlSKzcEmkzlbu4BsiS^Pv%ex7Eq)Jl;L6WcWfi7_HbKghc9TTnOonjBIA@3Et2# zKz!MPB%-`M3M6p1pfsZy{li@jqF79fx*8T4m9NP*kidAZw|5XmHjhoNi~KJv(i;wy zMw^|FI15>vc#)S!&iQev{~}2qM;_2uA`9EU2VL|*b%qPmR}4BT=go^@o)79k9k)Dh z1G;p1!wIu?DKk4O>NZ>(ZwL}9GuWA=LuWx9lISB{NhNgzx2I! zrySvOw=Jt+ZuN5FC361#>~$^2=E-S!;GH_W!%H}7%3vKJ+m9 z&X0($`@uH4^2#gyehJ*T`5>coBajx2e62zk6+es}Qnjz^RyoNSzEJFWGYp~!$H0I4UPb`ivrB@&FX>R#z!7Rh=0 z@$YF4p~&+uc7Vn}U&rUs&Jv-SY;AvAhJzn+IDP9@p|5{8)80-K?cHw+*KhLv_4Tb` z{|6S5EKl%uc?9|$oG!jgZ<76#9b<&;GtFcs(tF=7FY!M~vY*%1#V}X%I{`l#|MPiOM!7ncmUZ&U3r?>im~QyfX;(m!Ty&y|AtV8uB47x z{fVQ9JN+NLEerD<@-}3lJk4-KEBFTCFqsZb(0u-nFfZ~B>Y2nlz3!DXzrEuG5=vG* zM@~ZGn*0A8-i1EIE^jnj8<@ibD|s`07VVjyw5N3^2n_}s2m&v(4B8rYJ#fcYnt=&7 z9?&{$twndXU@&AXe!}RAfnm9WpOn7=l@#`B-!JO0n^WeZxVa!&sh|J5Z!OU$r5fjJ ze_5-|QzB3z5V{QfPZaZhCQkO+I&N#m>xTAx(C6iT**Rj+?{)RV)3r<=`))@Pmf!1! zsSM@3rCzJ6jxja4(Bj`fs8YlanD4&C0+ipk>-P@5zFn`kPZ@9D!L(z_eS7`Qbo(g& zyzG*8`!oji_mroc;^Fm``p32=V=)FMcisM9^8YJM+UWH>bXQKr;vwSaWt4mugilO zux+K03eyOI90-?$V>Id@iVGMEA16sNCgV6K1Rq4a8Xiu8^#%r6qNB}pol%D^VA`7_ zDgT&x3*oSdt}O*K97jpdkh#*-SpcI6hTIswWtolq ztb1(XIOGI%y4ik+%MONiysjp|I1?byxe(c5DNA#tZCg z_4aZ;07UcO!vCCLt$hGD)(_pnk@@!^B7-7m!ZEE*ae>L^x2d*_&Y& zyaCN;IcL!8jzKmqI3|P9JVzV0U2y?*03F)v%bJDlw>~plor9E?(wOwG-L(Dg5GmFP z=;xTnCh8m)biL4$Fmig~yf{*F51>y)@nqj=j17+hpy)t`Qx*9ODFeF{3rLn{1~HV6 z9yM7MZ&A{s{cr7??(*$ki8{NE0xqym8>yNF6Yz_744qMuE1u~Lhe!jOf{#W4=lTH3 z0q6lOUI96VZ$V+tv#my*dZPBv*KX&%NTjH=gORdfa zMFJfu2Z5}kPG9JS@@T*h`UWdiIF`)i)GDxN1Q3gpH_Gz%Egi(#N1#=vAV{3=ndZTx z978U|9IO3|xsFpn5N7ZU;3a58ob!f(lC9Q;NtG7q;$(6J#+3zOM8KUsy@UCY>mYKl zIvAVOMi zvUBhty#fWUwTM?4PN@_e0?$XFM|ao|g3C2<<0lQ?IPE1T(_en@z4f}_c%RMXyz(aB zjD83xUYiOWA^*#q|DDuPzL3e>;3gcAj+M2|;D4l5qj@Ol*FFC)$xv%6-Z0NP3pt;Y z3~xEv`5z#V1zef*hhl7aQV4mW0V zwd|Z}f;0aI2(84xSwBeT2>R@&E~jg6CfarzX?~(D8;JJY#MNjB0Us?Rhg2NpdG!OZ}558k8B?1_5 zlFYFFhV79vP|K5miO2ThoY$bdiZT(hlR5Bag0U2X`3k|Dqm>P3x8Ijw9N!Sh@G9+| zAD>4C8mzl(0i`JV{HHFXZ%nlJty>#N#W8m0jYL1SiK_D~qabXfx@Nkt{@=Q>%>P0- zy8%3*jYDP^}@1$pU~zv=5;FrPFE1|NZ)ZgW2df`qfA? z#+yJ^8VPd-%G)EO8e=6hYgRK*L_*B*2JCt?|3v?T(SP=XbyIcXCS|wa{z}110`6oJk6dN=KJ-( zIsX1X&fewsyX{E}s@m`AJ93o-~dJ>IBlFlNO~p)oDmcf(Zm=*2S{Mx zKhS89COUKA1O`YPm=3f9C%|uRcVeR9-R0SP)l<(?wbtkToo1i&en0!NE>*Rvo~m`( zYcqfKuicIZ{o~Y6#hrOSdI2W9K@c%{fDXiwzmxsDsp6H^q})~DGER@?@?Edj9Q2}t zF9)#9I`@w89rvXlo4Vcoo5Z=Uwy7AM~Vnt6wiW0R8KQ zz|_f-vC!ex@Q?aJ8#Y{O|NkQuY{M}6q>@{vUE>K#&+5Oe1>UJME^{o#`Nwd`6a*3! zbCij+fbMq9Il60%|7+a3=0@DwjwoTxa%c6qU>^K}I(I7mubOFSxZqGVTCoK$*Xy z@Wr?QgIgC`7ewO4`)QO|yKZ1GsRsMrMI%uv`tbLp!z!U?HBBwJ5J0Q`Vlo^3V%Pt$RK0wZ3r?t49+R*0>x#U}7F6El(B-)(7n9u9JM1!7b z^x8HkIuK;!Pt{2$X6nd~8@4yWodBvJdBk}*o#rgO(eyf^G%}r7z~JsGE*gKyz@T-> zV&g%wwF&azbD;)^?qdyxtG{QjAh$DQn-Uo5B9gV)|a$y zEkAY`0ruCY07RY>UsgGu_Sp@M9WdI$8$4Ws&yLQgNnK0eYj=&*8!X!2Ku@efs`PVSbHFx4Ef^%eBYEhZ<4~~*47#ef+xO(r4i{yHg>vAj*c0E$ZCvT~+JLyXsRrt{=$pzLwxeAsZF8|8F>$Fc z1b5y)OBT`EhyHJvQ`OfN>A7|J!#E-5Ori@i&;l2*aV4l7qmUjnT$h&FaS8ZU@teya zmH+^tqXc1_rWS2et|pd2cOCTOw#O26&k&c8pGp7Hk3NuC<&`!wCe`CgUwP$DG!zr+ zu*1RrklMff0Q;v;(bBAJ7#wX{4b$_5=qdbc z1=^b&$U~}g+Hk?rE(ce8l>sbJS@O2Rn@01?d(z;-V~ca8Qs=_MlU`Rc9ncG6InRnj zo6^MJP#$bb$y5J=ZCvdpI2mroIN5CLhfLaQum!M_%2K-EIz_=gl=fF5#-I(1(qNl> z*2G*yVSv_5S5m>c_s?=dcW9e^q9R%Yyn2;1z_0HEo7r}$lmYwSFihTrJ~(3cet#y3 z&SKsR2;2H`XWTBCtITdJO-F=2s=8Nv*a|ycMO53%Ewjz^n5?ixafJ zVn5b^7GuM8|88_By>5T>PyET>)Z&fb_>Et;U;0zOXutMP)|0{*_E>e9(G*-f4Tz|KRVSy)|90ok-U`*gs8<(cd5bU96|Y zt3@-$)0All3-kpsKf3XDobUdB)9n~#M5c~SblDec!mOAD7}+KwC3lD|h@D&eyH%>weW(_Tey_+=$NzLY zc>MnU?{)7FeE;wN{oX!)()%1kJE^!gam#y|5tN|~xglTIJ~K5D#sca0QQ!0Y_WH~R z_|x$$#>|k5(f&=+IZ>bOOa5YG`fwcVzwAYGi|+d1d^Ob=|5v>Yj{g%!qVfXAV)*bP z(RL12F(J17E;=&4Z41)Zz_4B?h(Vp(0x0|6gXJ8n5Vt(da(aY*xt`+dO@9FyLD_x zGnMge&$bf6J*@3|Q}$)mP9gj>xpr|B=D|)}#{X~eg&Kp`SkhM`9K{FwmzcI1>tJJT z?!|OL30YL%^~wG}wd-?yhl2Fb9XPaO^BRPgyi2KUY^$RkbX6AZs)vh8X9u6mk#Lyk zYmmWatT(}(GGu4k^9}p;2IbvWPp$mV4jo_g`cc;KTF~*-bfsoM5fC^5Olp|!!pZ^XMAB8|1CUqXnsrO4}TpFvjz!Fw2fi1!t4|%F!(dd-&(bs zY;ewo_e~z#A-cM961Z`v0DHDaYgGhtE^;#40<_@7MoB4P&o1TfT}(_GVVm?f&87(K z=z;Hue&3{C3`TD93ESw#8@JsM$Uh5N&s~isLBDnAaN_MoRuOeyv1gDd7$W%_*_~(2 zTx&=UdALx7_(J0LvQKX=7s9oHqpIFQqCxC*q;cL#gI~qt#buar@kZI;Q^sBStK1u9 zE~bH;)IsGT+bi`=F~?07#0Xywjzi?;o!;Asb(YUx-fvEPabhnD-?bj?9ki}!gO6wd zb1m4&{9JrtwUY+QsJ3e7(eB^KFP}(xZroB92F4Na8QLjSrwFx*L6CK9e3{L+#T$>t z@_8-;{IW2{iVyMex3=Y8Zu$SA$X-6M6YJt|%0GGV8U**h4e^T3=XtJMmyEFHsSv}u8D8xX3ru+25(4cZ$IEw zX#UiDr$kK3vmayq<-suMl4VRhES`**$Zt(E3Va^kLW65PH8H{S5F8~Pb8wG@xOa}i zq>`BDoqu3gQ^KUV>5*hW{2w+JpP_}rBI7bn zlDa%wajgMH%BC$w|493JtKk}Efk((0eW4cbd94qmPb&V(N{l+L{;E&Jl<%6xFoPgu zXi08!`6zF31_fB+BF0p`T>Q|fBR%|;>LrggA4UQw-C!OiP|L^+VY{w|s$;B@7;|!h zgtlGD#Mjcw5PwG&UiCSjm2G{0{IP%UPuidTkN<1?p~sK@`9FOZZ~W*-|Lnc~sqY7T ze(%q%mvtt0O{Tu{-EMhL|Hc%89PCHzWj8ag6O}d$ND8o?c9|4M(>|jX+M(M&#$MF^ zdu*TSJs;d*;&NzFV&@GM_@Mc8lU>X(s7lRo<7AqQvt^|*?0>^FCl56?=Vtue1MrOY zUw9xmAyP2=IG}CT+sV3di2u_PqUnR$fz&w;-*gS_15F6SAZUnhSGv2OWE%DV0~J~` z;(AcgVxJ1jgX4eNy$6{t@rJA3kA(}%cMQ5BtPgVIezs`&AY#o`eGg)Se`$uf$Y~iYh6G>@b+w@&`-OY3YizG;2 z<1Liqe|T2BU*3G#Rc%lDk{PyN=s1>Go%Ai&MBTDvm>IAFV}FH5(uXN1acjbN5&C%4 zE>?dp>3ZtD#5yAKWu2AwN*iQ+8>RoIe5sPDj^#^?l$+9oGF{YvDPmOHt}?Lze_~F@ z88eOlaqB0Y_Sozn@}7`%z$%U6#XEFb>No2%@$q3jeex6D%euwSMAd9%|JZ4q@uO!@ z*eQH>ejo9FgFrv<-1O9vX4<8WcQpIY{If^|tFT`S^-E&^-m#kY3V%d-s)SY6t=;v0 zF(+!~kf-!B?x0OfM%*mE5&x?X&e<2n4;xThX>C#qbEp&0xyMFpbTbL!?haPea~v~L_jsQ^j(&0Nfw^KG420Gsl)!W?nQT3f^C;! zo&PVcOk&4w|JOI{|69j*r{`+{hqVB2ePhN<`*l#BQaFzB+t!J3X@U^r$lSV1M)m(n zk@z-dq;SGGxTodd>iLUZGhU*f@6!15Egizh*N7VQnF7T3a=0J}0i~^7I-bw7+-7{^ zPheSs=v$qc3K@!HVY7398ecwZ1v!DnC4Ux1Advj4lN|y`vDM=fZepiJ2M*hz#+ldH z9_Hm;AG@}=@MvQ)A!Vh!C5LGZ(GZvHE?GT~%Kj{9$40l&-~8gP3(&*pEH!}ll$p7< z)fkv8xlI5v+g7>R1p8>|D_w;3L72`MJ_)kO@CpbP{od&D52$dus{@;-XD*&?TU;3b`N})M<-{B29aV$ps*YiJ2~iGwbY1Ff#xX3O)E9X7x5?9G;}@WxpfT?nu}?U!XPil%H) zu9&&?&Yv}m)1VjCx5?>zUN32ZdnX!RGLW|Ls)o+2Ci#J3`KJ~En77WBd0#A$ts=4; zgLs0;*ja;<;_KPGe<`xG_*8pbxQvW?}LiQh?8*EP1c&CcV<^oIW;H>-BOrA<9$Qz%feK}x^my?40H zru{Q^KHSR(72^!D!zepcA6Yiv&|mp)3cKzR5hDqxN=>R9RA&P}G%B+pMMqQ@V5wSf>2FKw)t^*5UG+83K4hR}#9$l#t7Aj8-Qf+$ zP@cyV9jGkXSoC^>5=4p@(|s*adGFO^4~WmW^a{zR;=w?V+EY(k6ur+d(VO*L^2|61 ztof&&dCErzfFjW6Lh6w8;3NINF7~MQn=P1*pe6+LQzc^mtq#gr`!kAZMX#xBdY|F7 zVdUumu*XYNFvfPaisQs;-N(RDY7(hrXFDgSuz8V(nUzh|Dqhe>$z=48YoA%nKdOCSG39hyab(zJgbef&>j zKNJ53I#2v-S(jiTWD^$6M$nM#x8?YsqrFV*Jj|P)i><}mxTu;yuuptIPUD!4)dGs@ zA7*2?|cN_)(-4@i0n9PRQTGun$zv^q* zipZ}fmsf|hNdjfh-dK-$flHk$7|VKwvFIq-5NiZyZ60_QWqT0^d7wtT5bOhsQkE`w zQv5k_v1*hTz(E2QM}cpowS)h6;eKk@;*;ZlbDVzhVdeir+cmm;F^&fRZ=)8Ia-mN2 zAJMQv{r}0YR?IFs&AO$$q-F29!bSuWhHqQH7~eGsF06Q2by3bDjw(pkgk3WKM;tZN zaodTN;J19u^INO&86VVKvtTUBy0VbsPRzkq30i}V8nm9!dZZmYsjDoN2lRB-Qwtfq zbNXCA@&A`g$6T%AnC074vo(&CI;2jwgG{rm^+F2n(*7%^PvuFr*S|31LKvdwa^cR; zDQbq2pPYF8NX!|{XMvSz|DTTUK0UYu$lk>pQgE$mhthZlA-Id`guGlkw>Bkgv_`81 z3KfimPIEG76}hh0r!FV*gqyKwWYsy< z3pw^+L;IDhxwFiW&NA|0>2j>^6B-S)4!>VCp3|Y&`Fy6f>a-!I(I? z(&Sc&4hHpFyVjQvARm%W2Vy=Q7+|tX06Q)ADt+-yu2oAg0Pv4!;qrHE(Kn7zn=s|v zi(WcAFX3Vpx(-5swsHz36EE7!hFqFpd^Y;ZirJ1)`Z+t}s^jxE126mp>rNc%xsjN} zsk3D&>0pPIvvrXHiYts9B)adkVgbW#YzK8n<*0&s$OTO(jsw7OcDG?L7K=Afj_7n2 z0%u%iB0Qv1X|nQSM(v(_u7wOT5a}HB+brl1=oEp0!Q`L=m*+Kz0zZZa1@>kgXM*&Z zYIrWV$TWD4#Tw57l4lb0y{=t0te#b;H$vLsuCl7m0E}kaq^$2-X)9^)uZRRXP5Bvj zvnn~6_~G7dXlW$fiB1!FQvTK;QvE^Im2KT7Z(MP8>07h)=0BzWDqlGV-HG)k9m8nV zWP1#2mW!=3dq`8yg;&H!b6^1rP4x|?D&@k&Kc8W61~Mwl!ZVZaNlM!3FUk^KY}7g~ z)teh-$GpbC>!(RG;=(@QrILHaf6Ip^QcbBm#PJ&IC zw`IR33@LABKg9l*uI}~^-K%XNljqApH-|A}#Q)GU*+5~#z124%;$zTL&Duu?x{8m{ z=MWaT&;$dfw71@e$z&0HQKH$i)ahx1wX;**HZkM;1$J9u+GqhQM#Xo8O^;@5hsKXp zdzg9S4;BW$i3Nl)-Ly>z@ldT3MUaj5AWW*#9;TrSXI{ zEzG=O9P{!PpA;pr)Ajt`WzYSfrmSD2qrQtlzUE5%hchjGk`_H#LQxYY^n2Cwv>m^Q zK|j`pI~DJppZIjMhRsV3jJJ2WsH~zpfhKvEmjLiKr_mRL#?ZSB*R*eefAspQ`ozL6 zZc_W+aN8!wo1oc8WZA?k_>RhkML_0F^H#hmRvDk6tuTj3R>Z|nANw@^7X#y)O&|^W zHfiss@)vq6Hqm&U^Uhj)89mML9~NErrc##=81~P^XZv4e)BbrMw$O6erv;Q6v*}Ow z)!ODEW8+qOEI+0NssCr(5Sf?$zp&!K=-&6^Ur#Vl9dWmfpw>qX+k7Eq2YFeuLyZ?) z1(bT{_3itW*riSrANJnA4rSz}t6~Z-q2&oZ{fN#}{*5H9cGeKO*gAO&m~_Xm$CVFb z3?bKP;p#!!J(qEO4F6=aRqFLGH8DDxbX7jY|E7o(EN|i?FiGq`vOZi_SPm>K}HO54`ajHv3>9{r?#JXxTrg{~tDDexv{YF#o3(r1q=Uc{)@hvqGylKPC@x zB%JdSfW z5s%TA&G>(h%e(!94(KX(SJ{tk0mqZ{QRFWOcurpud(tU-)BVPpbn>LRw=$7;#TOl)rD!6e+3jj|R| zKA(1xx@v-76sjhgQkC7?cb8plA$io!Ty6IS$E6_elp(=)3_^y&i#P&4P+0l|`RC2h9T|Kda3~ydI;52vFfwb+wsNFe9XcIw z>b3?M!RS1{=P8Zo(YWZqvhKSqTP%4*Zx`*ncNuy9zv_uv;KNRt3sBbtPaB|s{`AQq zea^wZA+hKK9?d1SPCXY{)MWi$T0y&oNq|C{N{YEX@v_tPr%wWcN4=ltp|p2enrK28 zVo$%XzHS_AK?`Hd>8wH}l#?Y60~gxcgr>86N^zCurt)C8fhLBG*11=@$Z~C-JFSjpW*jRwAYT|bmwnh*e zRz-qnu*j7UZ{xd{Zr36f2q7`C8l}o>vu+~4*#24MVvV>-OX%SDZCJEXgY%I)aop&7 zF;9f-*ioE?>0}b*c;7>wc&=^GbPoK+%VH?|LCwYjNi%sH+`9fymWn0P)yOh5sA{H= z$5U5r;coLe>=ePiU}oyN4tXdk@E42TLuc!Rf#s}M8<7{_Km3EzLTn1Gs8Vb{@Z5QE z)6Th~0~c1D2G(c;NK1RXMtXg=vM!$pKZV5?W<(|_0MaRXfKPoOYuhrm$m}sk>2l_s zoyMp%#5yXF4sWxwKid;qvw9GRiG^W}AQ!Al+w=KBZ&A=;VktvSl>?V4_eERc>)-V=X20#HR#|2LVJ&X#nBtV5TaQJ}IDQ1#MB4ux(A1bjoaO6>bU@Fnxo`fS z`~RRsG4QTQrL+CBP%VRUa8vn69uOy-S*7ZYMg0xI_M|EKSt9L|gm){K*y+PxpsI>{ z2AS4kY^zUX!2YX%WLo3gjo)VfT`nQCmKSoewr@002B3+l*(7CRhZHV!**;p z#s5otN8)1Z3n)hWdHiqJyH0ao_+YI*F4BwQO?5~^X1zaZBW`E;Yy0z+>p0P3lv(rR zC3}`t3-S)*e>(1^dhas+7s2treL{1$7z;t+;RD+??u+wRX7`x9!_)FQrO%qV+yA+p za`L#@ft-2TYD|5}dQ(S;AM)$ouGi#Tzo)KlvqkHRHoOR6U!m&to%;6|yMfsJL?Yu~ zt&aoJQOfE=M^!eIwpW5kUb%4JjEmh?+e5B*^r)3Kqt{lzuw1wRV3_!C{BrzX^>^iE z{12*$UoN@{Tt3IBEZPM@8xC9|DS)eAD-j?0s9Azo%$ywCg9qnb>^Y} zhi!J;=&GXjZ*d;~$GE%?`*#xo>D)ym9^Pzo_5XW2K=aTychgDRoln?KQsUqnJi$K* zHm*Mw2gd_nq5h;Y9^(IMkGwni@j>UY&tX=ostuxXoR+39Oqao0eB|ew<9|mj0}e%d z-go*<1^-Qs|7|rn$E?jwLY1&Cp3>k>lMTBu#mcac{@-I`{agOOEFdup_ODu5{@*vv zVcm)CuP<)$TPYU=vTXPQ^+oyuLHQ9MF$3_h#T2yv*&LqbmVMaS{)uwk{_A|k|B{Qp z#4Rj75F)70&&mDm_+R;HwW308Z`>O3e>%9)J?A zys`bdsx-(#ZnQcm&6*3d?z7-KZyRHmC^^M-*FA~4_sP#a z$9GKzQqZgHVJ&c5HW_UIx;Z!F0G3N*AkEqlg9B^$+=i9FwDVV=FQY=F!lASHlyMJQ z?h=}DH;xqAvl!%-GQJGwuqu`9bz@O~0p#Atk)LK+LVdTpb9_F4_t(899McKct98nv zZQWFZcDA=mN~vT8(pU8`YFHSIeu>z=c|S3(QJTQr9kT z9jthc>TrmzV!QiL=t%LrI}rrs=OQoG(Ye?LDt+9GsNH2k((7;I0qpzU>b+yaIxY>A zqc7T6Wlk-j&H;ovA6^R424!y@pcT=i4v!l`eMp;%(&lYpn%RYvJJu%!SVc73#ZbQ1 z=!HA17rK00h>{1kpogtIx z*acbEpl^(SQr=V4jqAIfVdsjHd)&E@1D|_XdW?k}Sj-ZQhOU=hycn@fu!9*y*O3Q_ z3QIGU39vq61XWV(C?#zq@=+c#7HXe)L|BD^V!U?ZR`eOY+1)XSw=+3aj- zKo*dbsVq1+v&vE(vgVsYY*;(X)^M72^EsdO0W7hOp#49GezN4sUXdeT;^TFZHVBC0M%|PgK?<-e0~PmiY=8fk0H8=OWJY~&8)%8e6G5R1nt_z z2cZVn1qW8NJ@>C_pD16!f6RB);%TdGiTeO3{n2W#?l+Hzs|=#~UFa%tFE*?|&KejL z71ify?Vss@Q8+~ddN}Z@=-Pj?5G%WbK9!QW=p*Zd$}$EQf9jw?Waz{|Kh$oDB@&3w z@W;hgdJMIdn66V|KNEcmc_uXAz{Jg(FwM6~ikHxm>YLdrpLsw|h215V5L{D(AVR7< ztDU7P_##2Qx*qUnO?2|Mj`|5o1v-fO zy+F#!UZE@ON2D@1?bL=`jNFvX)j~3q?bc!q)dR)&i7sv7dUMzZOn0HBu+S+FcY*z= zc6*-PxwwM1t+r&X-U*?z--Z4=vfk3idE!RYt5Q=N(e$WLla~(TeBh*+V1AfI=^JfI z$3O5y7qrmcI8LGSTEFmz{;>U(|MqX&4?X_UU-)zO_x|om|Jcp%`8l(n`eN1*2-mdu z8gS2KI{9t3HN|Lq+P^Ml#@|{*kAdId%R+;k@;Ueng<|C;6O7gKDfNrOo?a;J|K&Ev z^cML#=o=EZrfeeW6U4=q`c=VQy3zbwz@M#7m9H@cBNt)^#2l$|JLsn~j`NN|OoQ|x zF>5q!aIMgW{o5EvkiT?b-gH3gpS~&Nr41kBf1N+_hO$V#oQWYt3ZlmUDxH}Q=p!Dv ze~kY(Z6}2}!CbR_{ExDxw7eHHg-iOA&$Wj4N#;qb@XE%X#!2q^ui z7|G=eZQf}puFbSBT4VE~;_dTDMu;gt?h>O4u9kn!Hm}dCjQBr9Ub>4-#d=jX`EL0# zEwztvECmq7L#6Y;PI6tl}DJeUJZ9o`h7a53~k-1=&SEGZIE={}y7O-#mg@ zAj@h0Z($gBiTt(H3(~6R?+>;jcM&#)rh+Xmm~cKj}N?7k2+ozIOlr>UK`tPX1Mr z_yvXQ|4s*Fgcs)h)fwVCik|b)%O_gd8T+KDd~Y*1ng1KI-PBRq5144h)3ASOgW}x> zpW|2O#VY=HF5*M~VXE1Y9B6X zOpF~WQ_3FdmjuX5>#bQ9Zif_KPT2aPYO|gLL^|{?fb=RKAh?{_$?@xijxz5FPNCFx z7A9qZx=3f;w_bJ)9+H#yysgd@f?7WeMOH&B-CzfTrVJY~=%=jW?1QSE=GgA9v$u17 ziT*msY|CrCuw4I1f0EL`!o@C zo~WXws1h^>EJ11Oyj8S2ihyG~&{9}bZ(SzMwV;GBqEovF5Hy%o`qdu&%zTsalLm`0 zYT-sp8^tqS%R2hv4e0FPk?uGD@w#43XihjD!`%{HelZmnenbZHf+^caI zqbq{;dD;I+V;yVW&d+rjI?R`TZpC$R2dHITyn}MtU|jqmegU|-IUu$jqM$o>3@SuO zACSp4t8EJ}LVla=B5#yFDK3-_R{H4mU}2P73%Uz;B3ewuK?&`6RqG{NB0u8-jph5U z%HxS^YgdnTTo+?N^<|f&QU3s3RiDssGnrEy+$_kKp2xdXyvw+3KPV>#*Im}F6-!Rh zbU@|BG}evvxqWqO`ETQ`CbWil3w|O564eeMn<%0Y}*MSi_$iVg-?H^_waEuPnPDAMNd_PV#%`r|oJG>CiqZ>*t^D zs5Rdjqx}o+V6a+D$ki_%bO_CUc^9iAf#@H9vi&`Gu{5C#^_a#SS;>%b<}|gt;@5e9 zrRsj6+>@rt_`|Qkhq5Ml-t<2DM4`0MZ&n{E8b@X(F>|QhTiu6_sW_(`W_mE$K6Pb^ z2j1g8k=0FN7~>ptGs>pTV3+iDc`t(YA@)Dx$w$cS(7*LA-uStH z<+s|8JAUIge%*fQPyM3(+CTZ#_xii<+xYkXoZC-)387-q;f9`!l}sv)$ZUo#?LKHV z#PaDYG7ajvCswg}cnM4~YyE$|eR%K6k z207TE)u^8Wn}#sHbWPf)^jBjovth%&%LI5K0;g@c7(k}=>XncdRCMWrna_)?=K{^|&~g*=X7F8h?v+NmsfjR9&qvu+Fcph}%)4cFx5G7HU6K@VC9LoZwNY8$i#+ zFh;wqvw-PlJ2z6vE0Qz#{Wq_7dMWkGuz@cANFrI;p<&7tFKm{?hXA{;__|hHhKghPIWOPe|R#VauyIZIk&*F+UO4us!7(cej%f zDVP4z;CKGOcemjf%vzU06Z~%fw6nCI4ZF=a?r7yluvnwCzsY!34VxhEM;aSDi&nhE z8AARYjrzo<8VP*o*%!U*rQrDjP>4T`cGA(QFRuQ!{fo6(0q5cP?o`H;wykPPt%X69 zLvgmF8YLQUF~iA?5W+7NS@e#GvIwB)kQO;nm>p;c9`yi&ox-C+laRg=cct0%#s}~` zJZIqYes<_})B>_u4?e5H{$(@+eM-DQa|*{(*wFyCD{zQGv^OXW+n#|zVZw!V{|q4e zF;glCjP_C)NPx9KH;T7T8}uXrC@8}Yo(d{z5~5T3)`E+Si~+a{kTBqnk_)o#;0c}P zRCLJ7S_bl#6VRzKC;H`hYmbnYuI?XBjj@qp5Mh(9g2ZVvXezblVtF;ux)jaSCkZZ zqmf*_i!YST)tTj>A3lS!1|{>;D9`oHbLO-96v~^=kZ-+B`4tO7?%?NFGCkksVV+1^ zinRY?*S!TBf;!ejVo_}lyrfQnZmVbd{c`nJJrJ^{LtCIM^qLL7tn@iF0Ddp1C)z;G zXxvVnEM1AtK*`i_NVn^wXy@j03aM)>@zce3?iD%gq4@yn^p$xr`MeEA<2-@LB@G4# zo`nHwqL?&Kxbj47zYNJu}I4W!(Q23G7!3pq5SOfFV1*_ftBe%CFs7g-RNi-C%zMnLEVik zbn93+7Gxw(Vk3$rc=Te8MeP_==~RY8t{p~VYKb*NK0Q15X2>nVj=BX#6ptZC6WnUC zrCE#Ds}#I3ZK3dCu;{UFljR(aIr-}ngH?TH6Z><`D+X8uxavUa>D7o1o|@RuK|N#q zqRe4=_}_?gurDq$n%3FuwD}g;yBOSwe-k}&@dv_3`_uqvNC6KkXyVj?krJ)SOZ=3A ztbPuZI0DlQ<7BpX`h^lLQg@E22SHv1VqpG@l=7L<7V;c}Rd0VY`->cST-(0_S9Abv zGfzy2NkwP1pM%N78BO2(8+^cviDn&*y#T4~1@@i;qU{^ekT2wvcQMIosyA-7sT!%? z5o*MMyzN{2&Wq}eYRn!)nR+|FhziLIS;^oYd{YN;Gr>Heki>*WPwUTSPvXINlu^X)_N>kznK z!*X3G`M!N%AuehrEU}G4+DmoK%ovw?+Pbk%qIh+%#zseEI|kZT#!j)Y9GH5mZj)XYijv!udeT<>LI;P;#Qr-(C)UA_ z6edMITUxibgNywG0~eWn{6Bi@s~D7fk|f5sOcs5mg-7+dwanG}K`t^p$QOQ3>D*%a zZ9yfmuhi2h%N8xhSS&796k=-?OIt>$--!QlH)cFw)BZ)*t?P*LtvrFoxxj(6HvwiF zz}BmQ^zi|cVs!tNSB5DE`pYP`p&UUi*xq94Q{ zk#I!waG*Q^8H$`W-;F6i-`7 zTnm_N<;cyc?ollEK?BnSat3k)Gz*loZOnQ{!&0BhF{}Q{0cl>Me`~NVKdWQBzi2>B zMze4m!PbzYiOy|C%5pWBmQu zCI*XEZw*joJb!Ye(%HtCLd8jBH1p(1XK#es5D@1hvu`81S$4b}fPX zYelNCu8dtq+w`nGtb#NXn(z9^ChRDTEwDJg;Y*5l8ORWY)rA_SD&!A=ndXJe~Y#ERbK^)xm(>f=fr_ z(QVouAtoC1$-|D2c9x|$r}Uv?^V+dt5m5I-JkR{zDOZ1DHff<2d+K*=D3-cD_*uR< z%3j=idYoBd1pR;AHaB+U$FG!zU+v5+5}tb3tCib=jC6s>r)AW+mTf&4zMMh6n{D`7 zw&HdUGDTL;&uwu`6LJC>sSdWFR&B8CT_hFwlwJ?*XBip+O?^D+daJtm7o$+i$C+Wo z;xfwTI{BxM$CB+;@tzI6HhwlB`6kgH_{p2iPH%Ql3J)S|c0zEi-&ei`XTsFijJBH& zGlqh{yzM?+7sV+vrIQjvKs?Z&vb(Uh*w2g3(ceJj(L4X}8xHx{-hN(u`Vl!y%EC=I z9mMvg^YlSc?oh=?k>$dTi=9a1auWe`Y7A|*&Q_YK(o}xXoM0lQE}b@cb1uweS43<0 z-hIuwIqWx7MOa^S%IHsU2k}3Ae=BS8Ox8 z_!iX7MgP|7aU`(B|GXjY=y=o%6ZKY77Nkup7O#G^NOU(H5Qbp7K6YuN4_5b@!G+M@ zb*ngzpsfUOF06JoQBqV+O%CxcRO#*EM}Kvi)KR;(;DFeB=?ZP}4P5t%JM~`G;qd!? zcf5gY&Bm?qn;&nD=y*0t=(~Y}Znibj(+Bht5aTJFt5Q}UA$m@ITCpnf-AwxnfAEjk zU-=t9Z1Kjg{o`M?U;5L(`0l{*9KYo!&3@-k8^>+DOTx_#a$|WL{>lAM_8%b+J)19q zU`>$3$S>7b`LY0{)vvb7Hlr;$?bw86+v{UYs7R{doOgmW+4g)OM5LebAusFl+R!v_ zg-k@4eONqF%2t`O&QP-ITwe%iZCjJq^tp4;7x~(PUBl;*o%8^wqx08do}w52Ddl}L zk77IG|K@V5XjiXY=__grv$;4`Cjxq3#)nOJ9{LgEml&-{*3d;KJk(b?YZgrvog4l^ z=)N}p8C9n%2QraDIbb4Zw6yAzi@u5;DJfmQSH9z}!P1`Sw>RG>Wl#Hmb#rTiD80}& zDpd0x>-ko$vg_m5%AY;Qr8~$rI-PbUpkJ|T^zp2%A8V?;OmEGLQVwp`?Y8=t-t?TG zl=0Gqun%?Swfbk}oz$!X=HhlWM)o1SzM>giUon9lj7^t z52ZfEpc4H=#^TF|x!QtRr^`5Zp!77+)ZER|M(u0U6-4?+_DVNu#a-}KVWF1eIt0|jR88OP(?7Hq82vcWIvGP$f zvS^+lLi5FePpt|zuwxGaVAzw&C#xt`9sa+qPAzgE7xv{s90DYx%dNZSJ4FJ3$YAa|A#RPpEEeW(YUN>sXF-}esvdWW`4x2aGY^L#$+ zfAL5KZ}yLpZ#*keiL{P-z;6a)G5N3(1-BMTz9!FZL+0sals~2o>%kehp_HtFtV<(` z+Yq^I-O16OB4ZiA_fA@p4Xt1ZSZp*xWzbCqB6K0e)P?OR1INj}DW{P|Hdh zt==H9o+s+R^99Aff8jnMZk_U%kWm7bR?s(%J$8hRA(Iw?gP?Y zKHW%S;E9`+jZCYk&X8%l;CeA%+ZP7Yh2dVPRLY6%mPJR`gWKdFjdHa`Z?iV(+|D!A zGr=gklQcJ(cDIgO9AA_}UFRKvp8xsjk)0V%91M9j&c>B&X$=|GqKliKN_i%iYsZby zT{pPH2JRtGE%b<}Nz?sPEqB#T>Y@dIlm+=DBB?)SL0MhP1Kprpq8PthdbLYIhpDJW z>szVvuS*{PkWp~Z1~za4So6}})c!-uk^zoh$DklmzHZhRhjiaq5UX?)EKsO_&epUO@qEz>u z?p@keI=m5eGwfPr4E-nmEmJ+Gpd;J6&q4A-FXX!KP-ML|M^R=A;oT^i3wS zycvrTsq7G=)vy3@DRq*Id5wNNti}J)7HzV>qlXK&mU3oZ4DeJ`5!xQq^;-D<#wQ`A zG5~KtkiR8t*5|MAT4fg2+J+(UGo7m)Gfd(c;zG1A&z-)b{HT^Q1`#_Bu5&kH0Tg*! zi?>qQ2h^BwH!%)qhv4K~P^dm?%F?SGD`9&}C=z>4WngV34$KJhc@+n(x;9`Q$PFpq;+%Y>(?4(`U{wc3MkR`*O1+?tf0B7D$k(n1} zo8%Ez=nGK#@Efe_v;Dv6q<;nTDjGe;b;peQZR0CuKmV`&e)|jm<$w2s7H|B@-}z&46wq6uAFsd(f)1wFCMZ`T0wM#$cBUlgknt?ZPVsJ`_vEW<+a(! zfrnwgp(MH1vFe_?0-*0#TtDSq*8GtTJiyL9rN4qVZ4x7OLG2Co5WGbOu36bGKMp6n z6WT1zE2i=e-^O}f90+aDDa(VeB{lGgX1xst4mR`8bag*k>}dwyk-j1(ZiR7BcVNFd z07LtBk5;8bxBo6%>2Jpv|1-h#0c)$wy^YM+`Zi)A!TDWs`tgO>#kDZ zSrRw9Rr}xMJUPe$nqNL)ZP(={ACpO4Q#!l-Pu=etCiTESWgw+>@)HZ7lbG&XJxn(( z%7rdGYo9YbnO5r6<#Rj-*OaB^b?<25BM#P>s_F$#A7t*c&f>>Rb0(KNBd_V zgC+Mg^-NShMh7X<)Fb7RCrur(`^7sl9kk|V|LB($#KuM!Vu>}eCH_39>(G_APk6Cj zwMX+V$xr@&^*0xu>ExJit&zS{jUt znvZj#8~c44|La_do&0|-zL9jJU9`?-D0yABDLl3KZgVlP|BzCoUYb|cjab>6-iLPS zG=#8bgN`z+9feoUIVi|6n!mt3)!Jw^^*+<6Xw^;~5O8h&Uv@guc)>?wl>cw{p~qhO zpmYE^m%gU$q<-{qv4I$Z`N*$!MO_)T?1Ca#7_TTBEO%qTGd_;`o4-Lc9ar%$t}tgJcU&&ZAbJsokt+E8MA=!%EMhw{Gj!_B92 z_T@5e@gLEg78XUkRa}L=Nt^&m-n?T13eKdV-BU0GPzc3^?J!jWX#fZ!+wgXnfz)51 z5NlV9_d}xV=h?dDUHDNw??~Svs1TraE#kO>0Q=;{5XzGV@58nUg;;!Iw^V)$Rt4yd zXUde|w!{=7(Z%bH0n)K@wW_ZjExgsKgXED#mtETqMgZE&x^ty5cvI=?BCo~2%Eo8AjIt#eR1ppbgK&N#C~W}tUv&An{&2)M9>kyz?5aKD1ovG#MR0UR z=d^4p2<~_eHFY|`ItnX|or082C2^8-Wp7*u*1~LvKC$dFX*vv;(Fv-Z(5e?B#Y+-< z*Km2JZo*=)j|t9dC)HcYzka!p0`%O0`lEgN`yxOo)4vZ zU00wf<(J{cDbPmFkZX}oSnJ5HI-&yM@-?o{nYuhcs@zo_Bg>9O$B+ikc%Et6y#>0h z;{)?!{2K2kR4&0eMITIbg$>hn=^{8WwFbH8AwpX5PK?LW!s1&_3j!Lx$#0a>F%ajX zTG5(_rfaxX+<(*SWKur*95xSjjWNXL`=m%M8v~%em1Wfyl>oj|{`l~xkR5QRlIBsH z6eWmUK||X*#uaQO%@LiO(z#b}-E<`gY`;zCCs^Lp zisVu7!;JP|U7yM~l@2h|9l%60^>1@vg#r?KPX<#;8q`HABYXLM-#EfK7K{{~X8Hm_ zNuqp4k9+4}rD|ADyGNNur3 zQ1*2l*03M6bn_%}Lvu?vLgY`Y&>XbuXYs~=_Ba0f?|=Ud`*FwL{OfvzfJlF8uYw!rR`+CahJ@u63=DZxbFS``F!pqv(stD@r}hT zbObRw7cH3STWQ2k#vzQ?h!<YlZD^93C zgfOQKnl6d|-Bk;dUO?eRaP(fZ80 ze0E*Oy}T|TFw;c!gTub%m!RK40SjoJUjpe~WUbt_zJW3#uFijf$^o zhbTTF*XKriA>VD-b>4IMj4lheHcD4CX}}?c9fmSJUH=DiWJIyU~!u_Fw592#~h)07$ZYm z?Y+-v)lTF$l}zkSm6X?v)e*N7|B9xf&9fg9ytbU9393Y`6?n@E?!4Hj?scp?12^5w z+W5#E>w-3nhRNgj36&@5<;H9h@V`b7$!agRn;T~CkU zB)Od-2d>lD9gMDo>(eORi9I`Hd@V^vey>yoY3q%l_$02C=>43^_rJI2_vqu!{ z?+-?eom?x&%p{`?4p&Y8-~1N`I~>SDtP+K^&afHKLZ5oOTA54NMCrC5F0ZLi);g?g z4_tx75DknL+_l|Y)W}T+hJCO!7!vEQ=cSh{wK*H6TZtoSYvly(D-M$p-7b8Wu(NSq z$jzshZ!J~nURu0akLj%I^Y_knzu#33DcuOc0&C8(1u%CdkYIjMxXkUUwrkYMt{c3! z{=5q`a#6-2<+)xTSPLxTxFbK0l9Q&`Z6iOKvVS^7cHKzP)OOW<>x;r6$l(@|MwUT@ z;XQ2y4&ci`WMSJ%3b{hG`m1`mp^sozzZv!ktIX7&c=8g0)6^^YSM5wiYoxvQbDiLW zodQgR!*9B*l64_xy82M57F`+|dLMF+LeS^uF3gUW&Vn(3SbwJ{?K~!r zX}5EsV{cl;HTXp2&%DDQK4o6xo9up2obGFT{-Sd%;wrD=^V3n$?0(vIJyTmZ!LF?D zt}XA075e-X3n1k;7jKNi=ehbji7QbDCk`uBk1-mi$izf|#xPk=wd^lG5K-{kv1rL% zh7E+zqy3oJp6Jh%Au>@zLioww^;&e-O_!mZX{Sacc>NoEnKV(N)QJ=of}~ICq7U9j z2RiN|h3qkPzn7jN zf<;84-;%C$)$3339`t?{uO&-qqgwkYQwHjgH>%Wy$K*>1>iK{E_x{=a2jR-9KygQ#s%gs#C>c^HPmIpEg-X=Q+%V=TKH?IIt38Q1Abj-h+-$I4|}1 z>4A5;wGF7LO>0^VT=(9S(Kg1*D$csj+-d*yHnDS(XX!ohGu+f=E;~&O(SDYePq_te z8QTyLbP0BUnjr3D3_OQ#>#Di8g|@jT?a+1Hq%OyvS8ln|m)H(y`lf6BZRQrW+SAVkVj`&`2QF)ViVgQVX) zw!FX=E#^8D>cwBQxV+_zsPP$VX{fWckXP;0FOd^4(7QdjTXWAs6XoC=|0Xo|@xR$d zpYI&Go%W`3y>i)GNBuoc`Hz5!-uM${MEdt_&=h@lwBsof@%LY z?LX&t)c)Hx{qhfVyD`T0lsg>Y(f^0Z?xzD+>~LuPGMCVHeK6(C1?fYKY}20J_Zc8Z zKM5a@<1We1Won0BGV!saoE1lBFE1;tF#wTq`#;Fo26BZ6bqx zIyk4o@jv)Y_t}R;J(GXrw{8<*>}LsIlP3QLh3<@RH9eo3(5^D+{{LkEG0PkFuL)$d z$6#aojwYEd;40Eia$!Q*7|QdV7xjm4+QZ5^c?A!W+K=e9HV)xYs zLRH%0TgIjO1l+HP)TZ8B3k=vOA@*bbp5YyZx8Fi2Vzdb~?U0XyjY@+WIVwI~nx0n$fJ@{K#J}uCg@^LHSy#|~H&5Uo(8ZBwnlR-!u1T~vmG@?8JNpbGtLbB0045?5B7*-ir z0#Y#eLa*6Fgz?CV;a&q*lL>_2yIF%FZhfVOi42o!1gWiKoH9qdH2PFdeb5A* zp6i+mA^H)>JM!V#Ctc}GP%FJNWwwn1EPnc=S<17D(*gK=;!27iY-H8rb}}H7V9@Ki zPR6LtFK6UcW@ACiRCiL&JUgx_cXc{%$~y#qCZ*yb4Dv@pbey{50jA)YZh2mDOU#Y{ z^QvI)0U>UEq1tsh@rjMKWK_3c8^h9KXS3kQ($tKDG;6#6sIsGakxyB=D@LS4?E_(3 zz#SZzs9V_?xZbxofF)QQV&$tsqF?>w$$BIHwy#MW7IoC8Qv#n!xvMRZuksfkq^;Fy zR$E8oZS2Y`tr=bYkW)5hQjQhRz^-~GX=7Zgtb=04SzcqpJLI0)wv+@&NFCq= zR)IZhb`!h}!C>q34SOiPwvCJWMX-KLXJ~dxK|}W~Rz3mvkwx))&k=3F;1b}us{YFq z9+&^iyw@f7yPa`eh8M2ZjvU^x`2c6tF1lC&+qb{h@Fl_<^rBigLhL;Z^TxV3S7U(F zf&okYUq#%+L)Ec!e3< zK6MmhBdUno=tbJMraS6og0>T$$S;7GAsU_r9>z{d-Ouu>RRoiSfRddFJxaDqS$o_u zrH~bh-t~zatIvQiJk+$SjB?p#Ll-zPTsClrs#|Z9F-Qbop!I4x8+jMKm+7LXN*6jQ zuKC7uxmjd1Sst)%me<;D$op^BJ-!Ua_#(C;(o)uM}pw*g~n{ zFUXE9{DJ@we=b|_7RSO5X}{aRU7)0_KW}=qt2G*8*uxsE3!#t7*ZX%Nw4S0BVJc-> zbrdcI(^QM4qWM{`t;xvAkK&67D)|5gVR+(16m^~A~Et^I$KQ4F!`Rc>B|HD3PnAFz1iH-6*S?a%)Qf5!gN zKl}&p^>^R9(x3m?^^2nqGvptf^q9=Z%-_D>%K<)M^o60S&UdN_HPGkb-|6M(XP9Kbo&RbuxYVq zD+_}R3d7d@4fFD@PaEwCia!g*e%Y37gAGsLC%V!Gi0x2;HP10WTSmRB>AAvpd=5aW zs3WwV)Uzi)7GR0SN(Nf$s(RW%>~JIMss3Me`nu@e5MuF)98!k0D|=XQ@}51lUCw2pWo? z@fruHuh_K>{v|HxC<9=ObnFzq9gP4|HbwwL(IgK7<5sjMEcl9 zsI?M8OC8v4M*pq$u1702{eLZ#ydV=D@1gyhb^jlYKlZ_jL1X-1k@h#n|GSSN!HQQ- zE*jr%XZx=)leB*~#>5=&2g%W25ciH1oBdy`vF&KC^pvdV=h^Q_nYqmL__@()K?8DU zeEncHcd19+D^oPA%Y2sS7Slc z5A0TcM~yPPB=+ePawO94_TTE7I^fd`>-zW!mFIIJUvgoDOD9&hsl#}J57P$+O(>El zT%jXP4&|bmV*r{^%+9MppB5G96g%JOM%im6_A<~CY*6Vd@om+y{1-{XyJN@g&Da58 zcYcBe1V$p$lmC62JnO@o;n6DSR68g0&*(<_310L{XQ+;I-PH=(C#7I?Wu3nV{3T{#VQvD4a(o3^UJ*;Z7jZB7Sp%>rRQ{T08k|neSvb z1WqRDW&h1JMjTpW>ITd3+4iy58BLF+Go%YU3Tw#1ZT()zvs!%iA+MJE^{y9z7q^>= zVDdMaxvU0rdRo=v<-Q4Ru#xCKL(39&xOhm4 zK79h?R96jtwEWTAtv!dI%6!1ivYk*!$RJ${vhula+URMc>QCHg9jw{vIbVCpAdu22 zIwz87zK8f4jjy^klfX3^9A83}!lBy4RoCqBA_#Z<=fwXj4UvmAtmC+QrDa>#c90`G z`i9r6lGywHYPIOXiNE~yOt-))B+atnEH7)l=ZY~q=snt}Yw*g~M$%zZ15fL$ zgl8?4d+C%T~KCqHymz^Bxiz6Mfx(tR+e_ZlPno{7d6&NO`au}?xs7M)dGq1Kd!q*R!b2@1ctJ{BDcRqsQxvD4!E*QHDEQSGhtP& z_qtumxwR~&18vmZkkq^XhpsS5@BhEnJ|@mohbl%G1DmZkfO4;ERMyMvm|=xUeIVxZ zp*lk>WfeH?b|X4Q;BL%PW5iWvTmN4yt=UP4omTn2rHkQX%l=cE=!4(-NwQJy`#j6| zcmC)f|FMfVe)P}(sr}NQ`bGQEkN(+v{gdA}(Z4ztZYx+P8kIuJ? z$)_$d;N)}l;j%5tRv}N}?Na-1ewnoJ1v9Hy>2lHgBu{tY=y9j^cjwp}dGH}?8!ulK8tV~`d#fv!xT|B3JEz+!|F(^1i>z0BwAPp_u+$GZ#cv=#%`7#P9lj&Lqdn~|BdPx@&8xapsDW>`%hJ^ z>#!dd|Et!tINV8@`n}fmZU^~l{z!E-YoQ%RISx1KJ8aI$TG8Z*E7!39H9sPKf&*hjOZq{2W8r;_SP~`wp(YG+&C!XVc z`^VV7&P7s4VQ9T~?$eYuDL32y5*FDXG#hTG>cUI3i03eui}GoAjK1(5`Q;{ds)Zly ze;hu%1L}}ccFb(~xnzT}X^4f#F;{=u?b6$j4$G{?9a8FW(Y-GqXWz1aGaVZtzD)a{ z9H6bL8W*47z0ZjF9}~x6(w@zW?f6iQj~MdZZ4fa%WU0@i^7b)tYo)H^{0WuoS-_!Epfk$54TpRHG=g`^CYocJ zZ#sEP=hC6etpVL?`_Fem9v$# z(xe=GV@}&_FRir-jnTT36}VVWo$2QakytKm~*kJZ6Mi1 zCfUAdo0Y{)9Z|DSqrUoR^zO`p`fr`Tx6LncG%n@4HOFkA)!hy)w)9&kWq79u9iNhhoYFp)4=79F?g(CY@UvAkI!|PqFeN2E*i> zW!aZvjM{>Aarjmb?ApZ6IADYAWy}6`;3cYrvV&Tv1t0mG%J+P9(K~!(rz>om&u-^Q zn_HoxNm=N3_0OG(ZyPZa@=sWF8^TW%=kfb)XE6SCVfYzu^{cejWg!eex zj+xdWX~JY;zQ=niPx=KZZ&adNra$fc^S}2G*td>f`^Ud(|IwfQGqrf*XTBe?`kg36`3b$BfE?n2ttNnMM!FK>zDBf?LQ9ozsb0>P38Hpz(^N1 zAmi3&IprMsxL&N9cpnY2Rz1_!X=8)5qN^4GMe4=0f0MCgtH_&nBkn=5q#nx)fKC1y z)1q$mb1po%N5SwhLnk!y6DP6xo5>0Fdp=wFmu+{9o~1eM|CR(Ui)Mr$AMyXRR^*@YzuCml z&U`>q831KwzVDVDx%AbVOXz#Qs2G97PRQJhwRhQE5 z3Zf%1I+~6Nf*CQIhNkw;78v7yCgU&`t;|k0>o8LsnQeBEljLvy-wyWQ$V2keeHoRS!HNQUntm>HQW&=;!)W$yw}KcNV>I3i{5c@3j6leVkr{MjF~ee{R%oLxov3jdoMpbZL3|1S%N5opmSFfn z;DEB5DD)QxP-Sz0pW8}V9EOR{sst1q(%^y;*a&FD3bot7GnH8bhWZkTbO!3E7_Yda zia8w*oZd?hoTjJ3m-32ow)(o#nn~w6Y-{Tx5BIL2wN?C7_=93fxd7~H_c56o(G<~i zo8lM?Rp$4ob!QLH1rjdp?*d?y0%dA--+5m0kn+<4qw&VJ1C~`VU#1g_ z&2p(<6Le>%NS)q1bnymd-3RmLgoQ)@1js;b(*n1mRfFL7`r1LII)ow}io&Z7D~He+ zv6`3@9_qKX#Oq8@-C?W7?I#F7!d#9dUQs1*E!(~809efJv{Vx}CEN8zykAj68@1l&fr@PMc z{*AOrd(if@t~<}`c@m(0A%rgu?9t|jR2Wy4Os&d8=WPjzxr#PCkWve7wkAIzjNn^{ zjyK&=>=Oj}F&!D!$j)E+f7fl0wLa~X4aqrhgC%p4x92TtssJL=WnB{J%s3$^-`?PL z1+vj`)};*GBUZ^X(cP-E81498%;&KWXm@>Dm;%2`-vE7zh82lH0bNmFvdZP|8#1WI zmV77IB80>w=N8;(dM+Rg8g6kyI_Ty-h_&qy|5J*T<)kRHJvn$(yo!&a%{`Ot7cq~8 zGzPt}TkOwDwNn1%x;ig~=Qpte`RiF&wM)YpYqCeZ?brGdDI?Qgk&s#_o>sewKIlTx z;xUE{?Kg2$g^2^^^~nO%pIr;XH+Czz8Qas74r3cO9-EEX!I#PBMo^Em^`-dcX0^D{ zIq2`L0@hnm&y>4MnizBnjeSU!8C9>E;6+8Dvby+aFKe_|pnhxRM>A}kQFFXGiP9Pi zn|cw2H6^O_8TFQ0P}Zn2Di)>MG@6{axA#-bG*(~9-E{EzEmQ2vwOywl%rw`v+sy%; z#9ku(2Yy!$)Ztl@wz)U!%&IFcmHLp(B^usfa|{+5?O(4~9Xc_^rd8jmxmrKA)$hVv zZ!->9QxcI|rH9x*o|Ru09^gkUJ|j=Fv6P>Upr(eG!V52Ta%98QroYJ#9+(*{{~b5E zI5t9iLG@j3Zc=6@Qcnz`^Tb80?DS`McX+Lduz>8mJe1z^pUx%3Lx^^TK zzuGNg9~*dnpK;)N5iaBPIn4AcjH`V^C+8X%N(!KN5S*azlibN%(}1e zo{E@twtv`hA2tTR!yf=h%}mFg)c$qy-i1)NJ}+>I{lE4cV*jw$#MYWu5U%0t%6@%U z>?RkLTZ$k|pLIOwM(yNzb#zS^0$S2Z;Vc_c`IU^akwWDmY32m|V5yy-;{P5p$INj! z=k?Z)HJQUQVwH~nk#><+4C{BZIsRuu(;JR9UdDVqU3cNM^C~oCo=V{X;o0R|8#Z z_8**7N7v0_xBpGQo1NNd1L{Z9jmAUN^JP*T+Zvw0eo*{xlu>B}bW@wnMM~u__V^z- z8XxSZD_xWfY~!&%tFiGzo?G}l6!jig(>UDx{`&f|g2gL$4Mm zKZ`k5Irucp%TuO(^W^ojCFkPL>-x*J=y=oQ{r8^5TThkYtK9Fd^~U%Yt;0~4SNh^b zc@q$0lSHM1GMNI6H)_No(a-n7V!x#G<{Dq7jK|U zQm+8t)B1-p+C*jWJDJy|?;4_`k?nZUr`G19li4TjdU}!@rCS(aoeu3dSSdGrQ*x8w zEj1wevls=gwy}L|#jM+__W_>@@B``amjl3?vxKP!^`(54F>yH}S*0JiKB-ZeZXAI7dCt^ZKNTE7tY=9nbUkx|+5dd|aN|&bdn+eUR<&g$}C;6?sthZ{%mT5mEr# zU8=5NHqUi{l0Lj+*TgLsyMb)p7zr_zFMow={KB@%A6fTJA{_EFBoH>*!x!>K=DTlo z_@d9R-3U&E>Y=rG0NMuIQ&}@T@#!9)C$dpqRaieRb@h4)($VqO&WG07Jw|UIeFPoq z_r-5_C9cy*Tq*3ZX!5IbgZ(OYaWQej!?46Ht8&-%U9!jLolqWaUgd1fAGtWBY5&F9 z@b6tdSw^S~$HE8k|8ys#-A!qd^NTjvCd{qJf5?X|I9-Blm?7g9p7`M($_CGud&4*k zwxd2^0qSs}4eV+8D3AT9b#l2*K=a&+|1KY&0E~OuUX{licwvDFlH9V(mj)v-bet26 z)(*A3kXrTX7QLUJdk85bf&a&3ISBz8o;AD9EE+kqL%aOVwkKTJhyf*O{ zd1L8r9{?-eHMVKR%}JczBS&*U&t*I?HXot1ddkA!ztw(0X%bV}{* zTCxIEUxL{l+G49d?PpxMcms7~Kuzm2m1+K?a492|$D(iiCv7Yz){hxePz8I;mpFXe zSszV4<=|!^pJG6|Tgs{5SlWb1q~%LIF~3U4x9~_GIL890Ti?(R(*@WM&8IA-(> zv$C6NW&c}7nXKQaio^ZR0sTpw@r)@mMj{@v|IfO#0JOQb(QSjm*w1U41eoK_8d4S( zTDCX7!VZv|&#)NLrk`&8e=C`YAr@|BNjOPA3^}^Qh237mPQ2P?o8W=*YOqFE8o`qd z+O?woOsx%WBMQTrzx#4dFjXlbag5fTf6L1_>DM&nZ?*w+rh`97v06;!LHVIJkk_NM zW`;(Y1F^yHEr1p>2?^G>O?sy6$M4nTNSmv&GBIMuqC4|(%rAGt@aZ<9V!PHKD|tkZ z<;>UkW9Ggti51;UqJtVoW}GJ|{iKyogZT&kO%ZS7cgQXJMCA18F6CLKuUo3TQu8*3 z=uTW1{fDjpBmF;(0<}LRU5L^tEW%j$v>s}oiG8cIbGfgy-}e}=YxM+qG4c1tI(f%7 z9EMKUm|)n3Bg9U$os9pt(n%lr%73UM@%qmA_VxcLt1!q*Ca=t*(}7)n@5!ge?AGPj zcsbmj$k2DD6`iO0SW{)OMb-WDbE0FF$qB38f1BC`k0qsRo)fnrBFSlAeulx?14pBD9P18>K_g|5{lH`MaFosI) zv!CKy`am}pTlZ_V8u6PHFki-C4RF1@jG>?Ny4XG^tT_1 zeOt;$$L5NZ%C&yYW)C1s9QLojw4K=0zwuaFs4?@mD`ex&tgbSl$$_bNH$9jL< z(lsZ-jv8o(T&K#Tt!#6|A^cGhxRXYrFIy!uRViAht_E9mYjC0Q1d0k5I*_~ugb=9Em>AfFaj;AWH&kaJsx z6m8qrmW7^^4OIC`n~A-oh^~Jj|16@5aSSl zuEQYKuqQUZ(f($^Qzx}<*v$-0X#c=+>aG?=EZ(0{*A?ZMFzVy-z3xg{ZQK3AR#p|6 zI$;RFt#P5=3`Xq32fLe z&2S(y%91A%9#%p7!U~hXeig2m+%ZN6>N*WaTL@x=xS{+_PJ|8Mh7NS#8gb~N(O(gZ4jp{H+`7(%Q@P9eHIL5^>j0i2tQfkx-(-iFZ=K9N1R!@Qr}9iyr^4rUFvi{ zbLaXhj&X25zzFnM?`Po=_VH*~qBdoNHdWEB4AwtOC^z*cQBJkyrR(1KI1%mQKP3O5 z*muiQby1ZD_HLstAJ5G64W#N{xfbZPVB9Rf#T`L1i`?{ebu)&4^)wI44E=egCGJ?+ zI{ENSuWyE(G}+`?{22cq6gTy%Y>0zcz*G8aE2Z;RYjg!22Iw1=wEq(?>0#HiBlfX9 zc&(4m*c8K+l@5p1PNVgir8Ii_(A8!of9I}$r&#}%fl+el#+X~^A*&*Rk#pMn_t*c; zfBNU_fB)b9zWv+(?jN_``1SwWe&v7s+xCzC;XimEKYI4te%kEkzH<{V-o)mks)?It zU87H=AQdOb^CQRC{l{05yfO_Li}0r2(k9UsKEfp5U2L6{&wTVzV)z$9OKM>LFC|hx82XR;5+!?IlZGvmLZt$!4V4m5wxP?B@B_cdd8_ zW2CDnFPw{zt@~$*lLk{7+%_b0rldWh)NcQp)%#i;`mkx=gAUmLDI7WBbQE%lPHuS= z>!SJDJ6Edt$^Ipsq3k$+(#(Z{vn-=1fi%@GF%0(R|EpgoOnPOo@;}gkMoi_Ut&tY> zaY)|B|EgXi4p@w_KA+=%g0ExDmQ{(*g#DXI{NLN4cCW?xEVHRlNItoT$;-PBGQfYj z`i;ey>B0)8?S1OGyHD|y^+eicJ0xC#A1c2k{NQ|guPGz70H6Nf$xA6u(IggwbOYsd z7YpCUa?b=aLHp;a5 zu8%`Mi*_w{N@L)#S9B|Y2uXRvtI!}(j^!?rFHgyneblNR-ZZS{Y#X+wRzI1_OaW_ z^8c6YfAd`!UO24Awn8Si8PV;s~4dOuu-(BtgGYaix(|7eKQwST-c82~1wAR97d{%tg1MfY0?;7FJvS72ndvWe>f1iqEE8DjC!Vu)qYgY%2qWx$kah zF)M2wgi%{qUpSNP!L50$8N7(Xu!Zruc6}mLoB$KxAc`k6cmoP&11tq>lYfv)F}sJN-JG?N#Ha+SXSGLDLBxNKE|s1GHel)kVIO-jH^~% zBn`0)=>|Ja?_#H5$Eya6qEA51!*`>#Hnb9Bawb9u5(gEWT<;~3&eAos*Ep(k5uB*9 z<^+Z}(0fBBovo081eL0Z3oVG_6ZynS6OicXx&qGibPQ5WoKtii^1d}xXT~@{^==!f zSOM`Y-V{Ts8|Hn|Dvase8R&%Qs*q{iMsPfT225IRvf8W@0#(nnRZ+3;%OD3gl_WZ)q6qw${%UW0N}2)w|YGo7nSe2LBn0; z%|$NoQza7<`!&j5Wgh7_tZ+;*AJ;j^_&mRj9a_*c{9ZC`^JsoNX)k-OcA>Gffip_k zbTJIN^*!BwIQU3AfjsdkL-hV?pkLbLG*|DgdL8wKE2%7MTl`eK8wS6;<>`E8Qk7No zLGVC&u>}W!;5-DQj5hQP@VtZJCh?!d)oOk|bQDHcu}EvHx|!^Dnn|cNx=!}a1F;7G z&gZBxwCpUmBz{MW{}-QQaHwofbugPjzI(N`S#%mL=)(-3?&wXij;kooirs8m=s>x# z%s$XY{V?M|v)GoY&lSq0&G|M9V{GJi@PU5(9O$Vt!9l@8Z~gkS|F3erNu<}|ly5o8 zkoE@+lIJb`j;3qamB3kW$=pBMGi0WIrQ3f=A&8Z;L@A_ILh=U$+0}Uy%N1 z2h`vBGiE<=k+GBhHpcVQyDM#@czt@xR0qBMzv^9l&14w3*PC3l7(e;A5B5))1m)pGM7;gBbf?$c zwawH1{piWQ&%M=46 z_R{{VpRQ>jM^x|nFj5wsk7$W^*&~hm|5^;Q9X|ZAmHf+(q)rntsOPiZ!UVbNDI4{d zNKr#yus!iN4S;vxv^{Q)CIKv7o%MhEOpaEgCF#vb;Se7);fVL+?&-L#xa#IAw8k;R z+Am7GrLW7rogMrgX=NWXHZ7U`s%7Ki_bl~C7lVXJJP>9K<`%yPaW>C8T|_f7YhOPi z?O5EF)H&tvDbzvG27#jQ5Cql)$Eh@GNCl9HWSF2Z)R7T+ZewRr1b|43(| zL*UY)+Zk}cZN`z-Oq+Sg#TvW(w*A+O2x5;r*TT0eAGH6P?|h3{WwrjlPqM{cHeAue z3tPdP>Ta}`By7UASz9RIGyqXRuD__{8A1c>zl|@rv7KrVSK=4bSi4i+>_2OT6({k0`^3KtA;bxZVATWq`t*}pdmV^<@4g8VM!)GqY|mpaZP>qw{fFG2bg{2t$K$)xP5+WxjlvF}(|g^2J1w`#H-=JKW%M12 zgyvh<>XmtD;HI|8}ln9)73^C6Dy^^*1dNUF0XGXrhFuJ7I~<= zt&Hwn$Pue<`j7Rwn77PaeDTQTD{Tk8n)lglG|qSO&dCDif}YQ`d`+UUV~4(JL>X9r zVN5_4;1#ma268~=_xwHuXH64I_Wk)Pz-wEsD95Ob|4adTsxIrHuH{x zdu;{f*6it)H|ZnjglkKDxlgC1X;E0Tfh!Ab`?MoB;&!;5F~uq^1Qv-Qm~7g=JBuY1Ush6mT!UI z<2kQ+&xb7$I~sNGU#u{n&M;z_e`b9p!=3Z z9HK;h;l@hb7aK4g^AJOy z{1EGChk1QTrZGT5*1BAA;PWa9aad#&Qbx!kFj5^|JkjI#TULug^&!{=S;Oc`iYR5L zx5WdQVGgi$hbt67Yoy#`7e1#w4ifr|UopZ9F+6uCFa5Rl1 z*cYjYjNH0hn><|9de=Sm4M;~_d=)|BD|X#iX5YX-+v-XC<;&H4)M^i1Rovxcp;!z= zBDE$qVY6M~KWraJp9e&+=B7A+(Q|1ZW7xmMGkVbcWw52xzQ_MrYW=kf4o%aE&qDhZ zE~(%1Nd?{vZ|&GlM~y@9A-}LOFroPn6Y6G{$Kk4>gO~q zH1(<%KeBx3|99yyUMn4_{)<|3$Ng?;M!g)zb-+EqhT3+;4$T*gd~|rtuN5Y3a>o6A zViPh8uE?}5{y6f~5t^EzZG{QE@)e1Dq>nMDn3RR||7U%_<7do%_N&?d_jMKVb3d`$ zVDlFwkxW5voOn*m_+bB7%o4Vvi?oA_{WH$D(yQM8Cr!&d30#a=|wR7T(30+dHaG=+a<6(kZW(H<2}2Dt(leGM5QCO$xf`&*z|Y%O-Z^ z)~Uo*QO)c@J?41NC!gxo=H7T&a=0$3w-yjF&8{n!;{w6OJf<>GY@R4&5Z#uK} zMavfw3RP88&BUls54ncTc~Fhd#GJC)CsmXci^f= zjET@5twP#=$s{YjC5_r+2%Xda^Po|Gb8(=X^dqK3tYW&Tb~?xZ^*M8l2M5cU=J8x| z@rGbWu?`Ge^EMM>y1$42>lj3aI)+T%)TF9?>zX*TuKBFR`M$^F6>~dn19sZ@l=}S; z|5ty=Ou8~sNAa`A|A004*3$zyead zNP;x3pAG1mA-YZ(?LkzRo$9l-whzGq+COYPB!4%zWw(rkBr3uR5)GDK!d{{x0uo?@N%BCLml>PYb zgkWzY;!OtXV5P$#%e%${HDAUzH}U3DE(a|ogN?{{=k#mV)%6XNt@Wtbq0%S|T{M0X zMMn&Z28s)SwS*_{;Zvwjif4h!y$tB<%ahm23cNk_=F0-Ed=tSg-1pr1V=2>mV{fGh| z2Jj2)Bjp=LJ&Rm_;0LEA2ONO0v(5oXSX|^pH`*I?(nzTv1OphE7noXIS4)q9Qz!XJ zS_psop3AjY+ zU^}F5?R+?G4!d{wVL+lyM$4yA)~8XDmcVCGa#6?k|8$WDb`H7o1EOKVTdO047pBv; z>yxuq@`9mGx=rk<@}X@KAAJGvTc3v22)@@OqYh$bbXFGz)j52^S{b$l(KWa&E@EdD zJn>BzEzhp2Y_u@n%BSE|2XMWx8(YY$j%>qJ!Qyt^27Yy# z&BVtHY{Mb}xa#iPNat$C$em5@cj8;O1x75Fj#(NdHVU&SgREd(Nvh2SG04{zx-8-o zSES>mSl?vOuQAaw?$39+WlxWHPB0&HxPKI({-KCSaWlMZ5ZpJRJy%RQ`yKUdvL~!a^Im>}@eDp?|fNF21Zzt@*|_G-?glaB$hX zVF;cfUiE4ZXeJr$<>RdMP}{7n(Or{(v;7cnLa))WfB3(F_b(&rANl~p$+sb0lC=nV zaRu!{9Cg#SU(~ovkiv&75kzr3b$l7EPA+@Nwf+PyQdE8GV3Nanr>ATk?a%7>tImta!#k?7*`9yq64}oc1g< z>I4~{pJUrnHTq#2Snb`0O>}nih58#+dMmHuVXr2!)-kcs|8Ftg!1Lpk=bO5+Q)

^mov2E1C9I-R=J*hfHGHH=w>H4X>%P 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 diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast/discord.png b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast/discord.png deleted file mode 100644 index 6b585b8583bc0320363bf2db366c3a489fa2065b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73661 zcmZ@5O1hRtx^wAXy1Cx} z_x_kV&oe(}o|!Y}yze^^s><^CxHP!Wo;}0=q#&dI?Ac4$4k4C*`P$5aOifA_$w$#m3?Q$9WXl|4I;WIX6(`MBcYza`Li&Gzf0@< zX9VKsf5$kq%Y>_38y!oh=8Kmc;L6+`iYFw^FaOWx3H*`eT~!w8kA`vt;`k~Z6}GW@Y z-86_d=8aGz^Gj?3CX~dea;lU$X{7LMI^i+rCOl?`AK4D<`cZ~a4 zizxNF#N-_4gM^Z4C6qVCgK=q0JoRpBZ&*y%;PFp0b&Py}fW~!_d7Ah8J;$Z%N6!uA z-*;xa=L$O0bA%!Bjz~Ww$cnpi@%37mLP4&tk}1!vabkIm7D2ugV(>YVQnKU7yIwL+EkaNZ-q9qUxkeyKj9Ri(kb_@9@}bp-Bsl+ zAD3|c%gdGybIAx3+r2aWp8KVQbTMTS$82#Akpns#;p-- zQLH(dK*QY=5^&%e>FaE3Lo;!pQfF>z)?7+dr0@q!dZrDf5C7SuHPfBa!a0m`{rPUX z`3pJbdOTWM73u&&6Qx{(6q0pa;~Bn3jWGLd*wf8p?fMNlxnH}0mdoBD+fw9}7oM65 zo6ohM9i=nM-`kh|?RKD&3>XN`6WY>f9_H(Bt1<+M8AWO<@Q>u~tvj>sZmRpd{bNw4 z^>>Vzbt&|kBOR4>04p9**f#M_knWA!@8(@aqe1@KE|j|hp->sxm!Hr0>`)IvM*P4U zdzt3O0OHIN-fsWms#SD1?^@Rxl-g1H#&Qu`*0+v)U!NbMrm$ zaPP?hp|zA=>HGWlH!{EOwnQfo4?^@6P&B&$xjdlkWp4NZI z2VnIo?iR!^se-}<)Lrtf3D0B%+I@b)tBy#98GUNME~)xi+^G7&ojTnnKPl~1@>bqM zkZiE2-!vROOc0$C3DX5!mL8#uUV#mN{=;;v*>{)B@e3V$!b7>^In~i^6TH>@3SVSr z-eoyKAEK>ixsSX0;+Q68tDqBmrHG>`uAS9}l^#lCSn30n*;6^5!+m}WUjwh_(c<|| zF5Cm4POtC|mxI9LATelz&q%1Q_~`7z3qCy3oR>;u^fO;OPsDeyCO3JxdAQDpQ zqwHnz%5K}Cz0k6+J=uI}b{7^Q<-HPN!UvwlrL3x)k_UZek5y6SpW^UmvML*D(3BgqP=7S$e ze;~@bdEX*!=00P_IuI4@Cu5)*%c$pi5?p)LE#vBj`>gOXPERMI(mk0D``Kmm>B0hiGGu(ovn zNEH$AK6M_(9VM3~$b+-+Gw^Bnve$X7KW2RJvra4U0^`hCbm9-$-xF6H`JTHHf9~9k zw=0S*=TRHvyHrY`lXjRc#lFq&+T$*d7_?ktA5$=doi3%=J{Ev zHJXWk$B2HM15ANIy2*|H=0>NtH)Xv%dr*$Q8j(OR1HCqe$5#+J@(RcZL{xLvvdniZ zN7fp>-ecYQ6-Q)tENBeybaP*HeSq~pYs zZGMpWw6C3szsYpdgC~=bXI+N|Ca`Gp%!{F+#cCCDDCedoz+3t0r;-cIMVo}6cjAs} z40FMhDfwj!xvR@hhd=Kh1iHtEljJ+-$ZR$+_rbA^AwDnYVxV6+wqnTQja}OeSDAb` znoJGv#hSR#G?#D|f3>?flJ(o~TVrpj&uEKyO+{iQ*E;;vPLFq~x=-8^0GdO(o-@S9 zUn~r~>{{Rl=iWZW+>xGRH%MoGZ=}m384-R-q3bF&dZWf>Wg%AbA_%?dG1dr$-Rtji zI_PYo{h1s@G7al|2KgKyp7NHVq{?J?31`_1@p`Suna|MAQRi_qlp%e@P3s@#Ziu4p5xnnA-RoDm+b<>Kh#1ZI$9> zHf_~)40`Dg+Tko`H2Y+&VcvhSHS1pAh|^&7R(1cOn_X>9yv82$BD{neEQgs|GXD@xkrl|4C{3Kjio;Mix8ekc0<*k8URxj7^iXLvL(Rs zc6u+rNTD2t7t{H@vcskg+;MbHE9j1leX^k_53E5RBZcj7?CW|T;dZ`*3lZG~xcH?m z4cH&1?|bZg_C%NVBv%#)V7f#xIKk^G7k?_sGSw)Ax$g#!I0(#( zPMyixOE3~Jrm4eU$Cl^tgI}uh#ay*WkPmJ0w)>8KSVwv}J>BbL){p@hjZ6$6UM?+4 zk}=W%IIlQe0)6-f<-V3c2h%U?Hx&m9vi7VZtIeFGYQRZ@#lUU-j2U2=tj9Ox$F-fC zeXoym%E`8Nz)k<@gE{N&aN-;HMCA<9Lgn<^n>XpSVO~9O@S~T$X}VQ%ri}@b`jgB^ z+(*kLY)Lksi@a!R2sL)T+pI$MN6R9Ri$B0)y z<8<+!S}eg?kqzZtuiN%5y8Tr^geNf}0l}N)A)&OeyN+7}G~asE#cGO6+kLB#&(IAO z_(&*21Yn(eZlwVjb!YQ>)0E%u#n@KbdZO#pUYR$lc>iTwEpi!h$?MqNOn1X)emPKl(ToCqM@c7 ziif}TS`m9Px0MLh)7^Umn%7S;t%SGY9(Cd=pg`huXIbpi@6}g}f*ERU9A6@<%QhI+ z^|riQk#1@r_!Sw~k}v(W^051hneq``LL>gzK2Lu_MC1FHb$9m+GW{x_MxB8oj+0?l zum}1iRMUgL(mYyX@XOnoe?Hiu8)Tq7Cez_sH)zexK&q7e$ z-g`mA8ApA@tqfZLG_~Ol1|s?=eHCD`Hr8({!bxtMB+(@ zAaUWl2k5<4>pOw%Cf!0No;Fi)cLJJy2!_RiQ}BTOw#$w2pCahB=j8UgEU(UIyOS+O zp9C*>q<8HrjOWOcK8zr4LtFn-SuIA)fKjKfqN4!Kd~?S&c0BkAX|q9yu^*^&_jHgoVBj!uEfVrj%SWrrAO`@ir-)gHbmkACd+m?3yFYrP{~Th z2#46c=sXzjZoy`^|MwN~pqwzPs({Wz_sVQi2#RRKL zAfuJdPU$vAU)|4eUv2>pGWTj`a-B>?p}>UY&Jg-J*x5U*>sOtIbO-ViZcy4`k}w5Mnmi_85*sSoaTdNK}U|_ zb!JliICPk}7lk$OY}Do?E&JY@Yy2hVix40&y;>mTL0ef?%;~#sTux;XdTV8wQm_-W zwl3iDD(yee7Hd|?QzSvz?4FkqwVf7L;&a@$SPl*19yY{YX-pPWiRGxJB4EC+f-SxA z%S|HLHflyB&S3Q#{Dn&2SvVqpj^IKfV%x%bR(m87YvhDhkcNWKZh<>)hSdL#%jt6P4d{nl4lVz#0r)p->spB;Wt(rz{>u`mj=*V8XOtv9hCo$m7M#isoXmUYY% zn~Au(p*drxx$~1{2?cLO?b(sLRSyhaF&V9*Id&zsToa%}xxqim?&AEJm)Z$oEzP5i zODyYxl>sG$V+~b!rM}^W1yxJVzQc#2D(f~@8*3yh#pj|+1MVl+)YA_D1H00g#LVwY z)(XCx$r@OqCghsCk3AAl{?n?DfddG6L{BqD1ce;V}^2!Be-u@?K~#y=<&6)%QcNR6OFXT zJEu?C+tQYQIOGo#Yi;YB6w{sU{6L1wihkQ!M5oc1k;BCnM(|(~-NJgU>`wvmh}u#q z8Kj!H(noz$ATMifZ@cpbqv4Hl!rpo{Za^<@34E}qD(bv; zT+EXoH3%6HG-@K;Fn!oeK_Am?T|Nujs^?z1VG zG0pWfL&EqT8kM)wcUFA>IgMmM*0xs=A+|qdTkX2s>qNue)MSoHP94~~xRriK-0Owc zKyYwS2Q@~Iih4INSh z2la4n8dx4PyGTaSiH4+wOd^R)Ss{V$oT&dn_$1p)M5)8#cZvM!a&5#}K`)&( z>Pm4ycN2cf;D9TiSYJ`mbgLyc*y*xI zDYcz$WR`F?S=K~FdRHAU$|Hl$N_jgUOWMhAMUx5yV|Cn4LUgQ6ctS>Prm(O&(>(w7 zR>nrZql9Gg80IwN^+j=Rrxcu;+(lk7iA3-0`@Fo14VozO?|nZ&ng}(}TvkxN7s5tp z`RAuXE_k>iW0jumN1G$Mnb;BIEO?6Cf^AcI$Dcwnr4Cs-!{Cj)riE_G8S$jgGqtBITOnJc8il&z9q2h1P6Am zTko~^i>Wv%ep8{Q%4qPR_t4(|isS5{USYnN7|e}h3nw}+*XE7vd#en#3yYB`V8hgy zVd?qqKGYlC>o;KS$Eb9Ixw0J*kAkWooW03#>1fzC6tzDt>d3yHOHLBpW4`5B>#EBw z?4z3Gpk5w-C^F0=y&9Wn$S69-axW#W92S)icJA>lal{Kr^hVGwXrt!DwOa?HtP@Ay zTwQS0%R$%e#T=P3Iewba@!}re=0reA`)w-zOt8G}t2DaU7k%7hF->F1d|yOFg;wxy z0S#heOeCR?)<_4(mBlYgoz?x=#;_(hlAYxY&;d5J7?f4#0@Y>Yt7nWv7dq$7&^2 zFGdrf$Ti9CtF5tdM|w&E1D=c#+t1h=EEc7zI*GkB6IB5qdV>svvw>Eo$gG;)T(Uar zws5bF3cf#iCY3W>j3c?zdP6+kSp0IY2(j=RRg4qi7*Zh zYh{G;!dsRRZrUbuO$=w<&5GVdsxQ1}4vo@6Y zBE)&ir?WaeH*-Af{J*h}RBRG*(do|QZ>n)Verd+d6{TG&rR9;;)S)qGW2eM%k=Y2R z=agA`ZfA|^7#eww&l!Ttqs}!(B^Ud#io5cq-kSb`rEF@ViVm-Ekl&f$UtWixDl?{U z-=uI90ET4gT94sI>;pS(YScBS<{^WTi*J0IVgoM{HWEj7&XaCq+)L{i7o9+faa|yV z%)aNUhdjnL@s>1qj&88D09e}X{FKT2aaBR2-jCc!r*1w7zaIZZ5+6)^WXk!EM!-zhcu9%3xNCVluPTibqBZ_b%serr&Ht3jn) za$DvW`!Mlp?iui;vTO=drqX|kYY#g#YK-JA$i295idnLC&Mp)C>n<0uC^hhKt&EaZEo+6Pr; zjHs}wQQB0WKa0c%&3tmi4ho#wBLh-u7d-F(ewR_9*vr`<7N5*25hWt*(S=Ad$@T=R z5Bv)q*2%T6#b-EJKE7h6DQ+zatmrA#qR{)AuGfKXJm;a4z5W8G?QnhbYXCB7`v7E3ZX`u@w`@ZAwCy&xLt zwhGr*mBx`C#4DyNHxXH#_=6?R+Nb0fiBed}m;M)n7+07`ZIMAW-x7dBP4@yZPg`-8Vgy*PP}aJS z)`)Q5nf`2tZWp#Cy@f0qWmoD4!qxsP(Zc=%9PVaN41`i?5ZHjO7F}=ZGjnw`Qn+=M zzf*uDhJM(Q3SW2CcKK+OT*;GHL=v#dQw^c8b%ubP@aVA2DS8K8xjB&!{&94rw+UQO zJyrYC`)vOZ!71u}>G?^_&$%v2PHUHXluk|K0daWF63X0b!IuPt7k?u+Ll&awv78M3 zH=fCFXU85-e0%;P|E^&L!tIhl;^^sD!Jw;6xg!Vi!mRFOZqkO$bIt18A!>sQ@4oy+ zY?CKa5G%N6k!fjS)v_>7?*UYgQf!3Ot#Apk>0dEt!OHg{$lkR6IIxcEj5sk_bmnQq z!~M1grg?4QM5Wu8GRNN55fqb9sV-r2AWVqp3-`*l)_;rVMd+*@r=U%j&nulDXzwJE z1SH4QUTvs!{%A1>BVCKJbhFXDk@uSFH+0LqgJx|Y_f=t&c@7nW2=XVUtTkR_e7epk zs`m7g7%jK#liV$GNIFl z$mENrH09=*Dq>!kO?yJ=zjX1^E=iJaW!i!>hekPH%Kl?J%)aP1V9Yum6wLG4lhW{IB2+g9LIEJ zPe-M2l~MOanK=^dJ~@W${Vuh)&+fJ_iqNw2@(;bXp<>hh0x^5;zu0`7MJ@vtC9W|D zjEaLg@Gop|_sl2rhhBq@V8b132^s{E*HjFv93_#;!v5hLMq=;D9&+2URQ?6#3zt5O zt*Tk(e?}}>r*v$j*N_+NM)D$OwskE^^IOi+ZH%2^*8g6{bdZZaDF6-%Y+e=bE5fhTXA(i2kHvim!5nc3aBMFP2&x`m6hgA4iCL5KD z=Y!QJmAn!2Qp}_FvVpZ7w~Yk(c{#TYY3g!zR*=t;&_VZ;YMHIj$H!C4zeEk<-7Gn@ zxJ2?|iY|dOpFIyUe;qXOb>rBQ8N#pishi$yrT~e7&2S5dI|tMt=O-%GZ?)> zw8Y17rxPC#w@TmC%Z}sRvVX+i7dj}prUoiIns<+O`%Ru--qm*kFJsJk#zr|95eUqU zJa^%=y-sCmWtP0_@>V<@5v|IbIH`Zp12llsV1!D4%^hdfO^&xdQjq~VsLZ|g@KFO# zE4efn4-v9zMAPo0itwi>1_^_Iz2eEN;ezV_wzcCKV8tfF`o6QV@91tk*j;$4OQ?)n z^*~a2U$Y!>A}wkHZh#3$UGm{lCdt>WD9nVnj$O-uzReUB?wx1lFzx*!QPO%!TnV|e zI@!Y~Uo=^f-+kAs660xo|NUKBg%^9y?<-nsllJEX=cw7yZI3EW<=PYq5E@FC^52k< z@PCm&#+@}OpBS+hmuHNN=Cr{`v=FNN+F(~CRC>~rj^%Tgue-gh55+?4b;Ox-`fT00 zyBMV7OM-PzCSCNE0G^#mVAo;(>9L-?%)@z|<_c4r)EtpR$hIhK*llg9wMZwN*5=IRtLpN3VK z(+C%DG`>~3nHbJe79^tJsGx^!kO6j*24B#5H~;uh=uANA4r_h!#%HPS8Fm1<=pfDv z^+W8QT$=ZbC;`+bvp;fb$tRsnbCPW0B)`?DHU2C?$Xi`d`PY>KdiAFgBlpz8ZO1O_ z_F2{24A0@|#wgv>2EG+DK=<57tyWN-A(c`iuNr%_p7!86mDx zH@^Cbyk!!C5@4LN)USUe06KF;{!7s{(4Aor`lFQr?ju%|Dh}Tl-?&kD6j2(}Lbr^! z86d`A!-fxwp1s}gTn=V)WE6GMOKcHk5eklP^#rWneq(8YF&yUL@L#FKkUwhk<#~}E zv)yz^u!eA3PNQ9^ka^cZy~(7@LfO#2U7!m~U1FGm4rhY--tpm>kG zI{ggoQT#k;<Y#)h)_R zlU&4=kpPvL;zCib?OWT)lR3c3Ut~nTUXg~~=%XzacdXk+Mez~jhm{g`@TQmDcIIgB8c!LJ$QW3KJO_-!{D*qSHX z&C*k1!*Vbuz9xp&7cz!H)Ut`uZfQg#G--Uexcy}zUUZ!Xh zkDB=Q&CoeXJ#Ig;hO`UBB&fVyEK6XrAYLOuQ=Vn54=X5Z`g)Z|(``q*o5l6#N#3Ntg( z33&6%c}rA}i#i}*K4pYi{Oe&(naI;A)_-c>TPErE)e^ro^`gz@-;^_^XQx?qhX5=% zb&6}W>{IXHSCxnq;8`Uiga9;e3`fZzOkd zd(&^ka&SY64YyZc@)D_&(c6Tj7nl*c}g3FL^J<^AvG3Y}kGF6nfi zUZ>hK#In>45~1PiU$pA1#oPm%k>}7Gd9iNT)?q2{`t9tcaJk+}jz8xdz#(iDGTlJ8 zKH3z6%zS`hmg$Iuulac_o-k3RjRurvVZreSXs`uOtg!u)#v|e${xj;3;GmB%j|;&j z2FjrF%+nV*H`g8;9>BVfj5dRB@Ihd#kN*-q^I!zIvO#P2woxoVVoD|NQ@Xvn z7tf0KuWhOAaI}Vi`Uu0B>DNpf`G;7m@>AEQJ@~lk=d+GIoI=CA04guKL_^b62ISv0 zTf)AL$b51URi99RRcKhp39)TKz9PjSOo8J_EE53CIFNITv?@HuK*eUkf=QGTK{BA? zTb(c{8RtJ}9uP?pUZ1%iOiMS}J}fhL<*e^ytm)Lng43)YhCzFsddEa~%}6}Jy+Bfs zJ@EpA`e;QAEYr%6WI3O79fGVgXKFAI$iM}E?&4Lyk2U{yFVq+pO-HmC z9bqg8hXN<^zS5%d&wrqDVEYP>OorKZ6sVxK#3NKzChE%^RSV+kgX=N&K&Cwp{5c^x znd$u)%cdLiQzRjvu6+^MViXh>VE)35o6z*!D?0?P-qtD3!((BxBJt67~8!wl(bH+jFrxBltRXYsJ3MME%rw z<}I{o)sGtqr5KC%9`p|M#va9Gd6|dLPE3y-ioFa)GOgQhX=oW2D=~)*F%ic?dU?1r ztNg)Ntn~>rXq-VlY!M#r`UuovZ4Y`gMyQb+M1j?u3Yz#t4V?xIxe^cK6A9T(T+k$K zu3{xgGj00yRFimHqc=!%_j#sLo zCdz!d6;&3!{q;qkI>>_dNT-X*WA;;|v6Nb7u*DOl zAF!7roHVuV2gDvu&zDm;@u_EK4YTSPxg|#3b1h&{+QBh$rVz2aj9F+ zk+CL_u|0cVkiP!xQ+kWG6p;B$fwHz`=Zi0WLEARch(#ZG!$Z`{rZ5eQd`vj6O4hA|25(4wj(pZV(o&TJs{R3H(M2p*UaUy z`~?j$RQ^R!WON9v@HHew95}8pmW-d9gE8?l6tJ%~qo!~DSqnJnh$JNso>&RUeWCW_ zfxf^V`9qfrx%-)tF11ygQ-GP3n%*Li2@^5j;d5UWU1~#{7$~ZXmC#(|dZe;F8XOnR zarxVA^ifq%x0Nm+v0$&KD#lBuKdinq?OkCZ-%(njBBg@mFZPU!CfyYVrAaL1q8;?A z#g=~UuyE(PF}|oXtfnj>Q~+;1^u6h5V)Dy9CqxYj-_6#5*zczrIB0Xu!*YUv zOmdFjJ^TjiU7fm27LyRe$W9gqm73!_=r4YhN@8p3!>*TDyd@rI1A2EfGlR3iwDK+k z(T7Z%Qz>}5zEGLKpE3TC#AV3ot}>~U;K$i@9C1cG7K83J(PGQQSO{p=cIoh9T_gju z!-TDh4_;0L-UZzwC5Y%7M5W8;SBGV^+SU70uR>VlIoaU|N!{Gd`XR<3FoX{1cc zMM6Ib8SgZ5C?|B#)L6#pZQtp4G)_U{>-fE?&QfU*DAFV4F4W@g?&9%1K}%|MLA5LC zhv3NX5fRFNNJ(*!FT35a-o4<^(~gFf2Ll?SRTAALL&9r|`1^2$xRDpv)YNJZ+9N?` z!ud`OhP^sV+=*+Cf7W|ivx3&EprIuMz0g&8?vWC(8Vv_uZ^Q!hjh3Y##UlMQIW_9t zgDNVItajZNBjXtl;;J^&3|k+~JKizq`^QQ^zxJFQttthNj6T=N-Oo1(n?Yt}=(85S zmWm;*SYE4}YTyZI2$Bj3w=W*;5sig#S4pU&2Ca>Ntr#(z>Aj2E=3f(9&+e`GXS3h$ z>d4Y!Z_2m=_A+vCW(?QekFoc_c`@+;kL9PP-US>wI5{W8T`e6r022p z&dO&b6iK*(8I8iNbenoTbbHfGd-VGGn;HWQ9D2>TQ|kE0Qbg)~`Dkr0@NJGlO}|0b6ak{%ht$EC+ONoh7yei4vBz zd|p`hY(o5yO}uS6u0MP-3M6KbRi=KAb^p3a50+6!h;?G{Fa{Xe@cQ7u^^DoU&xNqrfry`nirX#q2rlf6v=$x_QxaOlp8WYCxOukR^dy0?EVRCQ(iHoxbXe)!Z z9~lDJ{NV(2VzQ(O#s#wdCR#g?^&BU;+(17Qk;BVI1pRTi&VH{!ztFPI2nEL?Jx$&u zN|zTbN7Rpf3cOMpYdlXS^!TVaqOlZoLuR;tPO+`l`#!ixX^PMeKA}Zjfb$gFTTj3e znp%QkkBzY~9L`Pt$n32qShhcOk$4lea4AS^OmA86;WEM^ej>HbmIzaU7e21%+lHc} zUX-M&?(M4``SzzUVgZ<>9lx+c_xqnF}* zgKKL*uekMGb-sAR&1@FIrf0WS7DlGr`n6fh5UJ7uU>RkLRZGk1TZY7=5}Iy=v=S96 zI54&JNzj0c&Q`zOs^emV$zmFSbd{^T8WN?a-ic2Uwd@xF>kw4V24@ccX{gjW8HI%* zTE&g~G@DGx`%zt#Cm`<&<1dDUojj+D6$5!gAQ_jthxS^@fj?=J(-Y(hqS~pqHu#_; z`epbvFGI(3(_Xz{rW;9W+X;i%?dHAb>Ggw$C}BAi9h!O2L4dw&M%9?Nl`3VkiuL9@ zvHgDUmwxxhmg-0M8AGA+s$QBKqfy-0%l0dSsNmg1#!k99%{Ul0Qi=fNS|;lxI)OCV^v!=;oBpn3qUPG2 zp*N-b*X;v=4#0)m3#?Gz$$&6Oi&Pt(Wm}jER{~>X8kn9V-z~>k5>@c7I)lYU85SJ) zpnjis+kNdUaTgp9$Km+k(LMQl{Ef#6JNLy~;(ys(?_JbC{%P@~UykgP_QO^)-qwMr zu6T3Pu=9B$oq}y1bwyyiv$_hBSjA1I(r75;ig(toDGr}^%fdj>hxUeVK3BdUT!5lR z3fjP}v)vHYEOz^JDxX!@k|-yIFVqXbn)8a#Z9R9$0_7(^1|BbZ2gCC@Dh9np4n0kTa{T}!?Uw`&kWA@pBhNU zVlSqFLMeOWN4`htb#o5L^NuU}L~YQO9j4A^S61xiv+7@*Zy4UsQmTVGE%w|#rrWRm z6DA!p2g7>YoxRWH8Ruk-)K$a{pH3%~{mEth*g{!wJ}ZJIrR(D#kZu*ULDpFW2mA{p zvZ#3F-c~-pDgrG8#$C5CK^)h{BO@Yf={-7TFLMuL%The1DK zN`YJEijKt?eX(F-OowQr#LPkoxBD63vY(9Sh!Y%(foN{8hQ#WIo$j-_H|K4H9e)@e zZARUs8&`7f^B6OK^^-Bu=)ia2rord|=EG(^NPXi+4e7L}H7FbuW zltJpz{tjMS^3WqNMTyA{h*8Q*QGEx%Cf7r<3U9ds*3Q>Qsl?jW-iGb=om(22?y*7hcEh{9&$WgIJ4isTPr3Y8!xJ|TYUJ%w-f%amk$QGfyU-=?r$x@B{3AmJMGOq1YCd7RsKK1@HT zK9hfO0ix(j1E?NK)DPY=4d_Pjw#Y=0k8&yt-5)(zM-{Uaj_s=`D|Ki)52gKPjR7w0 zwN|X3?7{;W{MPxOe)Yy=&Nj3*=RkkFANRXjT+THnHlCF7*Dl?{(|w}K0?6Ey)ld#( zn%*=1RJFF7;fY~km^I=!QPC8OW>Wq&_KkJj+YvWY zkfI<<4(Xsf>71qu>26k$;S|M{;#BDQdo&lS_h#3A?MAWhJ0j!k;(7a$@5&X-R zo#~lrIjsk6;WtisDK{&Ftk`&&>^a$zboc$C>z3U?7%vp|DX8U}FFhuy%m78rjhVBa zp60wnD8HW7mlMx-cPv%pF?MF?587M#lX=g*gW4~)W$rteRSLei;Wuf~w_y|X? z(_KxYt7SeR;HmCBDK}Or4CwtCbxZ$%WU}s$XSP1y8Dp!)UnFeaY7w54#ymf3mLTx= zF=DnY^SsF@gPc1|$iN*g)H|I#4U_9#ygBiz|PO5ZE z?HR5qhrqi0kTBip09Su&FJ(71?6C zH>s};3I^_@!d^k-1v@b9z52IY#0HF9|F}bG$KXh|y|8$;1b48j*a1TSm;Q70nTnZE ze_I)#ta0M#geHTuxbSHmQwvc1WqTbcpm6+Gm%GvRl71yoS#~njrk3yNEt$P-m-WBb^&>*xCN^k^jKxI7xwjsL*`R_iz+LUb%{Oz z^SG(^$oKY%ipl9X?$3Bhg zq)g`>Dtxw?ln6u9Ve$s&+sQN^v&+7P^(A`6>oK2>v6y~?d}#WLh+0JZ>>=OIDZdS!Qcy` z0lrw-C7B-=|CQ#4*xLdB2o!3Ch9jz-?Y1lNCfYB!FJ*TlcYfXVa|q?E7$hM>-P>@@ zW`EC1Y62DmjxyIV>A4G3^04QY(OoYtuM{8Ka7E1wnf2KXLx|9A6M)f34=c+?r8>Vz zqeWmIBF7%UIu|*wb#2zRcdOAU-%q!YMdGhpw=~@vNu#3`Fv&Hsyb$FU@qB^g<#KMH zj(<^A_ahti#i@fM(RNWAvjp6#>oRuxOPrC8e`RoZ*w5{%%oO;7)>VGlZyyw;^h*!% zpE+`4eK+kUg(AOpE%dUBi--uz%vOyiQ}fr%yKuT8g8?;veHP-fYIs$LhDX%EoTe}qc)<3r-z z%u2WT_UTb(O8; zftJan-6j9aT^vnK<4Fe`$~^AVrWFlTzkAI+|5KHLPlNq^N;35hp|pZFw3jMeG7pLzIsywpr@eeGzVD187ww@Id!DpHVmX)VUso@}vUb!Fw} zQS)W8b*g>cZgx!OA%9h&|IvO-2(6{xRkVQhc%FdU&flD6E!dIe5$S?ONc_Q>yMRYg z7ZLX8#KTg_r4*V2HB72;fQX0YPuvLIMx=aeMzwk8!U*inHJs>o!>{Ep#rB|NJ0|*Q zVTNImB`KvH@1gr>>?_(kAf^c3qr`x!;~gOD!axHXpz9uK z&raKJ8A>}{ZbfeBDvnF#m>AoPy2ARx9UD_k9sTd$-zCk;nTD9<59aowe5TcEYnebWhcR_g?qRJqsas9kzXD$B}; z!k_csnq-g_UFP5<<2s!D9&$9dx=1p`R4XYp=Q)v-&3Xv{eAwdM@^$=?{zsdGz>C#@ zi9Vti$--h&#%My{E8(1}D9qQ2Ye~M??kC&~*ic-koEWjmeIKptYuw;y1M{X6)i3!o z8o1ASZ%$j+(?M4D`s~z}t@Uf;$WN`^630dGQ~7zux(J+I$`zXJUI&R4*S3c+~&BJ*cr&0?%Rg2rYbBavaG88}^ak+^#~RqpSw%{=Qik!5pu<(8_FR z8a6SA8_lUB2R5~K``UCmv`zJ969X#YaT!Pn!B0`=Ay*-HyMMXqW!P!f9DBs_Ta$xn zBa`-`?Tb99u3iO9Q;&7r=v{fyOnh}-zlmwmwhXPI@qF3+PXo}z_AAMXde;CMwL3He z5z2kq^wyGShCjZ>eyMw98W_AA*nazUy8;h_Z7Q0ib=!A$^vJayO4wOBiaVJibK^wE zqY9Yw=R$Y^oK*!Py&}$d(iaX-2k5$4f-}U0HDz)2j%xS5(hzBXEz4^_g%gIT_bI*V zgi!uOo{1`t1Ttghk88HII2^s^)9LnN1-)3(?8xh|z* zGS>katS1Bbk9q0!+rCOgh9y-JdGF7FvE|87!1o2JYJCgPKlr|FocA-v7SQw5=@_s3 z3+CSUM}quk@E3ybykfoZSwnBfN$W}@RdUd)&Jk`!LBA4K=bm}b z9kT%}W|mYc2mn7l=wzIrx3wz;h8}awZQlwnE2t!LNEfe%bjIblc^Zgg6kmSsYC7ed zVZPCtiIgXgb-y9wHv2B!U-M#mMlIm)*1}q9*!*uelgbD{Shw?Z=v=$5EaHIIUO2|t zY4PMuV5j1)X8}35OtUT)Ln=jQQ_XEV1j!R`Th}og_Dc0J{o9zv+&iPFrC5 z<|I=d`z08f;&h2@^^O|h<*Tz^`XoCs!pR^zG78DfGDn= zmjtVf$D0U5_?kk6F*laii0*^N;T!r{t|b1DO;SwQuSoBf3Raskr>lKdhQM7cR?U&? z@HR7&Jf&S4Bs|^Ru4XDS9XtP|O8I~zq%#T=7p->l6Kj#+N@uDoIOJw-?eg&itD%o} z8GC~?t;p;_S4fmP4cl1@)Z?cO{Q=JdFeYVAw#665zp}Zk|MP=a(<^eC*daW(u;z_g zFXmmSfKG4c(c1q2>Od90mqbtxF^;Uul#z79icbKXX3Qti6A;uho?ku5@;>r=;oc7( zL+lUIv@{lMsF04zz+#;^W=JbGx{97W21qxjQH6G?EoFtTv3^dA+Ar}ejxUzse(^XU zsKA;k+YVmQQMwR;+g3Pmexw=Gi*-2HUDBVeq&K2$!h3agl*=ekZ7E(cP{9OjL$w+9 z8_qi&`fq8?v74n5EMKyq6Wlm9J$&b-X~!LR?0#;U@v!5?2yR3?Jfr<1&@b?N+&>-*w7uRQ`>}ql9tNm< zie}iR!h!EaY2C3lR9n@t$G);mGhdW~84=&8!)eU4_G>>gZ!sudWE!zA5z#IRqM%KQ z@bt6SQSt{P`S9P$_OMQj^lv1iOcQJe+hAPOHIIwJk@GwKQN9=_#I?@;ASgeRS5@$s zaNi=Hh~yuyan59R!RDceZ=^3HS?6(AIANbSFSnrrDr$@(K4sqHfC;wGzNPACTiA-~ z{pvXN?7_i|Q2HGy5S;Y+AbMFO1`3!nFb}K)J5t_0Uvr09Y4BjHsK2NC!S|FB_Iup6 zS}v*Z#0i=U7Y;g3J~%kT_#r5UkLbsSH2?MK_d=rpiK;hJlKz{c;>`}C6^udiEw%TK+|m(_vqtzlhC zu9&{;9H^iMzKX*4kq@|i=B+5=%>DNM}uF3#0 zUAaz011zujl*8x{m+7PW9|7_)29{LYn5ODQouzOq z;Th>Dyyw5|wKES$az4d#+B z6Xr9PL8bxHN~Ni5q%DKf8tH@g5LEw5Ji&n?EYHCr92XuVHLjRevYXsbtQY&ObRL7( zTY3e8bsX|E;;3-oJUx8g*X)ZaQP2o|ohZP8cEa0e3EReF!09Xts=U`7?M0?|mUTSO zKN%-QQ-uePzw#4HvcGzKMHmCwe!!}wC!j++CEn1jPTm~Yusg7U zLEgfk27WtS1wK$F6fd!CD*TyO)VQErD}(OWD4%#=mBD;G9&Vse7jd8i`ycfSX^Z`E zJ*J*|AYK@_L!DTXZKP)(1*Q>Cq%Ws2?bUCtFG^Rq^0-BEf_jJZDt^L#M>a?Ou9InR zM~$tz#yxc+{ek(TiGHLPv3w|R7F`bbRjT-dQ0I9TyI3;3Y8IJOyF#F`d&&*_Oz`b0= z6s10}U@@IA@Zx}zkP?${x?)1c#UOf8fRj!vTQUI17b(TY69t&>`%_9hR9#$7;iCG^ z^zreP%CWzy6cjEkVca}#WSAL;=pM_bQiKjAXDA0aR%Z2y3W91!-N!t9Pqy1?$<-Nr z;Qf-ph>S8uxe*t?#(v?Lpq%?XRX@@P`Nhll!GQ-HN7f~30-hd(K?OH9-E`A5eE9I5{y1F*6>`ZenWr8i3U({WKY3T+bi1?%L86KrRr@lepjj$nF(lR7Yu zq67X`Ws$ykkJI^#H1vK%^E;*m))Dcoj{*83>`y2hQVornt|~tt>VaEb-PlLRo9Bbf zQ_NqHj4RvAJjTE4(y?9Dj|i~7QqY-sl<$}Np&%MNc|DNO%})W2RaPNlrV37!JzPIWdn3AFdRO74c9D*1;uk@rWSDL^{%(Y$NnG@s`H@bJ1z}oIaY15d~=^UpP!?^23}(TP4Tgx*|BOR9keL%JIjS~@|E-B>1f2fK5W;c z?U>GZpf2H+9Oxt}9#BzymPxM%!P~?>D&A)Oi}j<<p!7H67v*QW z#DHZnSj6opz1zgOh-6*qtw>gTWH;6829{6NHp`sS=~8!4JN%mup5%CX&%}RafI~eo zR@clA5k5*cAx|rt$9ihhm%Y0a+<1K|!GhTjr{A9brb=*w>ELZ2_uh}Zpx{Pn%oYFE z9V#G;x(xGhV1r-V-~b1M;K@WSrQtot-vIa7h=Ms>uaX(IbNFsQ zKQM~qFRD8_BD$z+rDs%Lrf-SH6~1csSL~dkA<~lT#yX?1k0!`WSFXeJ;X_UCPo#sD zEl|I)KE{RbGwtv<(rM)A^(*%l&VRBeFW1Fs5xt}St8wt(?1v8>J~9m(zE<)TRx6%a znx0v{q=!F|hD@J`R_b>Nn&=Cb1KjAo)CP|Y^BCi-c!~Wuw9B|B8`H|KOLK0eNAVup zil}_6rRPc!T@-(N-AdO)`djtcK-raK_gk&Xqgo1Z3}i^{`pHzpn03I!jtGd?k-{z! zhZV6J6ci#KXf*KrWP|&m6i+En)IKsgf>QeUR9h(@CCtn$cwI+dH5pa-Iu50XCkHx| zB2c&vBzQwH(>u+Sa>TSK`iOX$BA)91L<`l{gtjqU|z;Dtoyt@zNhqLQ+ZM}Q8+4FfPLoi^EM(nC_EId z5$*~i8Ev)VfGoD(ZpY3@)DHeuIv(3Zv zW}V>1CVL&1N?^eaTe-1((Uak8DUQRj7TkEnt5O|&{a;T@zx~xu(k7d1l0PjQ(FOY+ z@q+*6ad2IS!6wX)qE|6hA6dV2&+V1xR+mE_&P`n~$FHPfcszap(MY-GCs_N&v=b03#wIZfFBr6ak$ zQr)a)@Qn9{tT7_3Gj8%gdgt%iWlPdivmWa4b1(pT_{V=*Y4d0Qazt9bcz%lPE&h(u z{F*`OOXhJOH|M=E6E{tVzWy&lxd{E$T1TJ|dxehPOFT(xR! znbQC02Rop@K-aC#vP#~h>UwTyz8kga=c&t$GAm8#BGu|rQWmNIE>L25WXU@y_@P5hOMe-kk(q$zciuAq0 z4S{tbrlXBi_A;kEKd?%V9d4{g(T4S2bggv0((gRs$)H(>q8FWI1e9v@{iM6>yFQ9d%ga+^oxHzCa-R&)NB0Iw@R<} zi>KHTFr6d4>~(X!I6j;0a$uUU`EHWmmOMQtJ$By>>Ao8;PD>WduE?l*c=6za52o>B zM^*S5C*gkPxr3s)0^~uJ?gu_BL0G_4H~GVKByiLp z)z>;hfC`(JMcI9eeDWtGnE|nF#)12+=&NYTMw4lQ67f1T6EV~w)H zPZT_zpXx@kf@KgC{t^Ev9iU`R=}Nqgcm?^Rj#oHM@gh5WoL9*W%fA8g8RN{sF4PBp zuN0WZ{wY59JUoF>0gi4Sdq2P3yVJaCKeA&=z7gu~v0h{+chkMzHK^c*y_SOGz`S+% z+m0JlaAV4pDczMD42nNpc9~ZdU$fr9d)NnCi86Zh=v@AS3ob}I?65<|slQ)uGKxVC z#8ZvMYhU}?wDr~lle|L?IV64i+u!cKhLiL4t(w#;{fB@$QuSN$=BV{HN_)KV#57{m z*c8DU!`4h&?0P^xZoKSg>H154Txl!fE87g1gZj9VgJlsx*&L=J*1>j`%kdL(#+hwq zgbN0LnYXhZXoGI=U+Ieoe?-Yg1gzgx;9%mWJ9dK`2KJi723zl)p1$TkSM}AJXqA0Y zPb=wOET?!<>19t}edvg^=OOY9C$5`U{Ya`}Oao_m!5M+*vYCnCmI#Ey)v z5*USS6!nZ4JQ7ruM-2cv1ROEsGY&vB!BuCz=Kgvc_|(?#`}dKt-H4WqyXQC9V1o_P zmRoL_uDIfg9)XAiEE|rA6Z7%F0}rGnOO{j=S!4*%Ig0p5(Mr)v2|)xUW5dpXAMB_^ z4iP#P*UiTan)YUjB*;lxg zaAZC!DQ8u`(iy5e(zMh+*nVBC*%Wqv?J?QDUVrhAdvIiXW5F2@YB*1E08Pmm+h2$1AyZG=u!(r9 zU~es7JN434wU5_I?{&-L%9+%zq_M0=HV0{d&}!+2ZjpYl`j#$TnnsQsxw3+Rz{jRL z?2|Uzao@Dz<~yeg&pstR{@~y9C!Db#+@5-<*96!H+Vjv(aAU)*8}KCLLEP{UgUmw9&D54kZDl5Zh{++hq4!lPYJr^Qa8?#p}K-&Wpoo8 zXJjgs31>tNb{%7?_6(GRg1?y=N`#3NGcu}*hI<)~OtOPu`SD(z62g4gmiL?ch^snK znP6QE7L=Geb!t8=u(8%!YvuG~k3BZsaKjB!5h;d$_q*Snm#7-dm@y*sYM1NbG2(tAf3?Xubsprp z86WH~em<7Roav42Dm`v)7m-Xu?SQTAI`IHW6D$8tp7E@aX%uR!2x&dkGiMy z52sDp^Vl?Hk7H8_IJkkEQzIShu;9kQhaQpYz=9i>U2;L%eDlroq^m?{#nVoYXbcsf z;=xY4(y`G-qbhKvbQRva^wLYy{rBIWmMvSBe*4?sX1_(rOr(RDAGv(v`29@LMbOPC(hlqTRxLevNcPK3_2F;k4+fS!u0x z#^v8jmn=-v{`9*veDxs}oA~xm{xppmGojn&%*SS$(y6qi^rF%kj5q$~`v&=y6bfz_ zuibwArD^Hnr_+d){I=KrtMo)(J@m4-rmKJRg}jog^neecZoHTeRQV_!#|hI~4eW=~ zx%`k3$CmwlRX6)Mc;+-t_qJ3J(hshKeIifeYb8UBQ$!<`rtBulpQ4Yl|Ive|NXL2} z9QV=~bGbS{hk1wXKGuWvGF~ayv#Rat3XWFlt|U{PsT1P54z~jbtcpW9ozs+{@;Q1E zp%g2Z!zD;u2J5M#3)aa4?)P{JVMNb}LG7?0#=!?4oP!#>?z*ci|Ni&CKizoajcLM! z32D@*QQg1?3j5;4i_;TNJdxI3d+ly;!@!&_I}SYX!2J8N%PvcQ`qQ6sutU*BfeHlR zQzL(>fO)!xvDHfp=1u0IR^Vu$A!Wg4hbVgASWUPiUW|m3)BKv{jRQDX#`EF?nCr8n zVzhyn=9D3AyX~0H=+i4j80Qj{6estGWfA+v{1VBq(j!XdqBbKrS9Phj{kPFO+AIoj z!G0lsIe&T@%D$(&VMcz&r?JTYUapQGnf|3~?z`Vp{cU2T(1GZoIXs!^z^*Pv+rZN^||MsOZy&rLfZfE4_ETdxaaSk$#Z`iF=9jyo`}2iXAXR{ z^u8IYJkyHnRWyp~;W|;ikfu?ak*wlaSWeOS8Vx=VA~x*eW!*<>748~9rVqa4(vTGffaCu~~v$!9)rXFBtfZ%!L-xM3PM zZd_WnXh^#MiQCh%`Tt1!9scp0$9V1RFC3djjU1kR?~>k(Y_EfoAHS}Hl78{~%C}HB z^4K}8RiK0IK^<VA13atS@ zF>j*yy&Zfq8W{tolPv>HsY)AkLJ*e|PdqUPH#}ew!S1{7p02y@y4rkAz}}y=!VC`H4ao+AGH{#S;@2Za(mHU`|I=Y7VYU!q4G7-=B9`N9UdU z$oQ9d64O|SOR|LcqJdC&C-S2!U)s+Ae3Y}!a?a6 zm4n}2oiz5vuS(+6H$3}2rh^|7w?C|LaAVkN%hOg{Z72Kgvim}U8@_U* z-Ozq09p(Mxv6f@l-?3rs-hIcoPYx>JAC0q;OU0}9z$Ma)k&R)!#ySN99DerI8yjuA zPo=(jvmVV8$&n+5cJ~Bv-Fwg9&P_x5`qR|6oS6P{{twc5XP=h$BpEt#U5QsKiC~=l zY(bt@?_^8She7EK#s91q80z$znym6zD&A1|sy=(b$*(0Hg*@{l6ls?Ix+RaN1q&CZ z4JVH4ZuIMI`Pf8wMs4D6)i=J5@^C=SR&H2u!}hfCe8zLjX5EwSzWUc`*1ScT*CtJx zl=TAIV+Ibms1*MPNS z^?zgw887_Jc~sfRzA}{VNBJ3_h@UaPvO#K`xXx1DT*lkOa})h$&=cTjr5R@0I&!aj z-Rp7?$yUcKTC^xlpFTZ(>QkRek3RZnj5qy`_yrXN;(_^b^2mk``-b(g;_24k>Qy>Q z@mBE83S!`C6dNhek7e2T<@**O>HMwMSKUmvpH`nJyqSlX-c5B=dk_YIo#*l6$LE9d zqf>p~efOn>9VbiCk#XU^c^&Tb;d`z;<~_lLRyDAJ*F65%FV?jxjqzo9;eLACHgQsP z;Bm&fxn0$MlwQgk8A=}Wx;F4y;Tu6jUk4?RTsEpl$syqSXOv!JPpsx4=Pxh^JH_-`#P5zM*|FM;7T1^&0zL%^5n^B%{AA| zd8bXAmS2gTJ$ts~r}fugKW)41wmI$YyYJ3lQSp3s&w>s1`_@};P0N=r&#&9=z4zW} z?AU?H;>?*d(_jAbmo#tQyh@)Ge*Syi4YuN8NWVDaBWd{1A^8;7ZoBF~(<3u( zPq$oqS#H|`987OJ+h&&&H`_6dow!+ky};6*p8ZIA^zOf=Me}A#`5QH6Qd(=hjgmV# z3!bjM0tuanrQb?g!>ikJ^o=I_cAIxF{`sdQL^A zt=yFDr>>I6&O?}o@4hZATl{nyxz4yWVag7j^!~KQ@R5~q-{EBkrstQ=PeWE8lIygw z+IX9n59kc5yRVg=eBhR};@M^0akFE1Cs>g;3SB80R@PowD(Q2hJHqDMzdSv5$Db-` z-j|1N`(ys{i1E7xIPf8wf?*@pO55%IrnK&aO*(yCGd(@;iS*#@e@zc`4z%?sG}vP2scER;yRw=)uKlmP zx5{q&AC<;U+A`yE?S*Hj$2xU*Ux#+mw&?VAgAR|aHD-MFVOKo2EY0rd^To3sNd3<( z>E=Z|S?}^(hV=~VOw_^rFsEeinP!oX#CeqNR}Z#OZkmh}ms9zhg3G)^k3E8P)hA_> zR!&VS=|e=z5*b$H(fv9o8Dn-lqWsKo1}aL4=Llcr@RXPie=A3$hvrK2tr2*P?R6VF?A054 z-$*lFC;7R&o-7!XNRe`0zdEZ6&`^TgvcaU2LEux#0UOcU)K07aliNKOg^HV4RSC%wrDBkBBSdiDja` z@KaXQ>8$JhjCB}{*=+wb`Q;x;CBWduR$Fb=gFD)_gAX}kP{EBUQ>NtCQqT!jyfkI! zH>GW-exNe}IzQcg<#$7Ur|6=fBy%c|?0MLRTVUhx!w>HUHXOFwZoBlZcfBk9<~P4d z|M4IH(Zh2VEHL|N@Wn5FF+VJ@uc%l)yZ4^=yeHrL%x6B6A27W9Ha&eOWSO{O;11s`#=A&d-D@_-;fgNZIi0q z4}53ZzB9>Wu*uuzyT3mDn{FLTKvEr1F!I`T+y=EAs?#Lb@fF*&kqZ%jR?C}-?)#RHB?bE(`rcLf zulYlI@UCg;RXe>ZjhnpPDm>3SGtIj1+C1(qQ`=8{dz!G>s#i|_dcj%Q$F!A55iIQB zJATt$y1zHy;pJ(}df!VA-*!ow{ot)R4drigCs61-%wJbT7&CsO^yU-4mqw0Wzniwf zraPr=_IO>IcIi*jv`c={t;g`UeP<4Q$7j;!JMAm+v;e~XN1c?e`NI#=U;l8HwEue? z@V2zi8;{R{gG$+*rlw~)K@tV5N4DDY4QYp|M|AM1>Wm4S?2xv9#bNmqzvq4DBk8Hx zGrN7aI(FaxZE4aLtLXU1eSc3!f9&5g4|QS4i2Qn)t>UpgSKjiWGb>-&ahSN-Zdo4w zaoeS7&i#KIXea5pCeKy)V4cJIL)q}?njcnr`^t~y`uSdD=h;q|@?t(!ziPWw2cK0N zUN=9~~aM}+ZU zXP)umBVuM>2JY~`CAeW=j>ca7T8`?PZX^3vh1AkN+Sfun|WB>SN7SjElA*L804@s z|MogZZMa9{;KsQ1)=rykwpovpg*3C38wVY7c(FbXZs4RD>qVHn^Py??L%)=-Ey!U! ze$Un43u(azuVhi}uq>d?buhX1V2s_fz=j#=t6r0@!OY>vf*`|M};qbjoO7!N8ux z4_|YXtYh3}d!*Hdt`&Y}bm*Gvrtv$zAx*#HtQ>4o_Q!%6dmnaECAiVO*O~lI-s7!# zW7Q$S*JZZb_vm!bjhClUBZh=J&t>O;~S~)W4qg=snk@O?G}wg|4>ZV%-TJ$k+29zcDlC;5;jl|&+)-0UPt;8!N-$VEAyf3KVR3~tHXhP31t`9e{hH&>PY(bgr7M)q@B-B6ZFZ=4VF+4{i^k&LLIKF%P18&yJt? zDCZzD(%jDoC&r!I!6#98m}BABuYBbz^HYugO>n{qCuAqX_o#4zSDSSDVamu3PDjP@ zVu7u^*mvK3^An--&O0wZUGnz*(|35b_KnUZFeB?v^f;2sXMCE((O*?Iaz6RDa!6IZ zN-iU#?B4@tCmCOzQu2ZIEBRD1p!$F`<9@2TdBQaqzEO1c>%sCM>%>w%%&&L_Wr9D= z$oakRI8LT7ls-|DAuq4`Qm4=PH{#JMOtR{9yrLIhWBINDO}(%ED0$+#oOe`vD8Er2 z>_6hg_N`rGyZm>FFIE4s4Mlf94AhDSg)4&!JYe0JhHbk(VVkB7w{s<1h-sGJCZB%o zUm#IbqB0Mi@CW54|m8dPv&a_1?n+EA2X z@=k}QJr4bHC2gCj@6QLznxZw23HCwth4pT9-!=vH;D)Wf*nj{1)0e;e<-B6!%rnnS zAN=44^Xn-F7NoFXglf|v(g_yaumFYyGAw{%D<^H=u?HV~FyG&K=biH>W^uxSXY?<4 zFN9@F7Un0tk)82Avz~BT?*5u%dV(AF6%X4W()RX1u*vcGZC;h0efHT^vP=^;pR8J0 zE^pw$4R3>C;wC$$+it&0`nwM6b^;hy-d3+LjN53-?j+pqD;!2o+9^LFZ(?V2`>pt1 z=1f8Ve!E!U&q()EA( zZCbWyZrWgj4T`#R`J#F0zG)YPZLd3V%e2)#N2XUC`MEUp9bZk8w%)taKi6jr#xD`S z8Q7~q7C3a>c=@mXH9fOzalU8eZolr*G_BJI#Y?-r`l!sS-Y0_tj{0~{K5I8>-@*B0 zo$*t8f*V%$;$NPg{_^`DRO+xGhpm3X_9I?VV4noW;D&ANdhQQCn-A>oxo3Sg{nzh) zoaWD;ANtkG2P=IhTj@_9PXx5V>bIvUS}KU{DO^frBVSPYNUUqn@2Z2yU&3}cALE)t zU#g#p9Vk($%qvkHZ8daEOZk)|#`nBEY+HdE7$;DPfb1A3haAiM(<`h?(W}mRRcY9! z-)H=>oH-Bw+rYj#e%N7$rJwxdCmGL30YnqmCS(0#fxY%(OJMCwF}6(Bo+u#js(})q zLgREid%gDL$q&2t*kh07d*&qCd&Nq)@w!eC`O|p|=vY5(X{`lfjJN!KXnV)go?7_V zQkeg+?smI3D1j3-dtcgCx**sN`R0JO3wi~|y-B|i>Sz?<&OBy3Yxr-v>8AM!jy-*} zSDElN6D;2pn&d?toz!f`cnUur1XurLS>y2H$7B%6Snd zHBpH4W`vW{hYByguXK$v*s6T#x56i~jdq_Mn27)D3_QzIBm*3fSL28E@z^o0miS8w!EJx}X}aKh$L3WVzH)>83%|B| zuX@eFsRSI{u)s?lzihnYA>EZ53|qhQgxseY*Pbl{N$lGo9kET62Moe1-GH|It#4iR z8pWI6{N}9lZBGqfNn?jSOgQVTvvM9=iFx6L7p7suhJ}6Z6RS)ts9~?6?6uckY29_# z?WW&(=bd@)5(8U>;ZJT=0Jx;TZQ_XiVwxb`n1{FA{k4_4F8;;o>5^Z4BjaQJeB~kU zORqfieck)JzUs(y%N4&&lO~P}@%h##4oiLHOa6^`FWj5-Wo$%Y1{@=(g|NWx3aRpCYfh`;>~H{{3p`* ziIdX1Py0n0GhvhN{c#&@k*>Y*51EGt>L2`s%b+Vmg7tkvSMy=k@8J0UFQ1oJZ#axx zcZ2lo^2d8#i$X9Nc=Wy-(!Za4Q1*L1{I&DanDtjph|m7Qv3b&KV1cf&>#m)H8y4j7 ziM7GzyB?69ee$-b||EUvDxFj9$&My@w<`xLse*br+ zwbt7>O~2+hUHMhC^sliP-WEW7?a?Ra*Ek$D>jY>nJ?D#=jy6_Ho}QDgIsZGUZ}_^I zhb-7(_YBVi4*y7nS1q`+qQ8}fuI$qeuXtn6Y6<6e4RliNge)d=0PyX(6(6RY-8Iz|-N4h*%U)Gzjaay=wUK+E`sI>lu zTU7E{p2zOHG5f@SedMOJ{HbHoMgQ@`v|!dldG)!EBl`oWL---A`a+m0kGd9Z8`?m& zaRzK>9NZQ~dM)xP5D+JIpF#05?hj)>GWF_0Fsz!!TD?N@LI~dq7UUOR1 z5q5&nQQ`X~M^8aSczj}^+V%sTB88g+I_XRYMI&TDKX4*kGNRm$DGvvG7Zo=<{NDuT z^q52P^wUolgBx{EO#c4@Q+B`nV-~ayiIRFxE&T2u4yY9NHh`$9F z>@^o#vTF`tX=$aw_19mY>-^9E{Lj4kS_!zVJ@(imz3Nr3%7^u>1h$Oxjyvwi?b&8< zYNFxSDBJer&g$QO`|Y!XS#mNg$YGxXv<>I{lftZPTv_`ub?VgaJwM}Nb=rou4hI}? zKz4o&7YkC@W_7ld`HCy9ND!TL*n8s_Uwm;I45$gA;&Hx*a?ktjc(B8- z@i*3sG*n9o`99Vc0UIFYxZ!WtyLeXmR5=2yM^)WXyUI?mOmcf_GJ~{f>Nv2@FF|B8 z@gCQSzq!99dhnU;iyAX@xK#fY4oEixx2MR15syVrt5m;`kMNrJ-0u;M@ zAF*DFz=IpMYQut`(U|$~F=N;7Ofcg53^2HX8;X`>$zDe(!IrN$KFzr4ztU>`^Lhe2 zOc(ZVB0q#zhH+m#-E5hGy*gqK2r#(eV6UE-Ewg*J+T#a5_(2X9DPUf8FkNn+$y{fh zbt*WTo_Da*$jIf&7;@EnH_+VBZ1*Hi@#=8O-)tz9;Ybb6UFJ zo70H3SMkPLV!hgG6T7nW`29Ck0vvnpe?h!ZOtvPP&fLttJT*+&}!uxK#I6b#yPFl9$i7cCA zM)sv$w|jLCprGH6_RrIC^@gtsQ*#;YuLIj$v`1(wQRlM#k9>m&B3!r*Wph>iYy%a3 zC3~hIy2t(%r-_Ox95STmW;+flC_#@}rKsi@lSL7%u2EsEx!SLU_43-@t zFB2L2$m!vJ;ovi^88568^ZJu8+sN>LE11Et&&OJj!xgG42X;J|ak;;dQh%|*f){6> zeRg)hY_lj7)&&a|r0;y^JNb(#4EB`Ymfl8SfsBuS^rM5`E5#=3cGxR47U1~tkAIxb zJ@?$u0kms-I`*LteP~eomDs8aJ8WsOJ>jr1v!Fni|I@1F{`T7f8vZ_XQ?Q_e9fuxz zXb!0S>Q}!?fBy5IyZ2EpT<-0QJSDI?EC68+pgkcpyc}#bhPAino_prtjkSsVW&Qo< zfBxtEdA1Um6K-Q}%c4&?<&?}9O`wTl<_R?$E_BS*2A+PnJh84a;NXHCrBCwlWM2ii z{l0Qau`Qg~M#h!zM+RK!O2(h{24C~!jd6+UQSC%HV0{s7qIU6jl)rS3>p0zwcJa6J1km&V+JgwMCb0Fk z+oyK_bKT&E@`DUemgaPxG;TcljbWYk+1dwY+?6)lc)>?m)wBgo! zSIS=U%hU5%`+g^?&uWFD$-tzJ7(DQ|-=yn)KRXQ>Ix4NR!Io+2q3`Jlc5Jl$tJCc( z_W)5~W4X(oJ9qtS^%{${ZRx&;pPeUeI5};+?aR{G37h6G*bE)EX5K$zD1B*W^m-el zrSl%iv|M{^b@j&mcl<5$smZ-T?q8q^?cTZ&B+GQ@1TVgQalKzPexohZ!Y8H==t`>J zaImi$`3euT1v75BJneJns+I2cYSvx{AK(4Gc;3vk(cM?2c@N(#{VvDb4}4wNbq%lj z@^fV098fkRU8sIz-Dqc(jG6dm@F&#zHyN%ov`AXT^}pVRPUC zwMb{(U-!;6p`j+XK9BfenA(`h|iA7c3z0qaXb!EnT{_GC^-T-p)JkeeZiK!Hp=N zfwf~#3cmW)uNGIFFxZpBZ-4vS>7DO#MF7@G|~eO^j80m1^7N_vIrU!}W6Cm2GPZ zb2?N(3*;v(nWVcj*R6o$ZR)Pt^Qz^bK(HQMN0#DpkeU1a-^30-H zssFj9;VUXfoP2(o`PVO`<%?#?2`~Es^XAS@_f5YmZLyVp8f$Rc7rv3s`uDG=zh3#L z9(*Hb@YV}Io9uF<9eRTt7TmD?K~^lA+tFzo_pIP>*+Lpc@}g`r=k@%S#{(4xW`9|y z;+WXV3J3e#E9ODqfRzHq6Tx(u>rPy`fqhvxe^#Z8eJb>#pME|E4cL#vey|?3V;D^a zEjYm|3as376d)L}=Gyt;i0?@fL9QpDgObazjv!r)FN4yRcn{C&-oOu8JnxTTo6)yY z+>fqIq_`hZK-A^I09QqF9u)w5;(?nho4Ibb%l>KINmXBC#_iXoO*Yz4>Sm>n@ND^P z{QZ;1DJ@BV{@JI}c2nP;cG+*0pJl<7IrA3eKx;G(zG}((7zH>i2;qCp*+#ZIz53`J z)Mx@uo3+POR|MM%7SsdYF9zqcHHJ3#yAW)jk@+3GbC62#lD!J1?3>Bmb^rDK^s+a+ zPu4qf%!X;peU45`x7#mo%-jF`Gd=SX)Hl9zgZC8lyxeEThv$BEJi_I%ZcgL25eK(@ zj2refvPDhW!MapAi0Oyz<(de^A=SdYm6d=EBnB3^JPx#qmZDR{<*@T; zusn^K1?#bN{Jat#Z2XmiV?ZY1f#m^qnka_Fx|o3xKOPIO|7~x3Tl(J)yY9Mce!_6$ zjW<@_QR8K!krRbbXMhcC;*CKLFJmhx?6aOWscsTzu%{OG)t2+mKfeb*+b83bpZsLc z3Y19kycod^gh(0Mg#E=_(ZjrNzh8{t27;{^u~infGQyl+rO-=YbhbL|C;{@E1y^jf z$6$gR4)&DhxZ{p%#ET9n>uOS=z|&nncmPA`IDZ=B4!z=!9%E`&z*J&ul}`1*u394L zI4Ql(zfn&yPTkcS&sSexLHckVs{N**oDELn2`cMZq#fT!T%-0QhautvZa-Q9qv*l7 zvi*o0CbZq^Nu%N?{;g=o*Jy{E(9g^BIa-l|c>5Wb8RN<&FZ0Z@g=y9uzey8zIZn1e zdcDc1Z|{@S^h<5!-ttuEd>Q=i*FR1l{lvd?o@&NA=J*c`Y|?klRhL)FB0cy)&EGHn zeCOPkCT+iJj}HuP?6S))-3^6#?+veyab?=FGvDnmb=mLRtAGw>=OX@7{MuJoWn+`x zk{wX|g}Ctw4({87cl@<8!CSSDf;~mvX0O+$+4tX2sh7uPuh+jT-Tv2e^Q6h~S1TKQ zr2zI}b(%;nUGj8ylTI~ZW4ud{+ZddB>@6Qax9xUZHtYFRQ%4g?pbMMHsjPxOI_|6a7QD?J1>fn$=4#{6wVSeXGqf$Kn+uomjF@qakH_M8x+<38HSK|J>*uf143vd|z{^}1$ znd_h~xZ!pBxE+1;(LEcX4hD<{fBoxUYx5!h@l!@t(2=vwy40?5rG`JL^dN`x1p*p- zH?l4Nd}@EIXP=JX5I6LjwduCP1r$y zvT)S6Ez{WT-kzSg`FoO{+|QZ+cqpCzub)mQed>#~K@JO8Tyxc*dT>J;yQ6M!=Vf0= zeXF(72HPCe4Q@QYd|ujZx83s7Sq^TfF+%$IwJk9>TH9wm`JU;B_10gtcZAtf3-;PQ z3vgI)+5*P5S#|{0{*)%I>x-YtD__C^_a(x~2l^Z{XWW`6YPb~t zkYhiSe({xe<_#Df4?Ms2?Wd-#_Bb$Y-8ug7{g0)^PtD5rY+ncj+tBgyUq9c|r_t+g zTuGZhYi4)FTqGl1-c2tYqPkxUsPIZE_aTH&|DKGdPg`%jbwzLB!LO(&`K|a{t=o|`!DsJ`}8TZ^6sp9>b z{PAGfC_)+~uWi=T10YnAA_uIJP57km|CZXRP~^L4M&>_#c|Kz&bjd{dxA?YpNeNj2 zil{nEdhI@ek*l@Ym_Qc7lr?->p>Ju4SlDR*Uu<_@(k%BXdLoCXQS@=HMfD*H4%{-% zW=DyP0;HsQtGV*5=ps{bZUdys&g*Pu(Z-d-zJ|B4Va5C%Q9I_AJdFx4w8}8%NOzSn zkp=3mJ2MNC)bFFg^Y^L4p;pbCaO>Zpn0-u{P1N|;xdT<7hkLk4<%@fboP-sQz~PqF zjH9;NaisP``>HK7Tk;M)GTNwj*XRe%4SuejMz%bgJ3K*MTG+yiXpdxYBB3pYg(Fty ztC07g3zGjvX@ciaJ6*M2-D?ipUU=Ud*4GMj_TDXJU#f=(*-cK(zRuNyp3vFD>2BFP zY$c1A)uG?ci|PFBa<8Q}yKOK2G5z%;2G@YgoaAe~RR3+;6$0fxrLv2V-{)tPEz2zD zF7yk@hQ&M>|Lrik)|^-GcRC|W5l!J`3M*g5N5j(hGHm6xmgI$wCz*B>>Aj4;0Vif` z()}JzB4qIet2;6>LFCd_$bWYe`#gVj4;FXEIXR)o{X*#a8Ggc}7iG7Qa4+_qg19?Z zF7hPsNii(6)BESYW-gv?)iP7VJm@dBT`VzSFpdPM(n?h!I`0Sxu>C28{X5{U4$5wK=VmlpGkJB#h z1T=agf7EI_oA*qp${x);`9K8HRCsqqtWkdPJCoa$F)%d&skf>)6RXJUP`4^|ru^${ zVI-_^QkAmf@(<{|arv`bV^Rid)ns$nAcsl#U79}G?p8EyZ-skwGZNp_NTYIw&F?Z~6AgbT~ zY!zJAR^c=>gW*iqp5Zr54`FrI;nNRi1BU*oTqkN~4>5|#UF?S|4Ft!^Rg7OHq(vrq zAD2KfPv#pa@AMa(+j`6;!{ET@X$DF=wU2IP$y`o{sd|_y)6HP14xr%PA-(mYl<~_r znBNl^Yv1>|dh!bzs=D`Ulmq%Fyk%;hWz9w_MU1k~4i;k9My1h6lhlRl4iLeL%r&Bk zlh}aGHhujEoHV&9bnASJe%1{V-zbNTq>2CWxOZvH+d9$=A<_2c0H*Xb{U2S+a9jYm zb_rugpn|xDh-_jr^ zA}8vLQ7y|vdZ4c>_tyw=>44gFt ze#ezI2g0+AP+|_pn?N;^RN$HKNB*22pBM17@7=mrix6x1PJXN68!I~KjPBCxP#GW*}6sh=yw6}0j`jap_vgtWvkez9b2 z`b=i2M6spC-+x;Oa;Db(8%$L@+=-(prm<16=gpteTvH5>1l#>)5Y9m$MsCtig)S_UwBLU9P#L=8HU;%}>x;^DQ(uSI)NMLrOK3mc6{QHwv_$SkM zNSnLl|7ltm@m-Y2s2UHJ6>Dq#CBLP0ni8YWwI!BG_Y#4x17OI-$fK}PSwHmi+W@Gx ztwsB`>S&$=i28B|pEJxA@bOET0Uwn@XqnQ9q-6T{M-?%KRnA(gc2Y>C^Sq;fbJ#pa z9$Klw7F-`hcg5zy;-uh>+hyDYTl0tfd7;3|&~5nH+X~(m%DppffvKo1CYISZ&9EOH zjNwjJ7YKp_pDcX*-R$~F63HIa7mXXtDB!m?e&T;ZSw(q9LDpz#lsFxvHFJU${eF6N zT(zJ1Njaoo!P$0t%cDncW%;WpJbkx;fF2F;hOHw@?Q2%c1+H~kbUzE+ticl;rvbFw z>G!u$5TDc761jF5zuT41ee^_+{nYr0)P^@rr{VA)O^+$lnnQl~h0~?|E@o@~_}HBY z8R}(^s)GkgX-+ksdt1*KVEq8#+Tg1U^TvrgQ8#}I$gVct*(|i-`d1=Z0bX}9CYRcq zU-yvvN(@FfW_@h^VA!Qs+j-8u=jmT%>3eo`Nc+CMP2REWin4}wsHVy7ZP?&v5Gi7QC2bQQT&7`jGw9^<{3&e~;Ib zynFRkC2lwuKGcpN3|WM>&+J#s!5uP$y+RJH!l^%QGu1*{&IX;z_v@ChM|J`%Chi}g z!p~0Isur!I(q=$f!WB52h4;yf6MFi5CyEc6tz)}xSKi2n2I!o;xp%94lIbP!t3BpG z|0?~h#Yq+&;ipURM@OjF@Q~HR|G7@xf~Zz!P{J1U-RMgqdL3`7eBiTz>az&yri*dB zxHsRI9gZkz4JS$W(VzokY9zCmGtKHGV}wDqG>zwmnkcnMV6MaHC4Jol;c$Tsp8J-7SvYTOhvVJImU@sg9#v6j@8_rqsf^itSLYXWqp(2Hi~@lyhw`Jri~Q8(5H_L{5^Z zyR+hTLcEYoZrx2&3ct9Ed{wJ?EGUFLFBYt>k97f>P!xkeY@MPo>dkniqa08y+RYO1!=s`N^Nv`i=O3S?nTYjTvb+nwwc{q_#6J3(eMt@N8<_PMv; zldi_0wD(`>J2!B-K&+3~iSS;*`h zE*vYgL~Q@cga;g69Q_?+$rotm8Vk+UT;@qRYs+jbE7C#euL<40uX|LfuY9Mo{iwTo z7?5y(=uDNZcE0$6gMbRu^jP%i2G68Idtn$M2BI@%o$QN#B>BwL&`nd2=i zJp;PQahu(#)@a(k9-B|=h;mO7{;#zS`X_Mj_Eq*7T5Bw}RrHk*W{TuedriCSy7)`t zX7)B6I2T}kVI9~`Tt-$eUAr@Jlh(RYCpt>6~rZ}je-}oUo>d2D#Cg|7?Nqz|Ri<6#kK&_kdBuCJWPL5&>1S*ubvkPQn`j!`%a9%v#Da`g789@B7?RZf+G=Zy*_d(Q(&MlB1YNUTh`f-(pj zON;wh^=lISZhe32X~1NHrvfqfjH`JoM}i!Q{OUfl`U9Ci9u^aaYojk{Z}D~;#iEXi zBs_<%LLadH#J1mrM?bVQCNt_Y-cJs+gh94d6hZPrGcSMFpg#1!+nRle*F|8L20$t4Cm%=J~}bMhCM@z52W^qLNzmr$|Nu zA26$>n0v>|s$?^lD)4vk)t*nvOx&smQuUN!SeM*ma&V!$?tdws$-i>TcXZ4BOa2@* zgTI5pBa)NQ7cl~9#}AkNK&|V*=0LsH>E>zn1{I=J&-~3L$5rZ$6CImYh^Y)|LA7pc z{0kWn_4;8woQebumcavAIO%c%3FNG^Es0ti-2KhqF}i>RNY6pX1HXEtWE9HX_RF|9 z+!9^YNJ%n2Fwjc7pbln9?7S(&T4F^*Kr@odbI-w*j zdX)uN{|Qpto!K*o;%$!RJgyV(IcFK1FtqSQY1PzQv%5`WTJIX%xki~79&mr!4?7Nd zR*zbVS3j&pu00JtCU)EsEzPSmUU0`~%3d*t{3m$z=ZA!>KkuAnv&4A(qiOer1W)~a z$6O|*Hb0VfuJY0g7x90OPQZ1X9!5SB+1nt!zP9ywPWzYMrevuXlp|5gRN-F5bJ64A zjSer#5pE5%+)TUo15Xb)W?Uz57<3cxrj2CM0mE0v^t9M}CUFuLTI?V6zm~e{@|;8X zPo@2Vj_no|Ac^w)GU=7J9~`kw?i|vGlT1KLP$)MyGRsOT+r zSdno*MzwJ3g7!SN*D%%G?>#k}WuAd$_P<52dfxWa>+on?I&E{{lp9E$5icj>K#wOv!h7W6DG#m=c_rBFQ58|}nh4WTUdr%vhF$oRBzj>230W6CY(xWgkjk1Gu~SEM5?W$P$e%#^-R84D=No13u$tx#)IMiW1!I zQ(6tq4qx(mez!)85;Mce?3eW*T-cqk$F!xS!e9Q(mjvW`AcE;ngOEexj9pX9uEzuT z-buFwCMALsULKl&zuA|2a5R{lm9*P(kt1Q%Lza`6ELW#pPoF`eC9u+Y$V`uL7%tQk zoWOP26Fqk67nscOW1rc+7tDod?x>jmQjN6c2#cq(973gge2~*C~ zRdcTuCiz?xw;>hiuo9U{O}_^rt2_(Td*aTVyz;#sOMd=h0ZawS+bjsPnYpU6B$wEv z><+(i4r=XKPPsc}A=L#L_R$)ys@~bYBjt6g-sA1tT+vjR z>D^?gp!Gh8>)r)T-Cdq*ZubU$Y0Jp+%p`CdXEFxM$`xc>9Kf!2hF)8w++TeLPn9he zuO#2S*$iDTqmQPE`=Pp%>ne^IzQ&{dUG^t1%y&wY${R zKZ~>%iMNXt7C@(N{s6}~5rH|7JK;qN!cs*$>-o#3PYYk>204-x*Q6TeMjy0jzYJ&a zhEXwRg~Nv|y`WZS*mz#n0^%XT}(?8i=P#;?NF>o1pbnGxtN z5TU6Li}q8KJ4^He#@5Q!9b({0ikaT$>lh}O(x(HE^EaA;0}AwS>H^R7E)TiERIPVk zS9<1nK%8oCbB4(*eQahcwJ=n>-oU%lcDlxv(&s?3pw}?)uBRsC&43w=J?ALZwGOM# zD$I!HhS?qaJ=slj)gW_j$}E$yAO*6Xb=c#-m7U&Nw=n;<7xs!1hqC66{^8h6vfh}! zg-Qoe1L|FKoK9kB>NdNsDO?17rJSpqsf{Q3Q9}S(C;bM$vqKR&dsw_C!agy%P2Li0 zRF=dAchULej9ayB522oGvgeJ#H*3=DHEw2XtDO0#o952>ld?*xhu6JeTqwc+9Gw>= z(HfL--AVegJAqH%a`P9OLE%nyYa>S(vROIV5&v0hn8}gd2-)fK+H0|Zvfu%GL46K; z`Q0w*>@}eC-s=)%r}b4@$ylCqvAUBENWjT4X@9W2&+G?z-j?Mvpi%;B)!AKo7_oQJ zKkUlQDK+i<%F|Em2g>h&B!NqV5m1m)=f!0Qck$E4??zm^-1)?%3*CuXXF| zgK`{nzF-1X4JP2*!j7XbCZLSW)xi)qJ~#S0wLZ&kuqgL)zt=EumJ+$@NV})r?QF&& znN!GT@k}eoVrVWok2P~Kr@!U+7XOm^x>CI31yXzzsig$KHwRPiXbi0@x!TcxmNhAb zt4wgr_*d~8)CX+Lx3yS>TEa5l0N@VY2ku!kW@ z483l#AlQ*aY`2}O1L-B+G5ORUcPPSPz$pE|LswS6SeQXU9t21!9Gh(HysEd+XNZ>o zD|Tp^mEpZhN2n#7CHrEmXj7yWpXVEDA5 z+{S+E_6_XsYeC17Q@&ko;OA!ZC3fj>XnI^d2w=}}b7o=rTZrs6$oXjUX!8Pj*PRkX zmF(`1iOhiuJx-RD=F$>!&r&mu;{G%0|xG(B^M~SpSIk^c^>a)b3 zprMnVZ4`91pOOLI>Y?bhniBTr#Yge$)}M@6(@*i8eDd~4##_PXDf8;+~55iuxc;&2? z+;nbHfQ|RV(U$U70b!V%-fiuC^IX}sEcW^SZ@owj2Xi1NG31U}($=o!DxJQ=V>_~L z)3yF;g=|d;wjx(!c&!!083!l_xWAtNIz}j}Xk~}W{-a8A#mE7PJHZiq@Q}LeXVEh{ zO*331*1op_aKnF?b}^`<Di!X6A?JRhs%bP>gN?nGVYLg+U+mCxZm^7$- zcTBYyxf87RF-j_bH%cy>=8baZ!;%)>^^zz92wXUKTKTanKuTl8NiQ(r0j$pF|TDWUo@CF zjh&dVYX3z^x;*ca{IWm{_HzIV1IQb|btq7g@@Y^ORE_M$d(TVPvd!aOmZ0e!i{W1(cqGZtgrDV9}Pg*=XL--Mhl0aAYPZDU)y@F?ibA*Ho zvD3#Z-HLw0LOB+LKHMNpt6lGuSB_=KINgx2#R(|l#_cO$QD()aG=1pe zgY#Osl1kM^-PIIeqF?e+=IF>QeZMc$nRNi@@Xrh5QN6g`5)BG*(5dTE0h<4OwKetB z^R)^Wm1Lc!hr>|%a$7(hV^Om7z2UJlaIqg};LoFKhu0K1I~+*sf6BiRbP~WHb2mNe z{6eS@E$GkUAL~`!0TK#jr-)iedO<@Aw#E!^#bV?4uw?prhGKF&cY)2XHI>K{uZ)~$ zG>=KLq7yp~%LanpTYVaLmA|6&KN9230w*Ub;YPm>)H2JrmA3M^j0*$fIL!+&7Nq7b zoZz#5za;=B?@8yW{E5TEM=n?!endmZ(@)&_W}KSz(3}SUau`PKET?)?HKF}TdGV?R z57&sAtewK2r#1!!y+Ty>TVS5pd5TZPF^t9fm9Rd-qYFV z3eXxyvIhIlk9Ci58vgd(o}3}JXG#0~rfvs_{wtAjcFo-K4G0Q!ID>G`O5*@Ur#$=F z-T$uit-JrKx84HV*9M)yaN%I4Wn#tN5Vj@5>G4)ho-BMO4SMnBL=Ow30G^ z+Y@Ygjq(B2xT0QgF=bCH@0_?eByo3Gmjf5f98K-y4&eE_~%b0Z+_ip z3UGvBazYQRe1znZPj#v3#{ElGWYc8Kgk!`gW6Ti2IdlvTxwCfC2tkz1#LQ&xtWDc z;jhn(Lu+BTj6V)zC)fiEWiaay~Qd2{hpS9R%r@XjqW_JK6fEus4<;p&IO zEu9Lf-W9c9ItEBR9&a4Gdts8H7M^H&8>S}uzXSBA|Pv$ zG924px>ylGLQ-rJyDy&i{4T%TS-2>JD#8BekWGqt#7Rr63fl<-JgCU`igZfM@&1*4 zFFKf-={97>`J?~2Ua-TatU*QvBrM$DXL!0s6Fc^```W#_v`#2OS`O>bVQZS;Y$Rv1 z>Hvx26H*rb;c-F9D26fsWCFjbva%R0+XY8D`EMDWIrsGGM~sy%SF9EyGF;Wbb-jlc zf%i=->$ciL1;8kJdrJ567L%1_DTAiIi{G{dcmG~%4vBa?R-}W!nZ|cVmh(dX<4WHh zA;rRsi>GbvK3{Er1H<)9snA1rSdH@cHtB~Uw8BOjxp(Ph_}-oTkusCFpV568e9%!P z_#>M`iusSW!_xI+__nH$oWj%m5Kdl&AF6i}aUD#zR__!`(02&2K{L*E z4b$nsgIJP;4VtZg=MwP4&$EVJazpPVQm8!9vnC_R{pqK;ySlfrP7S{(1_fN8AfK*{8B>@PAFZfnO0h zFi7&z$GZ+Ae7R+6_qv2&*S`j0S6ACXXgzY=vOsPAZiyU4kg$!ba50%(|F7qJucq_< zoCF1H77k(fOdTFf&9lvvM>tcT$?oGjrRDN6olk1KVeqi)+Vx};sA0fh7uc>+QJALM zr}NP=>>r1~B1MYl0wYy0K{OqnD12ti0mhViEa;%lw2LAs401d@?T1{%Gx#3=KIm$Z zx>_If216FymSmf8AJb;StWNfp?LLWAWfAKQ>@jnxs(1a75xyCA; zk`HW)TC(~hfDGo}4Z}P^Fax8=!PLSIyl`!AY^BF{yR-KGkBa|-p|BQhv zYd+HAX-7K%kBDU$B{<;lVAz8TT$+5(;0F#KOVpMlHbXRr6{}m;-}4d591r^w=50!U zaGkPyeCXQaxPwELNEsGXD9I2eX;*=ix)G@h3RGbDsdQE{|3?G#XbPP?uM6t zNi(R2=8(e&0QYy(l>K#ciSz?CT=uTV+29GC%|@GYm-QbWFQmJvEVV+xJOIzxuQYwb zYG!i<{iKurEz8lHD&v=J!5e+y3vQ{O4OPIAA#dLCTY-4aM0#&7Pw>rcN*sGX!pvf} zQN^HM0|D;!C5f=Yi^B4ciqTAm9$$4pPSdrS6r8m0({atWn6_qN1sPt`HO(TdVN@3M zQK;DKj=M{3-m=c^eRmFYLc+Zdq;E=31g|n%R6OpkcFOL&hSQiyq-UH|i@|8gKWo*N zA?+tDYL_2NMzi29j~A~ptmZWgnRWJ_&vM>U zvVc#@Jf}+p{Jx+R7E^76peyMvW0(wAUoN)BgOxeCWw{@SEmzGJjNY29b8VXh=UDVu z?rK z`9nnJ#F_%td&A82-I5zJ+>T`y)Bakc`rR_t|6#sR%0%()(V62Xn@k+49{UfH-LGqz zt5tPWA>4D>RQAGi*thN!;EJP#$b0%{k04{9@$wEBl2NxQ9{UccdTbuBOFK<<{q}A1 z0Myx=DMT8Wa$NiIEH{kMRvn*zSk1t{zUb|b&bYK;-jDfZaoB#5? z-XFY}<}E;uzNfTDKb7Q*TsH{dG08ng^i^Nx^c9&(t9+BLQ2e8F!+A`Thv%#M_hrT3 z_BYKG@RH&RfvU*G-ZeMI_3LO}5!j;IxpXAO~N|Ax@^eIBfX(Wq(^AWJK*) zemR+i0&iOiJE9Pf#kzjokQfa2wY^PauXnZYYneplLsbljKkZs<6ty|vtQWWj{OBXh zwMPSZiJ61cHK7#RE%#P7!q2OXIaAm@JK$^{T^Wr2yyl1ioD+ zEw2LrRo_+o8h%(<4NE>ht(i%uxCW&RkBg3C>U=dniP`!1*h~YPZ2yR+gkf)GrNL3e zGSU2#z!gm9!=A}J(`_HzsL=u;%YEJ{Fyp82r^zxbF-rM1^X#G`tF}}UL#RKct__xUqOjI4E)7mKs^0%Wk(YEM3;yMQ!UU%DX~1_T6{|Q*6Z01pZ5pcb!@Ye{h#MEus&cJr$6a0 zZ;xA(J z=k$NGriu!0zX&IW_K{dyLF_wlMu)@A4MLGT2fJ_&(dl(C-D*Vz&o)W-1gwN`F(I*U zlzTlY=0Vg_c&3r?ziX$G(^hY&R^^?!;U;xrCS*KrG_*bgB!KQLQOb~-Kl@$*xXuxZ zSQ}Qi_Foq|<7hAZu$9*tD_H?|tw>kHpOx)n4BF-M|!s*?nli1Xh*smSpZ zQ>DkBerbNN`0f{su5?s78R>h=tHXQwej|UhMYQTyTr`i9xa0P%r-9ikMioOkhaD}y z{Rs<<&Z%+%Acb*V;M+EjKL9$*Z2_YLjc1V=pIV2izKEIISlJ|+eK=`#kk)k(Wj(soTAFk+d&N$Uvb zAC(E#SEm1l#%^V2=;vef0Oc^Ns~ z(X+_ocwD2o?Oe69WU8?B(5jl*VT#X!Z3d!HLTTAO)R*=y!R$pdEiW=~I9_yGj~=1P zH6I-jSpOGvtc_+Ph6I93DGPwCdp>dk1i}D^+TRnXWeHwq7lz@2+;3asN8LL*T_?vD zK3p&m7&jsuDz!c)NMLOl%RP>)J_$|ZYd1whzc7*6-Y5a*O~9xhWHs#to#Isfj=9fMP58G@QkP& z--~B74~F`+K(xf{*=H7m_p5~WHH3aI{>1kAvIai;eX_Jq*xy!fd~8wJmci{?)<*EC z05+jgOp(H;SvLszNqqa1=|&B(yrsV=(`sPqUg8+LQ``msE_-Y<2Q(U-Jg~@Ju4m0c)j745?Q=2K+W*{w^ zJvqWF+y%#P$wAWgV-8-1z3Y}C6Z%mWy>-_4*qU$!<{USg0};=kPPoFbI6wnBuvj$e3Hu4Py#_Lqoy>x(}- zC0pM`{*(D^!!yUPY{I43Vs>8CD>5@0&*2L376FZp=LfyAq-}LF;Ob%DNxwQoK7E{%p;A5$zRu$y!XUsFVyR#&VsILm@@pU%keX0@H?%H-FXFar~10*$Q zS;~2=k?5fd)ZH#UcEgx2*^{lJ^U`lBi{Q6eb`Km~6g zE3XH|c^dc!vflQ>&mkz_+SUQ5+P+OnGsW7 zC;iRestYo5aISq(>C{aNoea2tLCq{bww68-=5O+Qx?!`pWHg>v-Lh2}Mv%LKSm)pH zr@~fS5AQyg)8I$#(U&i|Q)niunyL;uy6h}r>5feTn0eF?p29Zp>}MBc2PU_5S6JTD z_*upsEx-KF-^@DHHSG`r!hHy>z+ zmz+NaF9w(ri?z0si$krxa#F8Z)wpd6R3a?crqz`!W@f{dsi@qaayo}fy`c=+3G#X( z#o}xXdE5mhP=f`t%LhtB;XC4-tbqko(+U>8JmYOL~2q zwGV$f716QBq}oq`^ZtE)!Vha{xtlEdNcnM(vI+iW;>#OPg$%piD+(wZGcq9PM0Bb= zOLq3RL^z51{cqj3Co6livSwCl>Fr+yl8>>Jx@Q?4V1UE5FYwsf;nwLXK<*cK;O}A7 zC+)VwX*1>Et@36O3U*`^J>9Kj5)#Y}m^i&tdGqyJM3wt#r<} z)rLf=gyQ30J#QQAO&oRBKAq%$x3e$7(D(YSG3#TLzI)9kio%R2T9VcrwpU;YZh(9{8)Z_RpunNNJobi08|Q z`66AKSh{OKVP*zX|8jLdSeWF50S`C9TOL)X03B*VxWL_Dv^orHd>E+*7MjY3S zU1+GUO2*3Q#kEV-eHxQ_J0kb!0ky_gKJyp{WolZEE8+2z{~0%Y{IPU)e3o@$h(7TJ zp!cTNTe@n%)Gc z2(T(NseUvj$Q`+{#0H}V+zVxAQ8H+@@eNzs1R)cXW|aMNZUr~z z3}cVG4xt#_F_S3w<-bj9y>a76$Z@Ib=_iQm&1zE4(jS`t^5+h)FUich%O4iW;M$}a zckaNLZoS^vs%M<_j18^X{>A%4yH&AyFdmKx%L8XUR%PnQ0Ou$3WYs{wLp*|VoROIoR^_Q*Z=vI5jILC*6ba_F^U z?3X(m|eG=a{c`CyU98 zTkm9hriH8%s4Tm&&r@cS_M!1pP0Q*}qcu^~jY-&u6u;5yIYXRBWbS;v`*|Ut6K5b z0bzPga`=ckIYoXlPTWj$A$}etH!Qz-i8a-|6x8+CU)^Nou+*ST2m-7Qmmc&C!?>;y z^>vqvj1)DM-xaP;+^M*Js+t}4hOs-1b_8eOmLm&}P#)!9Kb`oosGkH9tOF^T8kM&n zu&}cfSnAzBQ`wmuoD9uUl!70<@K>7(Hx>?-9`;0Jh)zp(T=kDnhy%@gvPUPT%0R;e z>B1tl1ap*_R40bv>xe^nk@r$|t`keWzHILEMEv(aJnD%|IPH>QSt z2ytYv@yP^m$A{K=Q~v0D-HFugq!D;|5~u{^J0?+Bzxe%B?|P==+2aJy#(t9m(X)8b zREUYl9f$vU*4Lq$gwr6bdn0@f=q=p#{QaCSWNh`ougD(pY#e8$u*?Wc0c=$R>;m+# zoNmT7&g*sf=f~pI%S*Q)ryT_N4xL z1PL8p8%p9LPWB90M{m>;;&ORvlf&f~wh)coZdv$@jsLB09wEU!7iq||v<=|J3NUuM zcwfk9osjur&mn2EJXD*k{wXWavZVP!?BMZ66>ePtk2g_+^ldC1pUz?fOJ}@Sk3j}$ zN>4~S>;*LffTWSD9#Tu=8=a0~^X$)_Fd|h_@{M*EQ|JzAZj20FX&B{u^BGq~^|*@j zpvaL;pr?Mf_fGkdtEcKGkCF;^19&z~9vHf8!Pc??IM#-Cb(2D^jO`ec{7*-RjD$cNm;#?@BA)3 zNk^|3s;qaXU;JYz5cN64A>`*aauLI)(cz+X z!fribfr=sIZ6<)5ycS8@|8zuEKo-9(+Gqt7aCnq=SD%G`BFq<$tageYC&mEho$bL`;y z7e<7NVzWBhwZe0wDd44rh^)-bqiMv@cySH!@1?xcu_r=)!4^8vLizAAXS8<&X(y_) zj!R{Y6UI-_mt>};HvA3j+bj+_dD$<+<A1yD|Z+;p9uM^8XZ=>Rp}88hN`rXcbwqm@@~%e6sc`Jb*gki(Bv6( zrsr;-GT}$KLf*K*IwE;%5y!Q&eU}{1lG%A;hbGw3vj3`H08oBSAQbw9R>Z0LY5+Q_2hskp{?tMQ(M^d>_IVZNth-q2yV~s6^J*5CxCtJlQf=!_ z_$lpt!oBrNaB~>)ta~Nua5Ab4$P@2?Z z#EXv#-mmOJ_l$zEdQ---BVy60l9W%a!9g{($gzR)azd4?_v=d4x)u&omyE!`a=-b{H4ME092xHG;cHEq2=cyiDRwxiM+>Br_Jo+Jxm1n8 zPNffN<;$f2(em>LvG`O&QWx@aW%jf~)PKSq8|6YoIoAQK>cpbrJ38NZ z{}4M9%Q*{X@LN=vx@=t_wsvX-J6#f-;QZ>YY_r7q6EGo;Z}>go?j1zA2}bR>aJXn6 zNa_M+c*gaa{gTS|Lr~K^yJvg2X0>TJBoK=^Dh(Yz{*F9mnnI9##=4dU~>3yeeTZ%)*qX7O;$XQ!PQSse!?4Cq_nro#JK_- zjaXJu(*f-?#jB_ytDZYm^{Wy5SmbiDR&tZJrdMdR$WymbDkAcS*hgTnsyK|N_JKw=fTzsG1MaI)3(d! z0|63Al88BANzc9LmHxB2;~3kFiV51-(Ng8&P?^}HvJP@uAHOlDnylqJ>LxR-ff74O zQ_pOh--y^Sx@5V!*bTQS%&yIkHx|9^kqmDQKPxf?it^Ka&9rW*`eky}?&Gr3FmS$_ zRNuPefKUEEj;_U@ssI0fx=Mvgx?ogpA(Z?B_Xj%h`Ha!+!H0`x{+MAk;~jy z?w6t5N6lqe?)S!A=Q`JZ`~Loc^LU)cIj`6I^?tsd7vb`Ngz*uByiibN zo8sh6|0b=mu+Nh<8q*^mNn(K2^J%mSu;$~Ky%A)__%=JhMzc!<0_SGh9rEea=}JU) z^(oq?HEZ3U(=SK5PJtDj?0;BWOvT=#A7gsLJc5DL;NzcNzE$~8;X7|ZFL6Txhie|Buq)93%)oz8@f6Qi78i;Yrb_6_0shRolI8ZN zOJG?kiyxi2bx^TgNM_JYR;r}*;;i~nl zeQQPGvn5G^4Y5rpPEZXToS+;8W8Rm=UW~#Q!^p39i%&bZn#G<=g zVSdWj&9SNR2kUbG)^Eb4TfM3sOLN8wQ+*f~97L)560aUdF>4IGGI%)n8Duf3J z3d&9#EB{N#Jmt1g7){S;o__afe(zHw>y!4#!j_GUs_FBv&VzjXUcSG>o8iheQ#K4h z3q9PVu=ySypVP#@39};@G^rWNi8F-z)8!73wmZEac zNT^M5dXt9rrPl-AJvhK%<|RVcC3x^L|K7^6-U64{52ZQ4b|avy2;ef$(IgLr#o)U< zCC3rIvc-sHJtszvu7>QSHCJ@qFCR?t94`J!%p`kCs68awRik#^aHzd>Tg0=X@T#RZ z_02T)vNZSBIxTW~p~C;gZ|JRmrAMkS*^~u26TjQFp58hd`-dU_v+~3dOPb(}Rdc(!Y_UQSoJ?AHV*lW;nbD`-Wb-(bI z7J+fUkDpo7h)eDrjfu(w81c6*Rhy58vtIp2a_bues-&N8BQoQ5qRflxoCP2_T~vHB z1+S>LU7etn<5K+2kN185`+dp4?7D3z$~P!OJ%6pD@})qo+@pbH^!%)zauzjg_Kr}+ z@>}=)!``|*B*+h>kM$*_w_%IKQV5WZS!T91AbQ%0nY_|3SWRDa}_>BiOLETtL=HRfL<3K#H#;$3K+5=;M2d7$mN38^KAkiQOpqbkMk zLD_M9bn=&ZbeXoM(V!wCKfLf14Qqs|ShfToOEhC?uvIxY37ga}Qr$a2-8;W{ZNf@Y z^T_&{&3$vEC$NCvud`1v7mb1x@AU&;rCwMt4S%~oO4slPq~;-}bM-UCKc*_01)kFc znr@~>QXdP6;x6;NTeKD0_~O5w8xXRg66&36qPRG4*@%?{;k9HMVTy(KGKEvSw|ndg zJ9)bm$yX*?MgrOA5EX;l%)V|Nmd-I9sUb~N?s-`H^0UHzQ_}hwIin|!(oiMjd{$0W z1?x#^zJ5AOjx_7xQ8Q?Yc@5+*H+e)k1w097E9bja=c6w!uiYN+$j=Oiin*s=b9%7L zHL=DO=^NB6Fn+gvV%g0vZ6gnERKh1-+fT7RXth>b73_6ZN$oKxX|)_GcQR-GWm^Zf z9~5yGqbw+jt>*}AXdQu@ciwpePr9w~32yXZEA=C;Vo$DTh3oKgiJXqpv=zx5WYkts z+D+8YZP4|vkk+ZoMvh0?HTIo#2YM@J4lYWiMt5Dz>83cVyc2|>@vHj{t7(9suP_ID z)yp#IdkdrZqXj%=sy6vo{R)zpw(xY!3A1`YSxpBhOzV?NPSjQ!3kg?|rR@u$*E`;P=F>HGBeI3( z;0KSOkBJ(_+sIXHC?VVs!jY7v==ACMLiF^bG7b2Rn*J3Zpv~+_KA9$H>3WB*hF8Ea z6{kFt2l(r?qQB!r%JLH9a1sW7Z87Zov3t55#z*n|>!`t;IueZRr=b85SeGAW$sMh|GuzEV-6aoz@Xy+_kucsu&J(yx8 zXgQzAeaOQRk~BW!G*WdgVx|u$u5*I%VgX93xpXaOb@==A*^iZwUA1~1pOm)d!(p<{ z)$s~vmFh**{nQT%BMqmI66=808!rqB3K{v^#Q^@=9lv$zo5TZs5`-1QE}O+ox9m^9 zl2IeQQ@gxE;E~p;T3r$;-Hp)9&qZ!CJ_;VFyJ2*ij~LrH`yPuKskQ@CJAXvS*|{=! zuwDlTg4~S+(lhMF#;*T0)5@-b#fJlQO`!)b77maaB})=V5-=g=9d$4dZ+5M&*P|3) zf6+K8BVBVTxh-4gL(Az$1)AidkxD0t5=CP(beRlOW57M)}*U6$HI zUdY6r_fm9Bvc$mayH{OV`)qTH4G3)-WA*`%2nQRSY%;~5ieEUtC6KXfQC8|*NjT;q zz)Y$ZDIUh{*Z(=#sDoj3I{4Tl`sEK2_)UGhB9zo(G9}Fs$;!cA8J5iQU>41GNnmQP zTDOSg(8|o}=Tv)7!VC8v(U+Gh1eOoQbR>#U~%r(mRm}=@MSI$TvlMBH;e596-%%b1$zCbhOsgOe7m*Z*cYJk8Cv(5L%Nix1E*7u3I-Q-tp3F^!ak_)_udQo^Sa-Ko1IWUp z5w9U#nmJyp-O;s9T8hRTc)vlrF!dZHP2fCO<3s?DgQoEmRtCZzSq1;=&CS{GuM#8Z zR8jL7$laUcW0{BpPc#I`nEoPO;fg-xu06bIeBDK1^(xw1`-Bf{ZnmEiv#)t0)D32B zu*)EXPHoacw+8)@CcIHAhLQ`^Zt@f+gEe1krRZFm&8h9Gl5DHR`;?u<>v88o+1JeJ@%0Df?e>EESjd)TDR}ec};@F|3psm9}OA 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%0AvhZ2fO=9YKN!vYe7fucXb< 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 diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light/cs-logo.png b/package/SKSE/Plugins/CommunityShaders/Themes/Light/cs-logo.png deleted file mode 100644 index 6f9084da1853913c4c55002ddff3c42f2e2a0ca3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25838 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

InWm(!vh`}X0fYM+}jPN_#C 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�|O;<7Imxh}@`ERuDwOl0Y! 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

  • hl31O@dw#GQfvpF=`-g@4p5Y5Yg?5rgHO!=4ai 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 diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light/discord.png b/package/SKSE/Plugins/CommunityShaders/Themes/Light/discord.png deleted file mode 100644 index 6b585b8583bc0320363bf2db366c3a489fa2065b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73661 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%0Df?e>EESjd)TDR}ec};@F|3psm9}OA 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%0AvhZ2fO=9YKN!vYe7fucXb< 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 diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost/discord.png b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost/discord.png deleted file mode 100644 index 53705ef62c1ecbbd3afd5b92d0795ec430d554f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121874 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 z 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 z20!e%rUSNXIsDeveloperMode()) { logger::info("OpenVR not natively compatible, but developer mode is active - VR menus enabled"); } else { - logger::info("OpenVR version is incompatible. Community Shaders VR menus will be disabled for stability"); + logger::info("OpenVR version is incompatible. Open Shaders VR menus will be disabled for stability"); } } } else { diff --git a/src/Features/VR.h b/src/Features/VR.h index 459ca8287b..b08cebc193 100644 --- a/src/Features/VR.h +++ b/src/Features/VR.h @@ -31,7 +31,7 @@ using ButtonCombo = InputCombo; * - Drag-and-drop overlay repositioning * * The VR class follows the singleton pattern and integrates with the OpenVR API - * to provide seamless VR experience within the Community Shaders framework. + * to provide seamless VR experience within the Open Shaders framework. * * @example * ```cpp @@ -100,7 +100,7 @@ struct VR : OverlayFeature virtual std::pair> GetFeatureSummary() override { return { - "Provides VR-specific optimizations and enhancements for Community Shaders, improving performance and visual quality in virtual reality environments.", + "Provides VR-specific optimizations and enhancements for Open Shaders, improving performance and visual quality in virtual reality environments.", { "Depth buffer culling optimization for VR performance", "In-scene overlay menu with HMD/Controller/Fixed World attach modes", "VR controller input with customizable button mappings", diff --git a/src/Features/VR/Input.cpp b/src/Features/VR/Input.cpp index b4c2278115..43e7528ee4 100644 --- a/src/Features/VR/Input.cpp +++ b/src/Features/VR/Input.cpp @@ -74,13 +74,13 @@ void VR::UpdateOverlayMenuStateFromInput() }; std::vector mappings = { - // Open Community Shaders menu when closed + // Open Open Shaders menu when closed { [&]() { return CheckCombo(settings.VRMenuOpenKeys) && !isEnabled; }, [&]() { isEnabled = true; } }, - // Close Community Shaders menu when open + // Close Open Shaders menu when open { [&]() { return CheckCombo(settings.VRMenuCloseKeys) && isEnabled; }, diff --git a/src/Features/VR/SettingsUI.cpp b/src/Features/VR/SettingsUI.cpp index 08819d2084..b237379377 100644 --- a/src/Features/VR/SettingsUI.cpp +++ b/src/Features/VR/SettingsUI.cpp @@ -109,7 +109,7 @@ void VR::DrawOverlay() ImGui::Begin("HowToUseOverlay", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav); ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 500.0f * scale); - ImGui::TextWrapped("How to Use VR Community Shaders Menu:"); + ImGui::TextWrapped("How to Use VR Open Shaders Menu:"); ImGui::Separator(); ImGui::TextWrapped("You must open the Main Menu or Tween Menu before VR controls work."); ImGui::Spacing(); @@ -158,12 +158,12 @@ namespace if (ImGui::BeginTable("MenuInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - ImGui::Text("Open Community Shaders Menu:"); + ImGui::Text("Open the Open Shaders Menu:"); ImGui::TableSetColumnIndex(1); Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - ImGui::Text("Close Community Shaders Menu:"); + ImGui::Text("Close the Open Shaders Menu:"); ImGui::TableSetColumnIndex(1); Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); ImGui::EndTable(); @@ -530,8 +530,8 @@ namespace } ImGui::Separator(); const char* comboTypes[] = { - "Open Community Shaders Menu", - "Close Community Shaders Menu", + "Open the Open Shaders Menu", + "Close the Open Shaders Menu", "Open VR Overlay", "Close VR Overlay" }; @@ -587,8 +587,8 @@ namespace const char* controllerRequirement; }; std::vector keyBindingConfigs = { - { "Open Community Shaders Menu", settings.VRMenuOpenKeys, "Button combination to open the Community Shaders menu", "Primary" }, - { "Close Community Shaders Menu", settings.VRMenuCloseKeys, "Button combination to close the Community Shaders menu", "Both" }, + { "Open the Open Shaders Menu", settings.VRMenuOpenKeys, "Button combination to open the Open Shaders menu", "Primary" }, + { "Close the Open Shaders Menu", settings.VRMenuCloseKeys, "Button combination to close the Open Shaders menu", "Both" }, { "Open VR Overlay", settings.VROverlayOpenKeys, "Button combination to open the VR overlay", "Primary" }, { "Close VR Overlay", settings.VROverlayCloseKeys, "Button combination to close the VR overlay", "Secondary" } }; diff --git a/src/Menu.cpp b/src/Menu.cpp index cb6b9c253e..1e5d7596e0 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -291,7 +291,6 @@ Menu::~Menu() uiIcons.applyToGame.Release(); uiIcons.pauseTime.Release(); uiIcons.undo.Release(); - uiIcons.discord.Release(); uiIcons.characters.Release(); uiIcons.display.Release(); uiIcons.grass.Release(); @@ -639,7 +638,7 @@ void Menu::Init() } /** - * @brief Main UI rendering coordinator for the Community Shaders menu + * @brief Main UI rendering coordinator for the Open Shaders menu * * This method serves as the primary entry point for rendering the entire menu interface. * It handles window setup, docking configuration, and delegates rendering to specialized @@ -669,8 +668,10 @@ void Menu::DrawSettings() resetLayout = false; auto versionStr = Util::GetFormattedVersion(Plugin::VERSION); auto expectedTag = std::format("v{}", versionStr); - auto displayTitle = Plugin::BUILD_DESCRIBE == expectedTag ? std::format("Community Shaders {}", versionStr) : std::format("Community Shaders {} [{}]", versionStr, Plugin::BUILD_DESCRIBE); - // Use ### to keep a stable window ID regardless of build suffix, preserving docking state + auto displayTitle = Plugin::BUILD_DESCRIBE == expectedTag ? std::format("Open Shaders {}", versionStr) : std::format("Open Shaders {} [{}]", versionStr, Plugin::BUILD_DESCRIBE); + // Use ### to keep a stable window ID regardless of build suffix or display + // branding, preserving docking state. The literal "CommunityShaders" ID is + // load-bearing: changing it would discard users' existing docking layouts. auto title = std::format("{}###CommunityShaders", displayTitle); // Determine window flags based on docking state diff --git a/src/Menu.h b/src/Menu.h index 2e2b05774d..0f51be0110 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -56,7 +56,7 @@ class Menu enum class FontRole : std::uint8_t { Body = 0, // Default UI text - Title, // Large title text (e.g., "Community Shaders" header) + Title, // Large title text (e.g., "Open Shaders" header) Heading, // Section headers (tabs, category labels) Subheading, // Subsection headers (feature names, separators) Subtext, // Smaller secondary text (descriptions, about content) @@ -209,9 +209,6 @@ class Menu UIIcon freeCamera; // Free camera preview icon (weather editor) UIIcon playMode; // Play mode preview icon (weather editor) - // Social media/external link icons - UIIcon discord; - // Category icons UIIcon characters; UIIcon display; @@ -401,19 +398,19 @@ class Menu { std::vector ToggleKey = { InputCombo::Keyboard(VK_END) }; std::vector SkipCompilationKey = { InputCombo::Keyboard(VK_ESCAPE) }; - std::vector EffectToggleKey = { InputCombo::Keyboard(VK_MULTIPLY) }; // toggle all effects - std::vector OverlayToggleKey = { InputCombo::Keyboard(VK_F10) }; // Global overlay toggle key for all overlays - std::vector ShaderBlockPrevKey = { InputCombo::Keyboard(VK_PRIOR) }; // Debug: cycle backward through shaders (PageUp) - std::vector ShaderBlockNextKey = { InputCombo::Keyboard(VK_NEXT) }; // Debug: cycle forward through shaders (PageDown) + std::vector EffectToggleKey = { InputCombo::Keyboard(VK_MULTIPLY) }; // toggle all effects + std::vector OverlayToggleKey = { InputCombo::Keyboard(VK_F10) }; // Global overlay toggle key for all overlays + std::vector ShaderBlockPrevKey = { InputCombo::Keyboard(VK_PRIOR) }; // Debug: cycle backward through shaders (PageUp) + std::vector ShaderBlockNextKey = { InputCombo::Keyboard(VK_NEXT) }; // Debug: cycle forward through shaders (PageDown) std::vector WeatherEditorToggleKey = { InputCombo::Keyboard(VK_SHIFT), InputCombo::Keyboard(VK_END) }; // Weather Editor toggle key - std::vector ScreenshotKey = { InputCombo::Keyboard(VK_SNAPSHOT) }; // Screenshot capture key - bool EnableShaderBlocking = false; // Enable shader blocking hotkeys for debugging - bool FirstTimeSetupCompleted = false; // Track if first-time setup has been completed - bool SkipClearCacheConfirmation = false; // Skip confirmation dialog when clearing shader cache - bool AutoHideFeatureList = false; // Auto-hide left feature list panel, show on hover - bool SkipConstraintWarning = false; // Skip popup when a setting change creates new constraints - bool RequireShiftToDock = true; // Require holding Shift to dock windows - bool UseResolutionFont = true; // When true, runtime font size scales with screen resolution; when persisted to theme files, FontSize is zeroed for backward compatibility + std::vector ScreenshotKey = { InputCombo::Keyboard(VK_SNAPSHOT) }; // Screenshot capture key + bool EnableShaderBlocking = false; // Enable shader blocking hotkeys for debugging + bool FirstTimeSetupCompleted = false; // Track if first-time setup has been completed + bool SkipClearCacheConfirmation = false; // Skip confirmation dialog when clearing shader cache + bool AutoHideFeatureList = false; // Auto-hide left feature list panel, show on hover + bool SkipConstraintWarning = false; // Skip popup when a setting change creates new constraints + bool RequireShiftToDock = true; // Require holding Shift to dock windows + bool UseResolutionFont = true; // When true, runtime font size scales with screen resolution; when persisted to theme files, FontSize is zeroed for backward compatibility ThemeSettings Theme; std::string SelectedThemePreset = ""; // Currently selected theme preset (empty = custom/user theme) }; diff --git a/src/Menu/HomePageRenderer.cpp b/src/Menu/HomePageRenderer.cpp index df8dde47ec..85d3fde71e 100644 --- a/src/Menu/HomePageRenderer.cpp +++ b/src/Menu/HomePageRenderer.cpp @@ -66,7 +66,7 @@ void HomePageRenderer::RenderWelcomeSection() ImVec2 windowSize = ImGui::GetWindowSize(); auto versionStr = Util::GetFormattedVersion(Plugin::VERSION); auto expectedTag = std::format("v{}", versionStr); - std::string titleWithVersion = Plugin::BUILD_DESCRIBE == expectedTag ? std::format("Welcome to Community Shaders {}", versionStr) : std::format("Welcome to Community Shaders {} [{}]", versionStr, Plugin::BUILD_DESCRIBE); + std::string titleWithVersion = Plugin::BUILD_DESCRIBE == expectedTag ? std::format("Welcome to Open Shaders {}", versionStr) : std::format("Welcome to Open Shaders {} [{}]", versionStr, Plugin::BUILD_DESCRIBE); ImVec2 titleSize = ImGui::CalcTextSize(titleWithVersion.c_str()); ImGui::SetCursorPosX((windowSize.x - titleSize.x) * 0.5f); ImGui::Text("%s", titleWithVersion.c_str()); @@ -83,7 +83,7 @@ void HomePageRenderer::RenderWelcomeSection() // Intro text - centered const char* introText = - "Community Shaders provides advanced graphics enhancements for Skyrim.\n" + "Open Shaders is a fork of Community Shaders providing advanced graphics enhancements for Skyrim.\n" "This comprehensive collection of features brings modern rendering techniques\n" "to enhance your visual experience."; ImVec2 introSize = ImGui::CalcTextSize(introText); @@ -92,56 +92,6 @@ void HomePageRenderer::RenderWelcomeSection() ImGui::Spacing(); - // Discord banner - centered with proper error checking - auto menu = Menu::GetSingleton(); - bool discordIconAvailable = false; - - // Check if menu exists, has icons, and Discord icon is loaded - if (menu && menu->uiIcons.discord.texture != nullptr && - menu->uiIcons.discord.size.x > 0 && menu->uiIcons.discord.size.y > 0) { - discordIconAvailable = true; - } - - if (discordIconAvailable) { - // Calculate scaled icon size based on window width, with min/max constraints - ImVec2 originalSize = ImVec2(menu->uiIcons.discord.size.x, menu->uiIcons.discord.size.y); - - // Compute width based on window size with constraints and padding (handles very small windows) - float ratioWidth = windowSize.x * DISCORD_BANNER_TARGET_WIDTH_RATIO; - float aspectRatio = originalSize.y / originalSize.x; - float maxAllowed = std::max(1.0f, windowSize.x - DISCORD_BANNER_PADDING_MARGIN); - float upperBound = std::min(DISCORD_BANNER_MAX_WIDTH, maxAllowed); - float lowerBound = std::min(DISCORD_BANNER_MIN_WIDTH, upperBound); - float targetWidth = std::clamp(ratioWidth, lowerBound, upperBound); - - ImVec2 iconSize = ImVec2(targetWidth, targetWidth * aspectRatio); - ImGui::SetCursorPosX((windowSize.x - iconSize.x) * 0.5f); - - // Push style to remove border - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); // Transparent background - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.1f, 0.1f, 0.1f, 0.3f)); // Subtle hover - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.2f, 0.2f, 0.2f, 0.5f)); // Subtle click - - if (ImGui::ImageButton("##DiscordButton", menu->uiIcons.discord.texture, iconSize)) { - ShellExecuteA(NULL, "open", DISCORD_URL, NULL, NULL, SW_SHOWNORMAL); - } - - // Pop the style changes - ImGui::PopStyleColor(3); - ImGui::PopStyleVar(); - - Util::AddTooltip("Join Community Shaders Discord Server"); - } else { - // Fallback button when Discord icon is not available - float buttonWidth = DISCORD_BANNER_MIN_WIDTH * scale; - ImGui::SetCursorPosX((windowSize.x - buttonWidth) * 0.5f); - if (ImGui::Button("Join Discord Server", ImVec2(buttonWidth, 0))) { - ShellExecuteA(NULL, "open", DISCORD_URL, NULL, NULL, SW_SHOWNORMAL); - } - Util::AddTooltip("Join Community Shaders Discord Server"); - } - ImGui::PopStyleVar(); } @@ -153,26 +103,22 @@ void HomePageRenderer::RenderQuickLinksSection() ImGui::SetCursorPosX((windowSize.x - titleSize.x) * 0.5f); ImGui::Text("Quick Links"); - ImGui::Columns(4, nullptr, false); + // The Nexus button points at upstream Community Shaders until the + // Open Shaders Nexus mod page exists; swap when ready. + ImGui::Columns(3, nullptr, false); - // External links in a row if (ImGui::Button("Nexus Mods", ImVec2(-1, 0))) { - ShellExecuteA(NULL, "open", "https://www.nexusmods.com/skyrimspecialedition/mods/86492", NULL, NULL, SW_SHOWNORMAL); + ShellExecuteA(NULL, "open", "https://www.nexusmods.com/skyrimspecialedition/mods/180419", NULL, NULL, SW_SHOWNORMAL); } ImGui::NextColumn(); if (ImGui::Button("GitHub", ImVec2(-1, 0))) { - ShellExecuteA(NULL, "open", "https://github.com/doodlum/skyrim-community-shaders", NULL, NULL, SW_SHOWNORMAL); - } - - ImGui::NextColumn(); - if (ImGui::Button("Wiki", ImVec2(-1, 0))) { - ShellExecuteA(NULL, "open", "https://modding.wiki/en/skyrim/developers/community-shaders", NULL, NULL, SW_SHOWNORMAL); + ShellExecuteA(NULL, "open", "https://github.com/alandtse/open-shaders", NULL, NULL, SW_SHOWNORMAL); } ImGui::NextColumn(); if (ImGui::Button("Developer Wiki", ImVec2(-1, 0))) { - ShellExecuteA(NULL, "open", "https://github.com/doodlum/skyrim-community-shaders/wiki", NULL, NULL, SW_SHOWNORMAL); + ShellExecuteA(NULL, "open", "https://github.com/alandtse/open-shaders/wiki", NULL, NULL, SW_SHOWNORMAL); } ImGui::Columns(1); @@ -188,11 +134,14 @@ void HomePageRenderer::RenderFAQSection() ImGui::Separator(); // FAQ items with collapsible headers - if (ImGui::CollapsingHeader("What is Community Shaders?")) { + if (ImGui::CollapsingHeader("What is Open Shaders?")) { ImGui::TextWrapped( - "Community Shaders is a comprehensive graphics enhancement framework for Skyrim that " - "provides advanced lighting, materials, and visual effects. It's designed to be modular, " - "allowing you to enable only the features you want while maintaining good performance."); + "Open Shaders is a fork of Community Shaders that ships features the upstream project " + "has not yet released. Both projects are comprehensive graphics enhancement frameworks " + "for Skyrim that provide advanced lighting, materials, and visual effects. They're " + "designed to be modular, letting you enable only the features you want while " + "maintaining good performance. This fork preserves the upstream runtime layout so user " + "settings and themes are compatible."); } if (ImGui::CollapsingHeader("How do I configure features?")) { @@ -223,34 +172,36 @@ void HomePageRenderer::RenderFAQSection() "tab also includes upscaling options that can improve performance."); } - if (ImGui::CollapsingHeader("Is Community Shaders compatible with ENB?")) { + if (ImGui::CollapsingHeader("Is Open Shaders compatible with ENB?")) { ImGui::TextWrapped( - "No, Community Shaders is not compatible with ENB. Community Shaders will automatically " - "disable itself if ENB is detected."); + "No, Open Shaders (like upstream Community Shaders) is not compatible with ENB. The " + "plugin will automatically disable itself if ENB is detected."); } if (ImGui::CollapsingHeader("The menu hotkey isn't working!")) { ImGui::TextWrapped( - "By default, Community Shaders uses the END key to open this menu. If your keyboard " + "By default, Open Shaders uses the END key to open this menu. If your keyboard " "doesn't have an END key or it's not working, you can change it in the General > Keybindings tab. " "You can also edit the hotkey in the JSON configuration files."); } - if (ImGui::CollapsingHeader("I would like to help develop Community Shaders.")) { + if (ImGui::CollapsingHeader("I would like to help develop Open Shaders.")) { ImGui::TextWrapped( - "We're always looking for talented developers to join the team! Check out our GitHub wiki " - "for contribution guidelines and join our Discord server to connect with the development team. " - "Whether you're interested in shader programming, C++ development, or documentation, there's " - "always something to contribute."); + "Open Shaders is open source. Check out the upstream GitHub wiki for contribution " + "guidelines on the shared architecture; open issues and PRs against this repository for " + "fork-specific work, or against upstream Community Shaders for changes that benefit both " + "projects. Whether you're interested in shader programming, C++ development, or " + "documentation, there's always something to contribute."); } - if (ImGui::CollapsingHeader("Is Community Shaders open source?")) { + if (ImGui::CollapsingHeader("Is Open Shaders open source?")) { ImGui::TextWrapped( - "Yes! Community Shaders is completely open source and available on GitHub. You can view " - "the source code, report issues, suggest features, and contribute to the project. " - "The project is licensed under GPL, ensuring it remains free and open for everyone." - " Branding materials and assets (icons, nexus branding, typography, etc) are not covered by the GPL Licence." - " Any included assets may not be used without explicit permission."); + "Yes! Open Shaders is completely open source and available on GitHub, as is upstream " + "Community Shaders. You can view the source code, report issues, suggest features, and " + "contribute to either project. Both are licensed under GPL, ensuring they remain free and " + "open for everyone. Branding materials and assets (icons, Nexus branding, typography, etc.) " + "are not covered by the GPL Licence. Any included assets may not be used without explicit " + "permission."); } } @@ -444,7 +395,7 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() // Version text - two lines, both centered (reduced spacing between lines) const char* versionLine1 = "This appears to be a new install, update, or"; - const char* versionLine2 = "reinstallation of Community Shaders."; + const char* versionLine2 = "reinstallation of Open Shaders."; centerText(versionLine1); ImGui::Text("%s", versionLine1); diff --git a/src/Menu/HomePageRenderer.h b/src/Menu/HomePageRenderer.h index cc36dd1fd7..c4cee59dbe 100644 --- a/src/Menu/HomePageRenderer.h +++ b/src/Menu/HomePageRenderer.h @@ -7,7 +7,6 @@ class HomePageRenderer { public: // Constants - static constexpr const char* DISCORD_URL = "https://discord.com/invite/nkrQybAsyy"; static constexpr float TITLE_FONT_SCALE = 2.0f; static constexpr float HOTKEY_TEXT_SCALE = 1.6f; static constexpr float HOTKEY_TEXT_SCALE_CAPTURING = 2.0f; @@ -22,12 +21,6 @@ class HomePageRenderer static constexpr float DIALOG_CORNER_ROUNDING = 6.0f; static constexpr float DIALOG_LINE_TIGHTEN = 3.0f; - // Discord banner scaling constants - static constexpr float DISCORD_BANNER_TARGET_WIDTH_RATIO = 0.85f; - static constexpr float DISCORD_BANNER_MIN_WIDTH = 150.0f; - static constexpr float DISCORD_BANNER_MAX_WIDTH = 1200.0f; - static constexpr float DISCORD_BANNER_PADDING_MARGIN = 40.0f; - static void RenderHomePage(); // First-time setup management diff --git a/src/Menu/IconLoader.cpp b/src/Menu/IconLoader.cpp index d12f3ce69a..8c37c472e1 100644 --- a/src/Menu/IconLoader.cpp +++ b/src/Menu/IconLoader.cpp @@ -104,7 +104,6 @@ namespace Util::IconLoader { std::string(iconFolder) + "\\delete.png", &menu->uiIcons.deleteSettings.texture, &menu->uiIcons.deleteSettings.size }, { logoPath, &menu->uiIcons.logo.texture, &menu->uiIcons.logo.size }, { std::string(iconFolder) + "\\restore-settings.png", &menu->uiIcons.featureSettingRevert.texture, &menu->uiIcons.featureSettingRevert.size }, - { std::string(iconFolder) + "\\discord.png", &menu->uiIcons.discord.texture, &menu->uiIcons.discord.size }, { std::string(iconFolder) + "\\apply-to-game.png", &menu->uiIcons.applyToGame.texture, &menu->uiIcons.applyToGame.size }, { std::string(iconFolder) + "\\pause.png", &menu->uiIcons.pauseTime.texture, &menu->uiIcons.pauseTime.size }, { std::string(iconFolder) + "\\undo.png", &menu->uiIcons.undo.texture, &menu->uiIcons.undo.size }, diff --git a/src/Menu/MenuHeaderRenderer.cpp b/src/Menu/MenuHeaderRenderer.cpp index 27ec1a1703..0caec58d0a 100644 --- a/src/Menu/MenuHeaderRenderer.cpp +++ b/src/Menu/MenuHeaderRenderer.cpp @@ -25,7 +25,7 @@ void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShow auto versionStr = Util::GetFormattedVersion(Plugin::VERSION); auto expectedTag = std::format("v{}", versionStr); - auto title = Plugin::BUILD_DESCRIBE == expectedTag ? std::format("Community Shaders {}", versionStr) : std::format("Community Shaders {} [{}]", versionStr, Plugin::BUILD_DESCRIBE); + auto title = Plugin::BUILD_DESCRIBE == expectedTag ? std::format("Open Shaders {}", versionStr) : std::format("Open Shaders {} [{}]", versionStr, Plugin::BUILD_DESCRIBE); auto actionIcons = BuildActionIcons(canShowIcons, uiIcons); if (isDocked) { diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index 01d5f619e9..ed50ae1a3b 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -29,7 +29,11 @@ namespace std::unordered_map s_windowOverlapAlpha; constexpr ImGuiWindowFlags SKIP_WINDOW_FLAGS = ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoMove; - constexpr const char* MAIN_WINDOW_PREFIX = "Community Shaders"; + // Prefix-match against the display title set by Menu.cpp ("Open Shaders "). + // Must track that title — if the display name changes the prefix here must + // change too, or IsMainWindow() will silently start returning false and + // overlay-alpha logic loses its anchor. + constexpr const char* MAIN_WINDOW_PREFIX = "Open Shaders"; bool IsMainWindow(ImGuiWindow* win) { return win->Name && strncmp(win->Name, MAIN_WINDOW_PREFIX, strlen(MAIN_WINDOW_PREFIX)) == 0; } diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 4caf0a1aa0..61c85958c3 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -431,7 +431,7 @@ void SettingsTabRenderer::RenderBehaviorTab() globals::menu->pendingIconReload = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Uses monochrome version of the Community Shaders logo"); + ImGui::Text("Uses monochrome version of the logo"); } ImGui::Unindent(); } @@ -443,7 +443,7 @@ void SettingsTabRenderer::RenderBehaviorTab() ImGui::Checkbox("Center Header Title", &themeSettings.CenterHeader); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Centers the Community Shaders title and logo in the header title bar"); + ImGui::Text("Centers the title and logo in the header title bar"); } ImGui::Checkbox("Auto-hide Feature List", &globals::menu->GetSettings().AutoHideFeatureList); diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index ea9ed43208..866810bb20 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -12,7 +12,7 @@ using json = nlohmann::json; /** - * @brief Manages hot-swappable theme system for Community Shaders menu + * @brief Manages hot-swappable theme system for the Open Shaders menu * * THEME JSON SCHEMA: * ================== diff --git a/src/SettingsOverrideManager.h b/src/SettingsOverrideManager.h index ae17a1b899..cca9a56ba0 100644 --- a/src/SettingsOverrideManager.h +++ b/src/SettingsOverrideManager.h @@ -10,7 +10,7 @@ using json = nlohmann::json; /** - * @brief Manages layered JSON override system for Community Shaders features + * @brief Manages layered JSON override system for Open Shaders features * * This class handles discovery and application of feature setting overrides * from external mod files without requiring changes to existing feature code. diff --git a/src/ShaderCache.cpp b/src/ShaderCache.cpp index 38bd325651..a8d27b4b4c 100644 --- a/src/ShaderCache.cpp +++ b/src/ShaderCache.cpp @@ -1794,7 +1794,7 @@ namespace SIE // VR only shaders // Disable BSImagespaceShaderCopyDepthBuffer since we don't have it REed and it causes issues with cache and upscaling - // https://github.com/doodlum/skyrim-community-shaders/issues/1552 + // https://github.com/community-shaders/skyrim-community-shaders/issues/1552 // { "BSImagespaceShaderCopyDepthBuffer", RE::ImageSpaceManager::GetCurrentIndex(ISCopyDepthBuffer) }, // { "BSImagespaceShaderCopyDepthBuffer_DR", RE::ImageSpaceManager::GetCurrentIndex(ISCopyDepthBuffer_DR) }, // { "BSImagespaceShaderCopyDepthBufferTargetSize", RE::ImageSpaceManager::GetCurrentIndex(ISCopyDepthBufferTargetSize) }, @@ -2980,7 +2980,7 @@ namespace SIE // still reads high briefly, which would otherwise underflow uint64_t (logs as ~2^64-1). const uint64_t total = compilationSet.totalTasks.load(std::memory_order_relaxed); const uint64_t done = compilationSet.completedTasks.load(std::memory_order_relaxed) + - compilationSet.failedTasks.load(std::memory_order_relaxed); + compilationSet.failedTasks.load(std::memory_order_relaxed); // This task has already finished running, but Complete(task) has not yet updated the counters. // Include the current task in the local progress snapshot so the logged remaining count is accurate. const uint64_t doneIncludingCurrent = (done < total) ? (done + 1) : total; diff --git a/src/State.h b/src/State.h index 2dd811d445..fdddd770fa 100644 --- a/src/State.h +++ b/src/State.h @@ -102,7 +102,7 @@ class State std::vector>* GetDefines(); /* - * Whether a_type is currently enabled in Community Shaders + * Whether a_type is currently enabled in Open Shaders * * @param a_type The type of shader to check * @return Whether the shader has been enabled. @@ -110,7 +110,7 @@ class State bool ShaderEnabled(const RE::BSShader::Type a_type); /* - * Whether a_shader is currently enabled in Community Shaders + * Whether a_shader is currently enabled in Open Shaders * * @param a_shader The shader to check * @return Whether the shader has been enabled. diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index f1d7f6ac98..72c3f7a509 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -106,7 +106,7 @@ void MessageHandler(SKSE::MessagingInterface::Message* message) { for (auto it = errors.begin(); it != errors.end(); ++it) { auto& errorMessage = *it; - RE::DebugMessageBox(std::format("Community Shaders\n{}, will disable all hooks and features", errorMessage).c_str()); + RE::DebugMessageBox(std::format("Open Shaders\n{}, will disable all hooks and features", errorMessage).c_str()); } if (errors.empty()) { diff --git a/tools/feature_version_audit.py b/tools/feature_version_audit.py index 3c3f50f74a..a5df777b37 100644 --- a/tools/feature_version_audit.py +++ b/tools/feature_version_audit.py @@ -787,7 +787,7 @@ def get_feature_key(feature_dir, feature_meta_map): commit_link = "" if bump_commit: author_str = f" ({bump_author})" if bump_author else "" - commit_link = f"[link](https://github.com/doodlum/skyrim-community-shaders/commit/{bump_commit}){author_str}" + commit_link = f"[link](https://github.com/community-shaders/skyrim-community-shaders/commit/{bump_commit}){author_str}" def bold(val): return f"**{val}**" if is_attention and val != '' and val != '-' else val @@ -875,7 +875,7 @@ def boldmeta(val, missing=missing): nexus_link = f"[Nexus]({meta['mod_link']})" if meta and meta['mod_link'] else ("**Missing metadata**" if not meta else "") author = get_commit_author(commit) if commit else None author_str = f" ({author})" if author else "" - commit_link = f"[link](https://github.com/doodlum/skyrim-community-shaders/commit/{commit}){author_str}" if commit else "" + commit_link = f"[link](https://github.com/community-shaders/skyrim-community-shaders/commit/{commit}){author_str}" if commit else "" lines.append(f"| {boldmeta(name)} | {boldmeta(ver)} | {nexus_link} | {commit_link} |") return lines From 5117702cbc92be4d99f008292348ff041578766c Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 20 May 2026 01:37:01 -0700 Subject: [PATCH 02/24] ci: reconcile dev after hotfix-staging releases (#21) Co-authored-by: Claude Opus 4.7 --- .claude/CLAUDE.md | 9 +- .github/workflows/auto-rebase-prs.yaml | 109 ++++++++++++++++++++++++ .github/workflows/release-semantic.yaml | 103 +++++++++++++++++----- 3 files changed, 199 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/auto-rebase-prs.yaml diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 08f5eb4993..f6d340354d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -478,7 +478,12 @@ Conventional commits drive semantic-release. `feat:` triggers a minor bump, `fix **Default branch for PRs is `dev`.** Feature work, fixes, and refactors all land there via normal PRs. `main` is updated only through the release workflows — never PR a feature branch directly into `main`. -**Branch lineage invariant:** `main` is always an ancestor of `dev`, and `dev` is always an ancestor of `main` after a release reconciles. The `Release: Semantic Version` workflow's `ff_target` promotion mode and the auto dev-FF-reconcile keep this invariant — do not break it by force-pushing or merging shared branches manually. +**Branch lineage invariant:** after every release reconciles, `main` is an ancestor of `dev`, so every tag on `main` is reachable from `dev`. The `Release: Semantic Version` workflow keeps this invariant in two ways depending on the promotion source: + +- **dev → main promotion** (minor/major): main fast-forwards to the dev SHA, semantic-release appends a `chore(release):` commit on top, then dev fast-forwards to absorb that commit. No history rewrites on either branch. +- **hotfix-staging → main promotion** (current-line patch): main fast-forwards to the hotfix-staging SHA, semantic-release appends the `chore(release):` commit, then dev is **rebase-reconciled** onto the new main. `git rebase` drops dev's originals of the cherry-picked fixes (patch-id match) and replays any unique dev work on top. This is the only place the workflow force-pushes (`--force-with-lease`) — it is intentional and load-bearing. + +After a hotfix release, open PRs targeting `dev` are auto-rebased by the `Auto-rebase open PRs` workflow (a thin wrapper around `peter-evans/rebase@v3`). PRs from forks need "Allow edits by maintainers" enabled or the action silently skips them; drafts and PRs labeled `no-auto-rebase` are also excluded. The workflow's job summary reports the rebased count and lists the buckets PRs can fall into; conflict-skipped PRs need a manual `git rebase origin/dev` by the author. **Patch flow (current line _or_ older line, same staging mechanism):** @@ -497,7 +502,7 @@ Conventional commits drive semantic-release. `feat:` triggers a minor bump, `fix **Things agents should not do without explicit user direction:** -- Force-push or rebase `main`, `dev`, or any `hotfix/*` branch. +- Force-push or rebase `main`, `dev`, or any `hotfix/*` branch. (The release workflow's rebase-reconcile of `dev` after a hotfix-staging promotion is the one sanctioned exception; humans should not replicate it manually unless the workflow's remediation block explicitly instructs them to.) - Manually create tags matching `v*` (semantic-release owns these). - Bump `CMakeLists.txt`'s `VERSION` field outside the release workflow. - PR a feature branch directly into `main`. diff --git a/.github/workflows/auto-rebase-prs.yaml b/.github/workflows/auto-rebase-prs.yaml new file mode 100644 index 0000000000..e6dc3e9920 --- /dev/null +++ b/.github/workflows/auto-rebase-prs.yaml @@ -0,0 +1,109 @@ +name: "Auto-rebase open PRs" + +# Rebase open PRs against `dev` whenever it moves. Thin wrapper over +# peter-evans/rebase@v3. +# +# Forks: pushing to a fork PR head requires (a) a user PAT belonging to +# a maintainer of this repo and (b) the PR author having "Allow edits +# by maintainers" checked. The action silently skips PRs missing either, +# and skips conflicts. +# +# Token: RELEASE_PAT (classic PAT, `repo` + `workflow`). `workflow` is +# required because rebased PRs may include `.github/workflows/` diffs. + +on: + push: + branches: [dev] + workflow_dispatch: + inputs: + pr_number: + description: "Optional: rebase just this PR number (the action accepts head as `:`; we resolve via gh api). Leave empty to rebase all open PRs against dev." + required: false + default: "" + +permissions: + contents: write + pull-requests: write + +concurrency: + # Serialize so two back-to-back pushes to dev don't race and produce + # interleaved force-pushes on the same PR. + group: auto-rebase-prs + cancel-in-progress: false + +jobs: + rebase: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ secrets.RELEASE_PAT }} + + - name: Resolve single-PR head spec (if pr_number given) + id: resolve_head + if: inputs.pr_number != '' + env: + GH_TOKEN: ${{ secrets.RELEASE_PAT }} + PR_NUMBER: ${{ inputs.pr_number }} + run: | + set -euo pipefail + # Guard against rebasing a closed PR or one targeting + # a non-dev base (e.g. a hotfix staging branch). + PR_JSON=$(gh pr view "${PR_NUMBER}" --repo '${{ github.repository }}' \ + --json state,baseRefName,headRepositoryOwner,headRefName) + PR_STATE=$(echo "${PR_JSON}" | jq -r '.state') + PR_BASE=$(echo "${PR_JSON}" | jq -r '.baseRefName') + if [[ "${PR_STATE}" != "OPEN" ]]; then + echo "::warning::PR #${PR_NUMBER} is ${PR_STATE}, not OPEN — skipping." + echo "head=" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [[ "${PR_BASE}" != "dev" ]]; then + echo "::warning::PR #${PR_NUMBER} targets '${PR_BASE}', not 'dev' — skipping to avoid rebasing the wrong base." + echo "head=" >> "$GITHUB_OUTPUT" + exit 0 + fi + HEAD_REF=$(echo "${PR_JSON}" | jq -r '"\(.headRepositoryOwner.login):\(.headRefName)"') + echo "head=${HEAD_REF}" >> "$GITHUB_OUTPUT" + + - name: Rebase open PRs against dev + id: rebase + # Single-PR dispatches without a valid head fall through to + # the all-PRs default; this guard keeps that from happening. + if: inputs.pr_number == '' || steps.resolve_head.outputs.head != '' + uses: peter-evans/rebase@v3 + with: + token: ${{ secrets.RELEASE_PAT }} + base: dev + head: ${{ steps.resolve_head.outputs.head }} + exclude-drafts: true + exclude-labels: | + no-auto-rebase + + - name: Summarize + env: + REBASED: ${{ steps.rebase.outputs.rebased-count }} + run: | + { + echo "## Auto-rebase summary" + echo "" + echo "- PRs rebased: \`${REBASED:-0}\`" + echo "- Base: \`dev\`" + if [[ -n '${{ inputs.pr_number }}' ]]; then + echo "- Targeted single PR: #${{ inputs.pr_number }} (head \`${{ steps.resolve_head.outputs.head }}\`)" + fi + echo "" + echo "PRs not rebased fall into one of three buckets:" + echo "- Already up-to-date with \`dev\` (no-op)" + echo "- Draft or labeled \`no-auto-rebase\` (excluded)" + echo "- Conflict during rebase, OR fork without maintainer-edit access (action silently skips)" + echo "" + echo "PRs in the conflict bucket need a manual rebase by the author:" + echo '```bash' + echo "git fetch origin && git rebase origin/dev" + echo "# resolve conflicts, git rebase --continue" + echo "git push --force-with-lease" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release-semantic.yaml b/.github/workflows/release-semantic.yaml index ccc18240e6..57f1551780 100644 --- a/.github/workflows/release-semantic.yaml +++ b/.github/workflows/release-semantic.yaml @@ -1,16 +1,13 @@ name: "Release: Semantic Version" -# Dispatch on: -# - dev → cuts vX.Y.Z-rc.N (RC channel) -# - main → cuts vX.Y.Z stable (current line) -# - hotfix/X.Y.x → cuts vX.Y.Z+1 on the X.Y maintenance channel -# (only valid when main has shipped a higher minor/major) +# Dispatch on: dev (RC) | main (stable, current line) | hotfix/X.Y.x +# (older line, only valid when main has shipped a newer minor/major). # -# Promotion mode (dev → main): -# Dispatch on main with `ff_target` set to a dev SHA. The workflow -# fast-forwards main to that SHA, runs semantic-release to cut stable, -# then fast-forwards dev to absorb the chore(release) commit. One click, -# no PR, no merge commits, no force pushes. +# Promotion (ff_target on main): FF main to ff_target, run semantic- +# release, then reconcile dev. Dev-source reconciles FF-only; hotfix- +# staging source rebases dev onto the new main so the release tag +# enters dev's ancestry — the only force-push this workflow performs. +# `auto-rebase-prs.yaml` then rebases open PRs targeting dev. on: workflow_dispatch: @@ -71,9 +68,13 @@ jobs: echo "::error::main is not an ancestor of ff_target ${FF_TARGET} — fast-forward is not possible." exit 1 fi - # Identify the source branch for the promotion. dev → reconcile - # dev afterward; hotfix/* → patch-id dedup handles it at the - # next dev→main promotion, no reconcile. + # Identify the source branch for the promotion. + # dev source → FF-reconcile dev afterward (no rewrite). + # hotfix-staging → rebase-reconcile dev afterward so the + # release commit + tag enter dev's + # ancestry. Force-with-lease push; `git + # rebase` drops patch-id duplicates of + # the cherry-picks already on main. SOURCE="" if git merge-base --is-ancestor "${FF_TARGET}" origin/dev; then SOURCE="dev" @@ -153,17 +154,19 @@ jobs: # to dev during the release, exit with a warning instead of # rewriting their work. if ! git merge-base --is-ancestor origin/dev origin/main; then - echo "::warning::dev moved during promotion; FF-reconcile skipped." { echo "" - echo "## Dev reconcile skipped" + echo "## ⚠️ Dev reconcile skipped — dev moved during promotion" echo "Dev was pushed to during the release. To reconcile manually:" echo "" echo '```bash' echo "git fetch && git checkout dev && git merge --ff-only origin/main && git push" echo '```' } >> "$GITHUB_STEP_SUMMARY" - exit 0 + # Fail loud: tag is on main but dev isn't reconciled + # (same invariant break as the hotfix-staging path). + echo "::error::dev moved during promotion — dev is unreconciled. See job summary for manual remediation." + exit 1 fi NEW_MAIN=$(git rev-parse origin/main) REMOTE="https://x-access-token:${TOKEN}@github.com/${{ github.repository }}.git" @@ -171,12 +174,72 @@ jobs: git push --force-with-lease="dev:${CURRENT_DEV}" "${REMOTE}" "${NEW_MAIN}:refs/heads/dev" echo "::notice::Reconciled dev to main ($NEW_MAIN)." - - name: Note dev reconcile skipped (hotfix-staging source) + - name: Rebase-reconcile dev with release commit (promotion mode, hotfix-staging source) if: ${{ inputs.ff_target != '' && steps.semantic.outputs.new_release_published == 'true' && steps.validate.outputs.source != 'dev' && steps.validate.outputs.source != '' }} + env: + TOKEN: ${{ secrets.RELEASE_PAT }} run: | + set -euo pipefail + git fetch origin main dev + NEW_MAIN=$(git rev-parse origin/main) + CURRENT_DEV=$(git rev-parse origin/dev) + + # FF-only won't work: dev and main diverged. Rebase + # preserves linear history; patch-id dedup drops the + # cherry-picked-fix commits automatically. + git checkout -B dev "${CURRENT_DEV}" + if ! git rebase "${NEW_MAIN}"; then + git rebase --abort || true + { + echo "" + echo "## ⚠️ Dev reconcile FAILED — manual intervention required" + echo "" + echo "Rebasing \`dev\` onto the new \`main\` (\`$(git rev-parse --short=9 "${NEW_MAIN}")\`) hit a conflict." + echo "The hotfix release is live on \`main\`, but \`dev\` has NOT absorbed it." + echo "Until reconciled, the next RC cut on \`dev\` will compute the wrong version floor." + echo "" + echo "**Recover by running locally:**" + echo "" + echo '```bash' + echo "git fetch origin" + echo "git checkout dev && git reset --hard origin/dev" + echo "git rebase origin/main # resolve conflicts, git rebase --continue" + echo "git push --force-with-lease origin dev" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + echo "::error::Dev rebase-reconcile failed — see job summary for remediation." + exit 1 + fi + + REMOTE="https://x-access-token:${TOKEN}@github.com/${{ github.repository }}.git" + # --force-with-lease guards against a concurrent push to + # dev: if someone landed a commit during the release, + # abort instead of overwriting their work. + if ! git push --force-with-lease="dev:${CURRENT_DEV}" "${REMOTE}" "HEAD:refs/heads/dev"; then + { + echo "" + echo "## ⚠️ Dev push rejected (lease check failed)" + echo "" + echo "Dev was pushed to during the release. Reconcile manually:" + echo "" + echo '```bash' + echo "git fetch origin && git checkout dev && git reset --hard origin/dev" + echo "git rebase origin/main && git push --force-with-lease origin dev" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + # Fail loud: tag is on main but dev's history doesn't + # contain it, so the next RC would compute its floor + # and changelog against the wrong prior tag. + echo "::error::Dev moved during release — dev is unreconciled. See job summary for manual remediation." + exit 1 + fi + { echo "" - echo "## Dev reconcile not applicable" - echo "Promotion source was \`${{ steps.validate.outputs.source }}\` (hotfix staging branch). The cherry-picked commits on main have different SHAs than their originals on dev — semantic-release's patch-id-dedup will collapse the duplicates at the next dev→main promotion, so no FF-reconcile is needed (and would not be possible)." + echo "## Dev reconciled (rebase-forward)" + echo "- Source: hotfix-staging \`${{ steps.validate.outputs.source }}\`" + echo "- New main: \`$(git rev-parse --short=9 "${NEW_MAIN}")\`" + echo "- Dev rebased linearly onto new main; identical-patch commits auto-dropped by \`git rebase\`." + echo "- Open PRs against dev are auto-rebased by the \`Auto-rebase open PRs\` workflow." } >> "$GITHUB_STEP_SUMMARY" - echo "::notice::Hotfix-staging promotion — dev reconcile not applicable." + echo "::notice::Reconciled dev onto main via rebase (linear history preserved)." From 20a38a1256126ea82f193168ed11d123cc9e3bce Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 20 May 2026 01:37:17 -0700 Subject: [PATCH 03/24] feat(tracy): bump to upstream master snapshot (#26) Co-authored-by: Claude Opus 4.7 --- cmake/ports/tracy/portfile.cmake | 5 +++-- cmake/ports/tracy/vcpkg.json | 2 +- vcpkg.json | 7 +++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cmake/ports/tracy/portfile.cmake b/cmake/ports/tracy/portfile.cmake index 3f4e9f69ba..50b883cd65 100644 --- a/cmake/ports/tracy/portfile.cmake +++ b/cmake/ports/tracy/portfile.cmake @@ -1,8 +1,8 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO wolfpld/tracy - REF "v${VERSION}" - SHA512 18c0c589a1d97d0760958c8ab00ba2135bc602fd359d48445b5d8ed76e5b08742d818bb8f835b599149030f455e553a92db86fb7bae049b47820e4401cf9f935 + REF 14a1a3227e046e7bee3abed406c7faab61de3620 + SHA512 113b2dd35d75edeb1358514bf857bc694c048fb7b2604a22e8fcc7efa63d342a6c7cbf02162073d2ee8690821e0f80f95be6270416777645d9092d5508e76372 HEAD_REF master PATCHES build-tools.patch @@ -33,6 +33,7 @@ vcpkg_cmake_configure( OPTIONS -DDOWNLOAD_CAPSTONE=OFF -DLEGACY=ON + -DTRACY_ENABLE=ON ${FEATURE_OPTIONS} OPTIONS_RELEASE ${TOOLS_OPTIONS} diff --git a/cmake/ports/tracy/vcpkg.json b/cmake/ports/tracy/vcpkg.json index d4d1cd51f7..ae3a2a33e9 100644 --- a/cmake/ports/tracy/vcpkg.json +++ b/cmake/ports/tracy/vcpkg.json @@ -1,6 +1,6 @@ { "name": "tracy", - "version": "0.13.1", + "version-string": "0.13.3-14a1a322", "description": "A real time, nanosecond resolution, remote telemetry, hybrid frame and sampling profiler for games and other applications.", "homepage": "https://github.com/wolfpld/tracy", "license": "BSD-3-Clause", diff --git a/vcpkg.json b/vcpkg.json index 6a8d6f35c9..1d6bd1ab78 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -27,7 +27,10 @@ "pystring", "renderdoc", "stb", - "tracy", + { + "name": "tracy", + "features": ["on-demand"] + }, "unordered-dense", "xbyak" ], @@ -55,7 +58,7 @@ }, { "name": "tracy", - "version": "0.13.1" + "version-string": "0.13.3-14a1a322" }, { "name": "directx-headers", From 4f23e365f4b68eb0c6b957710d4ca41d460afefc Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 21 May 2026 00:19:18 -0700 Subject: [PATCH 04/24] ci: address CodeQL security findings (cache poisoning + permissions) (#31) --- .../setup-build-environment/action.yaml | 30 ++++++++++++++++++- .github/workflows/maint-todo-issues.yaml | 8 +++++ .github/workflows/nexus-upload.yaml | 7 +++++ .github/workflows/pr-wip.yaml | 7 +++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-build-environment/action.yaml b/.github/actions/setup-build-environment/action.yaml index 12f5614df3..c28030d799 100644 --- a/.github/actions/setup-build-environment/action.yaml +++ b/.github/actions/setup-build-environment/action.yaml @@ -96,7 +96,25 @@ runs: with: vcpkgJsonGlob: vcpkg.json - - name: Cache CMake build output + # Cache strategy is split by event trust level to avoid the + # pull_request_target cache-poisoning class of bug (CodeQL rule + # `actions/cache-poisoning/poisonable-step`): + # + # - Trusted events (push to dev/main, workflow_dispatch, workflow_call + # from a release pipeline): use the combined `actions/cache` which + # restores at this step and auto-saves at job-end. Cache writes only + # happen from these trusted contexts. + # - Untrusted events (pull_request_target, pull_request): use the + # restore-only sub-action. PR builds still benefit from caches + # warmed by trusted runs but cannot themselves write back, so a + # malicious PR cannot poison a cache that a later default-branch + # run would consume. + # + # Both steps use the same key / restore-keys so cache hits cross the + # trust boundary in the allowed direction (trusted-saved → untrusted-restore). + + - name: Cache CMake build output (trusted, restore + save) + if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target' uses: actions/cache@v5 with: path: ${{ inputs.build-dir }} @@ -105,6 +123,16 @@ runs: ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}-${{ inputs.cache-key-suffix }}-${{ github.event.inputs.cache-key-suffix || 'default' }}- ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}-${{ inputs.cache-key-suffix }}- + - name: Restore CMake build cache (untrusted PR, restore-only) + if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' + uses: actions/cache/restore@v5 + with: + path: ${{ inputs.build-dir }} + key: ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}-${{ inputs.cache-key-suffix }}-${{ github.event.inputs.cache-key-suffix || 'default' }}-${{ hashFiles('.gitmodules', 'extern/**', 'CMakePresets.json', 'vcpkg.json', 'vcpkg-configuration.json') }} + restore-keys: | + ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}-${{ inputs.cache-key-suffix }}-${{ github.event.inputs.cache-key-suffix || 'default' }}- + ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}-${{ inputs.cache-key-suffix }}- + - name: Remove stale CMake cache if configuration changed shell: pwsh run: | diff --git a/.github/workflows/maint-todo-issues.yaml b/.github/workflows/maint-todo-issues.yaml index d6b27c2d73..83a6a03271 100644 --- a/.github/workflows/maint-todo-issues.yaml +++ b/.github/workflows/maint-todo-issues.yaml @@ -10,6 +10,14 @@ on: MANUAL_BASE_REF: description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here" required: false + +# Least-privilege scope: alstr/todo-to-issue-action creates issues from TODO/ +# FIXME comments it finds in the diff. `issues: write` for the create call, +# `contents: read` for the checkout step. +permissions: + contents: read + issues: write + jobs: build: runs-on: "ubuntu-latest" diff --git a/.github/workflows/nexus-upload.yaml b/.github/workflows/nexus-upload.yaml index 13641ae496..4da6b588e9 100644 --- a/.github/workflows/nexus-upload.yaml +++ b/.github/workflows/nexus-upload.yaml @@ -86,6 +86,13 @@ on: UNEX_APIKEY: required: false +# Least-privilege for the whole workflow: every job here only reads from +# GitHub (release listing via `gh api`, asset download via `gh release +# download`) and writes only to Nexus via the UNEX_* secrets. No mutation +# of repo state. +permissions: + contents: read + jobs: prepare-nexus-matrix: runs-on: ubuntu-latest diff --git a/.github/workflows/pr-wip.yaml b/.github/workflows/pr-wip.yaml index 269228969c..2da55f67fd 100644 --- a/.github/workflows/pr-wip.yaml +++ b/.github/workflows/pr-wip.yaml @@ -3,6 +3,13 @@ on: pull_request: types: [opened, synchronize, reopened, edited] +# Least-privilege scope for the wip/action: it sets a commit status on the PR +# head (statuses: write) and reads PR metadata (pull-requests: read). No other +# repo writes happen here. +permissions: + statuses: write + pull-requests: read + jobs: wip: runs-on: ubuntu-latest From 2ce7b61068493befb273fa4ad6e857c5e2b1dcb7 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 21 May 2026 00:19:43 -0700 Subject: [PATCH 05/24] ci: debounce auto-rebase by 30 minutes (#29) --- .github/workflows/auto-rebase-prs.yaml | 47 +++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/.github/workflows/auto-rebase-prs.yaml b/.github/workflows/auto-rebase-prs.yaml index e6dc3e9920..dcbf96fedd 100644 --- a/.github/workflows/auto-rebase-prs.yaml +++ b/.github/workflows/auto-rebase-prs.yaml @@ -25,15 +25,52 @@ permissions: contents: write pull-requests: write -concurrency: - # Serialize so two back-to-back pushes to dev don't race and produce - # interleaved force-pushes on the same PR. - group: auto-rebase-prs - cancel-in-progress: false +# Two jobs by design: +# +# 1. `debounce` — sleeps for 30 minutes on push events. Has +# `cancel-in-progress: true` so a fresh push during the sleep +# kills the still-sleeping run and the new push starts its own +# timer. Workflow-dispatch runs skip the sleep entirely. +# 2. `rebase` — depends on `debounce` and runs `peter-evans/rebase`. +# Has `cancel-in-progress: false` so once it starts force-pushing +# PR heads, it can't be interrupted mid-flight by a subsequent +# push. The downside is a small window (the rebase duration) +# where an additional push has to queue rather than supersede — +# but interrupting force-pushes mid-rebase leaves PRs in an +# inconsistent half-rebased state, which is worse. +# +# End state: one rebase pass per quiet 30-min window, no half-done +# rebases. jobs: + debounce: + runs-on: ubuntu-latest + # The debounce job alone is cancellable — a new push restarts + # its 30-minute timer, dropping the prior queued run. + concurrency: + group: auto-rebase-prs-debounce + cancel-in-progress: true + steps: + - name: Debounce dev-push storms + # Only debounce when fired by a push to dev. Manual dispatches + # run immediately (the next job has no dependency wait when + # this step is skipped). Coalesces a burst of PR merges into + # a single rebase pass, keeping PR-build CI load proportional + # to landings/half-hour rather than landings × open-PR-count. + if: github.event_name == 'push' + run: sleep 1800 + rebase: runs-on: ubuntu-latest + needs: debounce + # The rebase job is NOT cancellable — once peter-evans/rebase + # starts force-pushing PR heads, interrupting it mid-flight could + # leave some PRs rebased and others not, or interrupt a push + # mid-operation. A subsequent dev push queues behind this run + # instead. + concurrency: + group: auto-rebase-prs-rebase + cancel-in-progress: false steps: - name: Checkout uses: actions/checkout@v6 From fe7195ff1d69b2c99972dbcd45e58a6200949783 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 21 May 2026 00:21:36 -0700 Subject: [PATCH 06/24] ci: add merge-based upstream sync workflow (#30) --- .gitattributes | 54 +++++++ .github/workflows/maint-upstream-sync.yaml | 180 +++++++++++++++++++++ docs/development/README.md | 1 + docs/development/upstream-sync.md | 90 +++++++++++ 4 files changed, 325 insertions(+) create mode 100644 .github/workflows/maint-upstream-sync.yaml create mode 100644 docs/development/upstream-sync.md diff --git a/.gitattributes b/.gitattributes index 72467a3279..4a8802abb2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,57 @@ # Ensure patch files always use LF line endings *.patch text eol=lf + +# ----------------------------------------------------------------------------- +# Fork-owned paths — `merge=ours` during 3-way merges +# ----------------------------------------------------------------------------- +# These files are policy- or branding-owned by the fork. When the scheduled +# upstream-merge-sync workflow merges upstream/dev into dev, the `ours` driver +# preserves the fork's version of each path verbatim, ignoring whatever +# upstream did to the same file. This is what prevents the silent-revert +# class of bug (upstream cherry-picks our commit, then later deletes the +# file in a follow-up: a vanilla merge would honor the delete; `merge=ours` +# discards it). +# +# Prerequisites for the driver to fire: +# 1. Each clone (including CI runners) must define the `ours` driver: +# git config merge.ours.driver true +# The scheduled sync workflow does this in a setup step. Local +# contributors who run `git merge upstream/dev` by hand should run +# the same command once. See docs/development/upstream-sync.md. +# 2. The merge must be a true 3-way merge (i.e., `git merge`, not +# `git rebase` — rebase replays patches via `git apply` and never +# invokes merge drivers). +# +# Important: `merge=ours` fires on ANY 3-way merge that touches these +# paths, not just the scheduled upstream sync. If a PR is landed via +# GitHub's "Create a merge commit" button (or via a local `git merge` +# between fork branches), and the incoming branch changed one of these +# files, those changes are silently discarded. **Edits to fork-owned +# paths must land via squash-merge or rebase-merge** so the diff lands +# as a plain commit on dev rather than through a merge-driver-affected +# merge commit. The default PR merge methods on `dev` are configured +# to squash/rebase only to enforce this. +# +# When adding a path here, also add a one-line note in +# docs/development/upstream-sync.md explaining why the fork owns it. If a +# path stops being fork-owned (e.g., we converge with upstream's behavior), +# remove the entry so upstream's improvements flow back in. +# ----------------------------------------------------------------------------- + +# CI workflows the fork owns end-to-end (auto-rebase machinery, the +# release pipeline calibrated to RELEASE_PAT rather than upstream's +# GitHub App setup, hotfix flow, nexus upload, etc.). +.github/workflows/auto-rebase-prs.yaml merge=ours +.github/workflows/release-semantic.yaml merge=ours +.github/workflows/release-hotfix.yaml merge=ours +.github/workflows/nexus-upload.yaml merge=ours +.github/workflows/maint-cleanup-releases.yaml merge=ours +.github/workflows/maint-upstream-sync.yaml merge=ours + +# Semantic-release config — fork has independent versioning and may +# diverge from upstream's release rules. +.releaserc merge=ours + +# Branding-owned: rebrand commit established these as fork-specific. +README.md merge=ours diff --git a/.github/workflows/maint-upstream-sync.yaml b/.github/workflows/maint-upstream-sync.yaml new file mode 100644 index 0000000000..c970a5eb40 --- /dev/null +++ b/.github/workflows/maint-upstream-sync.yaml @@ -0,0 +1,180 @@ +name: "Maint: Sync upstream/dev" + +# Scheduled merge sync from community-shaders/skyrim-community-shaders into our +# dev. Combined with the `merge=ours` attributes in .gitattributes (see the +# `Fork-owned paths` section there), this pulls upstream's code changes while +# leaving the fork's CI/branding files untouched. +# +# Why merge and not rebase: rebase replays patches via `git apply` and never +# invokes merge drivers. Worse, when upstream cherry-picks one of our commits +# and then later modifies the same file, rebase's patch-id detection skips +# our original commit as "already applied" — and upstream's follow-up edit +# then silently lands as a regression. A 3-way merge consults both branches +# at every path and lets the `merge=ours` driver fire for fork-owned files, +# eliminating that whole class of bug. +# +# Versioning: we deliberately do NOT skip this merge commit in +# semantic-release's commit analysis. The DAG walk picks up upstream's +# `feat:`/`fix:` commits transitively, so our version reflects everything +# we actually ship to users (including upstream fixes that arrived via this +# merge). + +on: + schedule: + # Monday 08:00 UTC. Weekly cadence keeps drift small without + # spamming PR rebases — pair this with auto-rebase-prs.yaml's + # debounce (PR #29) so the post-sync PR rebases coalesce. + - cron: "0 8 * * 1" + workflow_dispatch: + inputs: + dry_run: + description: "Dry run — fetch and merge locally, but don't push" + type: boolean + default: false + +permissions: + # `contents: write` is the only scope we need — the merge push to + # `dev` is the only mutation. No PR API calls, no issue API calls. + contents: write + +concurrency: + # Serialize — two overlapping syncs would race on the dev push. + group: maint-upstream-sync + cancel-in-progress: false + +jobs: + sync: + runs-on: ubuntu-latest + # Guard against downstream forks running this on schedule: they + # don't have RELEASE_PAT, don't want to merge community-shaders/dev + # into their own dev, and the resulting failure would spam their + # Actions tab with red runs. Other maint workflows in this repo + # use the same pattern. + if: github.repository == 'alandtse/open-shaders' + steps: + - name: Checkout dev + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: dev + # RELEASE_PAT so the merge push can bypass branch protection + # and trigger downstream workflows (auto-rebase of open PRs). + token: ${{ secrets.RELEASE_PAT }} + + - name: Configure git identity + merge driver + # The `ours` driver is referenced by .gitattributes and is not + # built into git; it must be defined locally. We define it as + # a no-op (driver = true) which means "keep the version on the + # current branch unchanged" — the standard recipe. + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git config merge.ours.driver true + + - name: Fetch upstream/dev + id: fetch + run: | + git remote add upstream https://github.com/community-shaders/skyrim-community-shaders.git || \ + git remote set-url upstream https://github.com/community-shaders/skyrim-community-shaders.git + git fetch upstream dev + UPSTREAM_SHA=$(git rev-parse upstream/dev) + echo "upstream_sha=${UPSTREAM_SHA}" >> "$GITHUB_OUTPUT" + echo "upstream_short=${UPSTREAM_SHA:0:9}" >> "$GITHUB_OUTPUT" + + # If we're already at or ahead of upstream's tip, exit early. + if git merge-base --is-ancestor "${UPSTREAM_SHA}" HEAD; then + echo "already_synced=true" >> "$GITHUB_OUTPUT" + else + echo "already_synced=false" >> "$GITHUB_OUTPUT" + fi + + - name: Already synced — nothing to do + if: steps.fetch.outputs.already_synced == 'true' + run: | + { + echo "## ✅ Already synced" + echo "" + echo "Upstream tip \`${{ steps.fetch.outputs.upstream_short }}\` is already an ancestor of \`dev\`. Nothing to merge." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Merge upstream/dev + id: merge + if: steps.fetch.outputs.already_synced != 'true' + run: | + set +e + git merge --no-ff --no-edit \ + -m "chore(sync): merge upstream/dev as of ${{ steps.fetch.outputs.upstream_short }}" \ + upstream/dev + merge_status=$? + set -e + + if [[ $merge_status -ne 0 ]]; then + # The merge=ours driver should have handled everything + # in the fork-owned list. A conflict here means upstream + # touched a non-fork-owned file in a way that genuinely + # collides with our changes — needs human eyes. + echo "::error::Upstream sync conflicted on non-fork-owned paths." + { + echo "## ❌ Conflict during sync" + echo "" + echo "Upstream tip: \`${{ steps.fetch.outputs.upstream_short }}\`" + echo "" + echo "Conflicted files (resolve manually, then push):" + echo "" + echo '```' + git diff --name-only --diff-filter=U + echo '```' + echo "" + echo "Resolution: clone, run the same merge locally, resolve, push." + echo "" + echo '```bash' + echo "git fetch upstream dev" + echo "git config merge.ours.driver true" + echo "git merge --no-ff --no-edit \\" + echo " -m 'chore(sync): merge upstream/dev as of ${{ steps.fetch.outputs.upstream_short }}' \\" + echo " upstream/dev" + echo "# resolve conflicts, git add, git commit" + echo "git push origin dev" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + git merge --abort + exit 1 + fi + + - name: Summarize merge + if: steps.fetch.outputs.already_synced != 'true' && success() + run: | + { + echo "## ✅ Merged upstream/dev" + echo "" + echo "Upstream tip: \`${{ steps.fetch.outputs.upstream_short }}\`" + echo "" + echo "Files changed:" + echo "" + echo '```' + git diff --stat HEAD~1..HEAD | head -40 + echo '```' + echo "" + echo "Commits brought in from upstream:" + echo "" + echo '```' + # The merge commit has two parents: HEAD^1 = previous + # fork tip, HEAD^2 = upstream tip. The range + # `HEAD^1..HEAD^2` enumerates the upstream commits + # reachable through the merge that weren't already in + # our history — exactly what we want to surface. + git log --oneline 'HEAD^1..HEAD^2' | head -30 + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Push to origin/dev + if: steps.fetch.outputs.already_synced != 'true' && inputs.dry_run != true + run: git push origin dev + + - name: Dry-run notice + if: steps.fetch.outputs.already_synced != 'true' && inputs.dry_run == true + run: | + { + echo "" + echo "**Dry run** — merge completed locally but not pushed." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/docs/development/README.md b/docs/development/README.md index 92cc9e7fff..4dc103e5e4 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -4,6 +4,7 @@ - **[VSCode Setup](./vscode-setup.md)** - IDE configuration, extensions, and auto-deploy - **[Shader Workflow](./shader-workflow.md)** - Fast shader iteration and deployment +- **[Upstream Sync](./upstream-sync.md)** - How Open Shaders merges with upstream community-shaders ## Quick Links diff --git a/docs/development/upstream-sync.md b/docs/development/upstream-sync.md new file mode 100644 index 0000000000..63058dbb53 --- /dev/null +++ b/docs/development/upstream-sync.md @@ -0,0 +1,90 @@ +# Upstream Sync + +How Open Shaders stays current with upstream `community-shaders/skyrim-community-shaders` without losing the fork-specific CI policy, branding, and feature setup. + +## Mechanism + +Upstream syncs land as **merge commits** on `dev`, never as rebases. The scheduled `Maint: Sync upstream/dev` workflow runs a three-way merge of `upstream/dev` into our `dev` and pushes the result. Two pieces make this safe: + +1. **`.gitattributes` with `merge=ours` entries** for every file the fork owns end-to-end (CI workflows, `.releaserc`, README). During the merge, git's `ours` driver keeps the fork's version of those paths verbatim — upstream's changes to them are discarded without surfacing as conflicts. +2. **The `ours` merge driver itself** must be defined locally. `merge=ours` in `.gitattributes` _references_ a driver but doesn't define one. The sync workflow defines it as a no-op (`git config merge.ours.driver true`). **Local contributors who run the merge by hand must run the same command once per clone** — see [Setup](#setup-once-per-clone) below. + +## Why merge, not rebase + +We previously used `git rebase upstream/dev`. It silently regressed fork-owned files on every sync. The mechanism: upstream cherry-picks one of our fork commits → we rebase later → git detects the duplicate via patch-id and skips our commit as "already applied" → an upstream follow-up that deletes or edits the same file then applies cleanly. End result: the fork loses content with zero merge conflicts and zero log noise. The rebase reports success. + +A 3-way merge consults both sides at every path independently of patch-id. Our `merge=ours` driver fires for fork-owned paths; everything else gets a real 3-way merge. Either a clean result or a visible conflict — nothing silent. + +See `.gitattributes` for the current fork-owned list. When a file should join or leave that list, update both the attributes and the comment block above the list explaining why. + +## Setup (once per clone) + +```bash +git config merge.ours.driver true +``` + +That's it. The `ours` driver is intentionally not built into git (for security — driver definitions can run arbitrary commands), so each clone declares it locally. The sync CI does this in its own setup step. + +If you've already run an upstream merge without this config, git would have raised an "unknown merge driver 'ours'" warning and used the default 3-way merge for those files, potentially producing surprising conflicts in fork-owned paths. Re-run with the driver configured and the conflicts disappear. + +## Running a sync manually + +```bash +# Make sure local dev matches origin/dev before merging upstream. +# A stale local dev would either reject the push as non-fast-forward +# or have you re-resolving conflicts already resolved by a prior run. +git fetch origin dev upstream/dev +git switch dev +git reset --hard origin/dev + +git merge --no-ff --no-edit \ + -m "chore(sync): merge upstream/dev as of $(git rev-parse --short upstream/dev)" \ + upstream/dev +# resolve conflicts (only in non-fork-owned paths), then: +git push origin dev +``` + +The scheduled workflow does exactly this on Monday 08:00 UTC. Manual dispatch via `gh workflow run "Maint: Sync upstream/dev"` is available for urgent syncs and accepts a `dry_run` flag. + +## Versioning and changelog interaction + +The merge commit's message is `chore(sync): merge upstream/dev as of `. semantic-release sees it as a `chore` and doesn't release on the commit itself. + +**However**, semantic-release's default DAG walk follows the merge into upstream's commit history. Upstream's `feat:` and `fix:` commits that came in via the merge are visible to the commit analyzer and **do** drive version bumps in our release stream. This is deliberate: the fork's version reflects everything actually shipped to users, including upstream fixes that arrived via merge. + +When upstream cherry-picks one of our commits and that cherry-pick lands in our merge, both copies of the same logical change get walked. Version-wise this is harmless (one release can only bump once at the max severity). Changelog-wise it produces a duplicate entry. If this becomes annoying, the fix is a `writerOpts.transform` in `.releaserc` that dedupes by patch-id — not done preemptively. + +## When the workflow halts + +A real conflict (in a file _not_ on the fork-owned list) means upstream and the fork have both meaningfully changed the same code. Examples we'd expect: + +- Both forks bump the same feature INI version. +- We add a method to a class upstream also modified. +- We rename a function upstream also renamed. + +The workflow `git merge --abort`s, posts the conflicted file list to the workflow summary, and exits non-zero. Resolution is manual: clone, run the same merge locally, resolve, push. + +If you do recurring syncs, enabling `git rerere` is worth the one-time setup — it caches each conflict resolution and replays it the next time the same hunks conflict. Per-clone setting, not repo-wide: + +```bash +git config rerere.enabled true +git config rerere.autoupdate true +``` + +Caches live in `.git/rr-cache/` and aren't pushed, so each maintainer builds their own. CI runners start with empty caches every run and benefit nothing from rerere — only the maintainers doing the merges locally see the time savings. + +## Inspecting what a sync did + +Each sync workflow run leaves a summary on the run page with: + +- Upstream tip SHA +- `git diff --stat` of files changed +- `git log --oneline` of commits brought in + +For deeper inspection after a push: + +```bash +# all changes since the last sync merge +git log --first-parent --merges --grep='chore(sync)' -1 # find the merge commit +git diff ~1.. # changes the merge introduced +``` From ec2db80e65c8572d41153abcfd167c7eff08e66b Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 21 May 2026 23:11:02 -0700 Subject: [PATCH 07/24] fix(screenshot): bounds-check staging texture mapped region (#25) Co-authored-by: Claude Opus 4.7 --- src/Features/ScreenshotFeature.cpp | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Features/ScreenshotFeature.cpp b/src/Features/ScreenshotFeature.cpp index fa84c3b39e..8f6d445293 100644 --- a/src/Features/ScreenshotFeature.cpp +++ b/src/Features/ScreenshotFeature.cpp @@ -10,6 +10,7 @@ #include "Utils/FileSystem.h" #include #include +#include #include #include #include @@ -66,6 +67,10 @@ namespace if (FAILED(context->Map(stagingTexture, 0, D3D11_MAP_READ, 0, &mapped))) { return false; } + if (!mapped.pData || mapped.RowPitch == 0) { + context->Unmap(stagingTexture, 0); + return false; + } const HRESULT initHr = image.Initialize2D(format, width, height, 1, 1); if (FAILED(initHr)) { @@ -79,12 +84,32 @@ namespace return false; } + // Driver-mapped region can be smaller than height * mapped.RowPitch + // (alignment quirks, partial mappings). Cap by mapped.DepthPitch and + // clamp each row's copy to whichever of source/dest pitches is smaller - + // stepping past either side hits unmapped memory and the worker crashes + // inside rep movsb (see crash 2026-05-19). + const size_t bytesPerRow = std::min(destImage->rowPitch, mapped.RowPitch); + const size_t mappedDepth = mapped.DepthPitch != 0 ? mapped.DepthPitch : + mapped.RowPitch * destImage->height; + const size_t maxRowsBySize = mapped.RowPitch > 0 ? (mappedDepth / mapped.RowPitch) : 0; + const size_t rowsToCopy = std::min(destImage->height, maxRowsBySize); + auto* destPixels = image.GetPixels(); - for (size_t row = 0; row < destImage->height; ++row) { + const auto* srcPixels = static_cast(mapped.pData); + + // Initialize2D leaves the pixel buffer uninitialized. If the mapped + // region is short (rowsToCopy < height) or narrow (bytesPerRow < + // destImage->rowPitch), the gaps would otherwise read back as + // undefined memory and SaveToWICFile would encode garbage. Zero-fill + // up front so any uncopied bytes encode as deterministic black. + std::memset(destPixels, 0, image.GetPixelsSize()); + + for (size_t row = 0; row < rowsToCopy; ++row) { memcpy( destPixels + row * destImage->rowPitch, - static_cast(mapped.pData) + row * mapped.RowPitch, - destImage->rowPitch); + srcPixels + row * mapped.RowPitch, + bytesPerRow); } context->Unmap(stagingTexture, 0); From 62ff479af79c6eb4b6786c89cd8708262753788d Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 21 May 2026 23:11:19 -0700 Subject: [PATCH 08/24] feat(remote-control): add MCP server core feature (#27) Co-authored-by: Claude Opus 4.7 --- .gitmodules | 3 + CMakeLists.txt | 2 + cmake/cpp-mcp.cmake | 107 ++ extern/cpp-mcp | 1 + features/Remote Control/CORE | 0 .../Shaders/Features/RemoteControl.ini | 2 + src/Feature.cpp | 2 + src/Features/RemoteControl.cpp | 1024 +++++++++++++++++ src/Features/RemoteControl.h | 118 ++ src/Globals.cpp | 2 + src/Globals.h | 2 + src/State.cpp | 2 + src/State.h | 4 + 13 files changed, 1269 insertions(+) create mode 100644 cmake/cpp-mcp.cmake create mode 160000 extern/cpp-mcp create mode 100644 features/Remote Control/CORE create mode 100644 features/Remote Control/Shaders/Features/RemoteControl.ini create mode 100644 src/Features/RemoteControl.cpp create mode 100644 src/Features/RemoteControl.h diff --git a/.gitmodules b/.gitmodules index db0fe8c6f7..7bed141dcb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,3 +8,6 @@ path = extern/FidelityFX-SDK url = https://github.com/alandtse/FidelityFX-SDK-DX11 branch = optiscaler-build +[submodule "extern/cpp-mcp"] + path = extern/cpp-mcp + url = https://github.com/hkr04/cpp-mcp.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 2350126edc..d225f0eac3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/cmake/Streamline) find_path(DETOURS_INCLUDE_DIRS "detours/detours.h") find_library(DETOURS_LIBRARY detours REQUIRED) include(FidelityFX-SDK) +include(cpp-mcp) target_compile_definitions( ${PROJECT_NAME} @@ -144,6 +145,7 @@ target_link_libraries( unordered_dense::unordered_dense efsw::efsw Tracy::TracyClient + cpp-mcp Streamline d3d12.lib Microsoft::DirectX-Headers diff --git a/cmake/cpp-mcp.cmake b/cmake/cpp-mcp.cmake new file mode 100644 index 0000000000..e218957891 --- /dev/null +++ b/cmake/cpp-mcp.cmake @@ -0,0 +1,107 @@ +# Build cpp-mcp (https://github.com/hkr04/cpp-mcp) from its vendored +# submodule as a static library target. Upstream has no install rules +# (PR #12 still open), so we drive its build ourselves — same pattern +# we use for FidelityFX-SDK and Streamline. +# +# Only the server-side translation units are compiled; the bundled +# stdio/SSE *client* implementations are intentionally omitted because +# we are exclusively a server. +# +# nlohmann_json ABI alignment: +# cpp-mcp vendors nlohmann_json 3.11.3 in extern/cpp-mcp/common/json.hpp, +# while vcpkg ships 3.12.0. Both versions wrap their public API in an +# ABI-versioned inline namespace (`nlohmann::json_abi_v3_11_3` vs +# `nlohmann::json_abi_v3_12_0`), so even though both files share the +# same include guard (INCLUDE_NLOHMANN_JSON_HPP_), the symbol names +# differ. If cpp-mcp's own TUs picked up the vendored copy and our +# consumers picked up vcpkg's, `mcp::server::set_capabilities` and +# `register_tool` would link-fail (LNK2001) with two different +# ABI-tagged signatures. +# +# Fix: patch mcp_message.h at configure time to use +# `#include ` instead of `#include "json.hpp"`. +# The patched copy is written to a build-tree mirror; the submodule +# stays clean. Both cpp-mcp's own compilation and every consumer +# then resolve to vcpkg's 3.12.0 → single ABI namespace, symbols +# match, linker happy. + +set(CPP_MCP_DIR "${CMAKE_SOURCE_DIR}/extern/cpp-mcp") +set(CPP_MCP_PATCHED_INC "${CMAKE_BINARY_DIR}/cpp-mcp-patched/include") + +if(NOT EXISTS "${CPP_MCP_DIR}/src/mcp_server.cpp") + message(FATAL_ERROR + "cpp-mcp submodule missing. Run:\n" + " git submodule update --init --recursive extern/cpp-mcp") +endif() + +find_package(Threads REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) + +# Patch mcp_message.h to use vcpkg nlohmann_json (see header comment). +# All other cpp-mcp headers are copied verbatim into the patched mirror +# so they live next to the patched header and find each other. +file(MAKE_DIRECTORY "${CPP_MCP_PATCHED_INC}") +file(GLOB _cpp_mcp_headers CONFIGURE_DEPENDS "${CPP_MCP_DIR}/include/*.h") +foreach(_hdr IN LISTS _cpp_mcp_headers) + get_filename_component(_name "${_hdr}" NAME) + file(READ "${_hdr}" _content) + if(_name STREQUAL "mcp_message.h") + # Fail fast if the expected include vanishes upstream — otherwise the + # ABI mismatch would silently come back and only surface as an LNK2001 + # well into the link step. + string(FIND "${_content}" "#include \"json.hpp\"" _json_inc_pos) + if(_json_inc_pos EQUAL -1) + message(FATAL_ERROR + "cpp-mcp: expected `#include \"json.hpp\"` in mcp_message.h " + "but did not find it. Upstream may have changed the include; " + "review cmake/cpp-mcp.cmake and adjust the patch (see header " + "comment for the ABI-alignment rationale).") + endif() + string(REPLACE + "#include \"json.hpp\"" + "#include " + _content "${_content}") + endif() + file(WRITE "${CPP_MCP_PATCHED_INC}/${_name}" "${_content}") +endforeach() + +add_library(cpp-mcp STATIC + "${CPP_MCP_DIR}/src/mcp_message.cpp" + "${CPP_MCP_DIR}/src/mcp_resource.cpp" + "${CPP_MCP_DIR}/src/mcp_server.cpp" + "${CPP_MCP_DIR}/src/mcp_tool.cpp" +) + +# Order matters: patched mirror first so its mcp_message.h wins over the +# submodule's. `common/` is still needed for httplib.h (no ABI issue +# there — it's not shared with any vcpkg dep). +target_include_directories(cpp-mcp + PUBLIC "${CPP_MCP_PATCHED_INC}" + "${CPP_MCP_DIR}/common" +) + +target_compile_features(cpp-mcp PUBLIC cxx_std_17) + +target_compile_definitions(cpp-mcp PUBLIC + MCP_MAX_SESSIONS=10 + MCP_SESSION_TIMEOUT=30 + # cpp-mcp's vendored cpp-httplib pulls in . Skyrim/CLib's + # transitive defaults to the legacy , which + # conflicts (redefinition of sockaddr, WSAData, etc.). Tell Windows + # headers to skip the legacy winsock so winsock2.h is the only one + # in the build. PUBLIC so it propagates to every TU that links + # cpp-mcp (including the PCH compilation of CommunityShaders). + _WINSOCKAPI_ +) + +target_link_libraries(cpp-mcp PUBLIC + Threads::Threads + nlohmann_json::nlohmann_json +) + +if(MSVC) + target_compile_options(cpp-mcp PRIVATE /utf-8 /bigobj /W0) + target_compile_definitions(cpp-mcp PRIVATE _CRT_SECURE_NO_WARNINGS) +endif() + +set_target_properties(cpp-mcp PROPERTIES FOLDER "extern") diff --git a/extern/cpp-mcp b/extern/cpp-mcp new file mode 160000 index 0000000000..a0eb22c98d --- /dev/null +++ b/extern/cpp-mcp @@ -0,0 +1 @@ +Subproject commit a0eb22c98dbd8ce8b3ef69679310c1a038905c08 diff --git a/features/Remote Control/CORE b/features/Remote Control/CORE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/features/Remote Control/Shaders/Features/RemoteControl.ini b/features/Remote Control/Shaders/Features/RemoteControl.ini new file mode 100644 index 0000000000..000b60a568 --- /dev/null +++ b/features/Remote Control/Shaders/Features/RemoteControl.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 diff --git a/src/Feature.cpp b/src/Feature.cpp index c6c0df6fe8..5264802f5a 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -18,6 +18,7 @@ #include "Features/LightLimitFix.h" #include "Features/LinearLighting.h" #include "Features/PerformanceOverlay.h" +#include "Features/RemoteControl.h" #include "Features/RenderDoc.h" #include "Features/ScreenSpaceGI.h" #include "Features/ScreenSpaceShadows.h" @@ -240,6 +241,7 @@ const std::vector& Feature::GetFeatureList() &globals::features::extendedTranslucency, &globals::features::upscaling, &globals::features::renderDoc, + &globals::features::remoteControl, &globals::features::weatherEditor, &globals::features::screenshotFeature, &globals::features::linearLighting, diff --git a/src/Features/RemoteControl.cpp b/src/Features/RemoteControl.cpp new file mode 100644 index 0000000000..bda02d65e2 --- /dev/null +++ b/src/Features/RemoteControl.cpp @@ -0,0 +1,1024 @@ +// Remote Control feature: hosts an in-process Model Context Protocol (MCP) +// server inside CommunityShaders.dll, letting AI assistants query and mutate +// runtime state for A/B testing. Off by default and loopback-only. +// +// Transport: HTTP+SSE (Streamable HTTP, MCP 2025-03-26). +// Endpoint: http://:/mcp (modern, single endpoint) +// http://:/sse (legacy SSE, also exposed by cpp-mcp) + +#include "Features/RemoteControl.h" + +#include "Features/PerformanceOverlay/ABTesting/ABTesting.h" +#include "Features/RenderDoc.h" +#include "Features/ScreenshotFeature.h" +#include "Globals.h" +#include "State.h" + +#include +#include + +#include +#include +#include +#include +#include + +// cpp-mcp headers. Kept inside the .cpp only so the vendored httplib/json +// in extern/cpp-mcp/common don't leak into other translation units. +#include "mcp_server.h" +#include "mcp_tool.h" + +namespace +{ + // The control endpoint is intentionally loopback-only — exposing it off-host + // would let any networked client toggle features and dispatch captures. + // Only accept literal loopback IPs: on Windows the hosts file (or a + // hijacked resolver) can map "localhost" to a routable address, which would + // silently break the loopback-only contract. + bool IsLoopbackAddress(const std::string& host) + { + return host == "127.0.0.1" || host == "::1"; + } + + void NormalizeBindAddress(std::string& host) + { + if (!IsLoopbackAddress(host)) { + logger::warn("Remote Control: non-loopback bindAddress '{}' rejected; forcing 127.0.0.1", host); + host = "127.0.0.1"; + } + } + + int ClampPort(int port) + { + return std::clamp(port, 1024, 65535); + } +} + +RemoteControl* RemoteControl::GetSingleton() +{ + return &globals::features::remoteControl; +} + +RemoteControl::RemoteControl() = default; + +RemoteControl::~RemoteControl() +{ + StopServer(); +} + +void RemoteControl::Load() +{ + // Settings have already been read in by the time Load() fires. + if (settings.enabled) { + StartServer(); + } +} + +void RemoteControl::Reset() +{ + // No per-frame state to reset. +} + +void RemoteControl::LoadSettings(json& o_json) +{ + settings.enabled = o_json.value("enabled", false); + settings.port = ClampPort(o_json.value("port", 8910)); + settings.bindAddress = o_json.value("bindAddress", std::string("127.0.0.1")); + NormalizeBindAddress(settings.bindAddress); +} + +void RemoteControl::SaveSettings(json& o_json) +{ + o_json["enabled"] = settings.enabled; + o_json["port"] = settings.port; + o_json["bindAddress"] = settings.bindAddress; +} + +void RemoteControl::RestoreDefaultSettings() +{ + settings = Settings{}; +} + +void RemoteControl::DrawSettings() +{ + ImGui::TextWrapped( + "Exposes Community Shaders over Model Context Protocol (MCP) so AI " + "assistants such as Claude Code can drive A/B testing, toggle " + "features, and trigger captures. Off by default. The endpoint is " + "loopback-only — any non-loopback bind address is rejected at load " + "and bind time."); + ImGui::Spacing(); + + const bool wasEnabled = settings.enabled; + if (ImGui::Checkbox("Enable MCP server", &settings.enabled)) { + if (settings.enabled && !wasEnabled) { + StartServer(); + } else if (!settings.enabled && wasEnabled) { + StopServer(); + } + } + + // Port + bind address can only be edited while the server is stopped. + ImGui::BeginDisabled(IsRunning()); + ImGui::InputInt("Port", &settings.port); + settings.port = std::clamp(settings.port, 1024, 65535); + ImGui::InputText("Bind address", &settings.bindAddress); + ImGui::EndDisabled(); + if (IsRunning()) { + ImGui::SameLine(); + ImGui::TextDisabled("(stop the server to edit)"); + } + + if (!lastError.empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.4f, 1.0f), + "Server error: %s", lastError.c_str()); + } + + if (IsRunning()) { + ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.5f, 1.0f), + "Listening on %s:%d", settings.bindAddress.c_str(), activePort); + } + + ImGui::Separator(); + ImGui::Text("Connect from an MCP client (Claude Code, Cursor, etc.):"); + + if (ImGui::Button("Copy MCP client config to clipboard")) { + ImGui::SetClipboardText(BuildClientConfig().c_str()); + } + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Paste the JSON into your Claude Code settings under " + "\"mcpServers\". Other MCP hosts (Cursor, Continue) accept the " + "same shape."); + } + + if (ImGui::CollapsingHeader("Config preview")) { + const auto preview = BuildClientConfig(); + ImGui::PushTextWrapPos(); + ImGui::TextUnformatted(preview.c_str()); + ImGui::PopTextWrapPos(); + } + + ImGui::Separator(); + DrawClientsTable(); +} + +void RemoteControl::DrawClientsTable() +{ + // Snapshot under the lock to keep the listener-thread updates from + // racing the draw. The snapshot is small (a handful of sessions at most). + std::vector rows; + { + std::lock_guard lock(sessionMutex); + rows.reserve(sessions.size()); + for (const auto& [_, info] : sessions) { + rows.push_back(info); + } + } + + const std::string headerLabel = std::format("Connected clients ({})##rc-clients", rows.size()); + if (!ImGui::CollapsingHeader(headerLabel.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { + return; + } + + if (!IsRunning()) { + ImGui::TextDisabled("Server not running."); + return; + } + if (rows.empty()) { + ImGui::TextDisabled( + "No clients connected. Paste the config above into " + "your MCP host and run a tool to populate this table."); + return; + } + + constexpr ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | + ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable | + ImGuiTableFlags_SortMulti | ImGuiTableFlags_ScrollY; + + enum ColumnId : ImGuiID + { + ColSession = 0, + ColConnected, + ColIdle, + ColRequests, + ColLastTool, + }; + + if (ImGui::BeginTable("##rc-clients-table", 5, flags, ImVec2(0.0f, 120.0f))) { + ImGui::TableSetupColumn("Session", ImGuiTableColumnFlags_DefaultSort, 0.0f, ColSession); + ImGui::TableSetupColumn("Connected", 0, 0.0f, ColConnected); + ImGui::TableSetupColumn("Idle for", 0, 0.0f, ColIdle); + ImGui::TableSetupColumn("Requests", 0, 0.0f, ColRequests); + ImGui::TableSetupColumn("Last tool", 0, 0.0f, ColLastTool); + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableHeadersRow(); + + if (auto* sortSpecs = ImGui::TableGetSortSpecs(); sortSpecs && sortSpecs->SpecsCount > 0) { + std::sort(rows.begin(), rows.end(), + [&](const SessionInfo& a, const SessionInfo& b) { + for (int i = 0; i < sortSpecs->SpecsCount; ++i) { + const auto& spec = sortSpecs->Specs[i]; + const bool desc = spec.SortDirection == ImGuiSortDirection_Descending; + int cmp = 0; + switch (static_cast(spec.ColumnUserID)) { + case ColSession: + cmp = a.id.compare(b.id); + break; + case ColConnected: + cmp = a.connected < b.connected ? -1 : (a.connected > b.connected ? 1 : 0); + break; + case ColIdle: + cmp = a.lastSeen < b.lastSeen ? -1 : (a.lastSeen > b.lastSeen ? 1 : 0); + break; + case ColRequests: + cmp = a.requestCount < b.requestCount ? -1 : (a.requestCount > b.requestCount ? 1 : 0); + break; + case ColLastTool: + cmp = a.lastTool.compare(b.lastTool); + break; + } + if (cmp != 0) { + return desc ? cmp > 0 : cmp < 0; + } + } + return false; + }); + } + + const auto now = std::chrono::system_clock::now(); + const auto formatRelative = [](std::chrono::seconds sec) -> std::string { + const auto s = sec.count(); + if (s < 60) { + return std::format("{}s ago", s); + } + if (s < 3600) { + return std::format("{}m {}s ago", s / 60, s % 60); + } + return std::format("{}h {}m ago", s / 3600, (s % 3600) / 60); + }; + + for (const auto& info : rows) { + ImGui::TableNextRow(); + const auto connectedSec = std::chrono::duration_cast(now - info.connected); + const auto idleSec = std::chrono::duration_cast(now - info.lastSeen); + + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(info.id.c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(formatRelative(connectedSec).c_str()); + ImGui::TableSetColumnIndex(2); + ImGui::TextUnformatted(formatRelative(idleSec).c_str()); + ImGui::TableSetColumnIndex(3); + ImGui::Text("%llu", static_cast(info.requestCount)); + ImGui::TableSetColumnIndex(4); + ImGui::TextUnformatted(info.lastTool.empty() ? "(none)" : info.lastTool.c_str()); + } + + ImGui::EndTable(); + } + + ImGui::TextDisabled( + "To force-disconnect all clients, toggle 'Enable MCP server' off and back on. " + "Per-session kick is not exposed by cpp-mcp's public API."); +} + +std::string RemoteControl::BuildClientConfig() const +{ + // Streamable HTTP transport per the MCP 2025-03-26 spec. Same shape works + // for Claude Code, Cursor, Continue, and other MCP hosts. + // IPv6 literals must be bracketed in a URL authority (RFC 3986 §3.2.2), + // so the IPv6 loopback "::1" becomes "[::1]". IPv4 / hostnames pass + // through verbatim. + const std::string hostInUrl = (settings.bindAddress.find(':') != std::string::npos) ? "[" + settings.bindAddress + "]" : settings.bindAddress; + const json cfg = { + { "mcpServers", + { { "community-shaders", + { + { "type", "http" }, + { "url", std::format("http://{}:{}/mcp", + hostInUrl, settings.port) }, + } } } } + }; + return cfg.dump(4); +} + +void RemoteControl::StartServer() +{ + if (server) { + return; + } + lastError.clear(); + + try { + // Re-validate at bind time — settings may have been touched via the UI + // or hot-reload since LoadSettings ran. + NormalizeBindAddress(settings.bindAddress); + settings.port = ClampPort(settings.port); + + mcp::server::configuration cfg; + cfg.host = settings.bindAddress; + cfg.port = settings.port; + cfg.name = "Community Shaders"; + cfg.version = "0.1.0"; + + server = std::make_unique(cfg); + server->set_server_info(cfg.name, cfg.version); + server->set_capabilities({ { "tools", mcp::json::object() } }); + server->set_instructions( + "This server exposes the Skyrim Community Shaders plugin. " + "Use the tools to inspect engine state for performance " + "investigation and A/B testing of graphics features."); + + RegisterTools(); + + // Drop a session from the bookkeeping map on disconnect. cpp-mcp + // dispatches this from its listener thread when the SSE/HTTP + // connection tears down. + server->register_session_cleanup("remote-control-session-tracker", + [this](const std::string& sessionId) { + DropSession(sessionId); + }); + + if (!server->start(false)) { // false = non-blocking + throw std::runtime_error("server.start() returned false"); + } + activePort = settings.port; + logger::info("Remote Control: MCP server listening on {}:{}", + settings.bindAddress, activePort); + } catch (const std::exception& e) { + lastError = e.what(); + logger::error("Remote Control: failed to start MCP server: {}", + e.what()); + server.reset(); + activePort = 0; + } +} + +void RemoteControl::StopServer() +{ + if (!server) { + return; + } + try { + server->stop(); + } catch (...) { + // best-effort on shutdown + } + server.reset(); + activePort = 0; + { + std::lock_guard lock(sessionMutex); + sessions.clear(); + } + logger::info("Remote Control: MCP server stopped"); +} + +void RemoteControl::RecordToolCall(const std::string& sessionId, const std::string& toolName) +{ + const auto now = std::chrono::system_clock::now(); + std::lock_guard lock(sessionMutex); + auto& info = sessions[sessionId]; + if (info.requestCount == 0) { + info.id = sessionId; + info.connected = now; + } + info.lastSeen = now; + info.requestCount += 1; + info.lastTool = toolName; +} + +void RemoteControl::DropSession(const std::string& sessionId) +{ + std::lock_guard lock(sessionMutex); + sessions.erase(sessionId); +} + +// Helper: wrap a payload string in the MCP tool-result content envelope +// (an array of typed content items). Tools return application data as the +// "text" field of a single content item; consumers typically parse it as +// JSON. +static mcp::json TextResult(std::string text) +{ + return mcp::json::array({ mcp::json{ + { "type", "text" }, + { "text", std::move(text) } } }); +} + +// Helper: emit an error result. Convention: a single text content item +// containing a JSON object with "error" + optional context fields, so +// callers always get parseable JSON whether the call succeeded or not. +static mcp::json ErrorResult(std::string_view message, mcp::json context = {}) +{ + mcp::json obj = { { "error", message } }; + if (!context.is_null()) { + obj.update(context); + } + return mcp::json::array({ mcp::json{ + { "type", "text" }, + { "text", obj.dump() } } }); +} + +void RemoteControl::RegisterTools() +{ + // Five tools, each semantically rich. Reads vs writes vs lifecycles are + // separated by tool; within each tool, kind/action discriminates the + // specific operation. See agentic-renderdoc's "Why this design" notes — + // fewer rich tools outperform expansive suites because the agent reads + // fewer descriptions and each description carries the operational + // expertise (timing, gotchas, verification routes). + RegisterInspectTool(); // reads (non-feature engine state) + RegisterFeatureTool(); // all feature ops (list/get/set/reset/toggle) + RegisterConsoleTool(); // Skyrim console passthrough + RegisterCaptureTool(); // frame capture (renderdoc/screenshot) + RegisterAbtestTool(); // A/B test lifecycle +} + +// Helper used by both inspect(kind="state") and (potentially) future tools. +static mcp::json EngineStateBlob() +{ + const uint frames = globals::state ? globals::state->frameCountAtomic.load(std::memory_order_relaxed) : 0u; + const bool vr = REL::Module::IsVR(); + return mcp::json({ + { "plugin", "CommunityShaders" }, + { "frame_count", frames }, + { "vr", vr }, + }); +} + +// Helper used by feature(action="list") to build one entry per feature. +static mcp::json FeatureEntry(Feature* f) +{ + return mcp::json({ + { "name", f->GetName() }, + { "shortName", f->GetShortName() }, + { "loaded", f->loaded }, + { "version", f->version }, + { "category", std::string(f->GetCategory()) }, + { "isCore", f->IsCore() }, + { "supportsVR", f->SupportsVR() }, + { "inMenu", f->IsInMenu() }, + }); +} + +void RemoteControl::RegisterInspectTool() +{ + // Single read endpoint for non-feature engine state. Kind-discriminated + // so future engine reads (weather, cell, player, render targets) extend + // the same tool rather than spawning new top-level reads. For feature + // reads (list, get settings), use the `feature` tool with the + // corresponding action. + const auto tool = mcp::tool_builder("inspect") + .with_description( + "Read non-feature engine state. Kind-dispatched; the " + "response is always a JSON object delivered as the " + "text of a single content item.\n\n" + "Kinds:\n" + " state — { plugin, frame_count, vr }. Frame counter " + "monotonically increases each render tick; use as a " + "ground truth for verifying that deferred operations " + "(see `console`) have had time to run.\n\n" + "For feature reads (enumerate / settings), use the " + "`feature` tool with action='list' or 'get'.") + .with_string_param("kind", + "Currently 'state'. New kinds will be added here " + "rather than as new tools.") + .build(); + server->register_tool(tool, + [this](const mcp::json& params, const std::string& session_id) -> mcp::json { + RecordToolCall(session_id, "inspect"); + const std::string kind = params.value("kind", std::string{}); + if (kind.empty()) { + return ErrorResult("missing required parameter 'kind'"); + } + if (kind == "state") { + return TextResult(EngineStateBlob().dump()); + } + return ErrorResult("unknown kind", + { { "kind", kind }, + { "supported", mcp::json::array({ "state" }) } }); + }); +} + +void RemoteControl::RegisterFeatureTool() +{ + // One tool for all graphics-feature operations. Action-dispatched so the + // agent has a single description that documents the full feature + // vocabulary plus the gotchas across all five operations (silent no-op + // for missing overrides, listener-thread caveats, etc). + const auto tool = mcp::tool_builder("feature") + .with_description( + "All graphics-feature operations — enumerate, " + "inspect settings, mutate settings, restore defaults, " + "toggle on/off. Action-dispatched; each action takes " + "the parameters listed below.\n\n" + "Actions:\n" + " list — no other params. Returns a JSON array; " + "each entry has { name, shortName, loaded, version, " + "category, isCore, supportsVR, inMenu }.\n" + " get — params: shortName. Returns the " + "Feature::SaveSettings(json) blob. May return null " + "if the feature has no SaveSettings/LoadSettings " + "override (e.g. LightLimitFix); set/reset will " + "silently no-op for these.\n" + " set — params: shortName, settings (object). " + "Calls Feature::LoadSettings on the listener thread. " + "Safe for value-assigning LoadSettings (the common " + "case) and for features that flip a recompileFlag " + "(ScreenSpaceGI, DynamicCubemaps) — the render loop " + "picks them up on the next frame. Settings that " + "synchronously rebuild GPU resources would race; " + "none in-tree currently do.\n" + " reset — params: shortName. Calls " + "Feature::RestoreDefaultSettings(). Distinct from " + "set({}) because RestoreDefaultSettings is " + "feature-specific reset logic (may release/recreate " + "state).\n" + " toggle — params: shortName, enabled (boolean). " + "Flips Feature::loaded. Disabled features are " + "skipped by ForEachLoadedFeature so their per-frame " + "rendering work doesn't run. GPU resources allocated " + "in SetupResources are NOT freed — A/B perf/quality, " + "not memory reclaim.\n\n" + "A/B testing pattern:\n" + " 1. feature(action='get', shortName='Skylighting') → snapshot\n" + " 2. feature(action='reset', shortName='Skylighting') → defaults\n" + " 3. capture + tracy capture → measure\n" + " 4. feature(action='set', shortName='Skylighting', settings=) → restore\n\n" + "Gotchas:\n" + " • Some features have no SaveSettings/LoadSettings " + "override. `get` returns null; `set` and `reset` " + "claim success but don't change anything. Confirmed " + "case: LightLimitFix.\n" + " • toggle keeps GPU resources alive. If a feature " + "still affects rendering after `enabled=false`, it " + "has a hook that isn't gated on `loaded` — file an " + "issue with the shortName.") + .with_string_param("action", + "One of: 'list', 'get', 'set', 'reset', 'toggle'.") + .with_string_param("shortName", + "Required for all actions except 'list'. From the " + "list response.", + /*required=*/false) + .with_object_param("settings", + "Required for action='set'. Shape that matches what " + "action='get' returned for the same feature.", + mcp::json::object(), + /*required=*/false) + .with_boolean_param("enabled", + "Required for action='toggle'.", + /*required=*/false) + .build(); + server->register_tool(tool, + [this](const mcp::json& params, const std::string& session_id) -> mcp::json { + RecordToolCall(session_id, "feature"); + const std::string action = params.value("action", std::string{}); + if (action.empty()) { + return ErrorResult("missing required parameter 'action'"); + } + + if (action == "list") { + mcp::json features = mcp::json::array(); + for (auto* f : Feature::GetFeatureList()) { + features.push_back(FeatureEntry(f)); + } + return TextResult(features.dump()); + } + + const std::string shortName = params.value("shortName", std::string{}); + if (shortName.empty()) { + return ErrorResult("missing required parameter 'shortName'", + { { "action", action } }); + } + + if (action == "toggle") { + if (!params.contains("enabled") || !params["enabled"].is_boolean()) { + return ErrorResult("missing required boolean parameter 'enabled'"); + } + const bool desired = params["enabled"].get(); + // FindFeatureByShortName filters on loaded==true so it can't + // help re-enable; walk the full list ourselves. + Feature* target = nullptr; + for (auto* f : Feature::GetFeatureList()) { + if (f->GetShortName() == shortName) { + target = f; + break; + } + } + if (!target) { + return ErrorResult("feature not found", + { { "shortName", shortName } }); + } + // Marshal the write onto the main/render thread. Feature::loaded + // is read every frame by Feature::ForEachLoadedFeature without + // synchronization, so writing it directly from the MCP listener + // thread is a data race. AddTask runs the closure on the next + // tick. + auto* task = SKSE::GetTaskInterface(); + if (!task) { + return ErrorResult("SKSE TaskInterface unavailable"); + } + const bool previous = target->loaded; + const uint enqueuedFrame = globals::state ? globals::state->frameCountAtomic.load(std::memory_order_relaxed) : 0u; + task->AddTask([target, desired, shortName]() { + target->loaded = desired; + logger::info("Remote Control: feature(toggle, {}, {}) applied", + shortName, desired); + }); + return TextResult(mcp::json({ + { "action", "toggle" }, + { "shortName", shortName }, + { "previous", previous }, + { "requested", desired }, + { "queued", true }, + { "enqueued_at_frame", enqueuedFrame }, + }) + .dump()); + } + + auto* feature = Feature::FindFeatureByShortName(shortName); + if (!feature) { + return ErrorResult("feature not found or not loaded", + { { "shortName", shortName } }); + } + + if (action == "get") { + // SaveSettings uses nlohmann::json (unordered). Keep the + // intermediate value as plain json and dump as a string so + // we don't have to round-trip through mcp::json's ordered map. + ::json blob; + feature->SaveSettings(blob); + return TextResult(blob.dump()); + } + if (action == "set") { + if (!params.contains("settings") || !params["settings"].is_object()) { + return ErrorResult("missing required object parameter 'settings'"); + } + ::json blob; + try { + blob = ::json::parse(params["settings"].dump()); + } catch (const std::exception& e) { + return ErrorResult("settings is not valid JSON", + { { "detail", e.what() } }); + } + // Marshal LoadSettings onto the main thread. Many features + // mutate UI/render-thread-visible state inside LoadSettings + // (palettes, cached textures, settings JSON read elsewhere), + // so calling it from the MCP listener thread is racy. + auto* task = SKSE::GetTaskInterface(); + if (!task) { + return ErrorResult("SKSE TaskInterface unavailable"); + } + const uint enqueuedFrame = globals::state ? globals::state->frameCountAtomic.load(std::memory_order_relaxed) : 0u; + task->AddTask([feature, blob, shortName]() mutable { + try { + feature->LoadSettings(blob); + logger::info("Remote Control: feature(set, {}) applied", shortName); + } catch (const std::exception& e) { + logger::error("Remote Control: feature(set, {}) LoadSettings threw: {}", + shortName, e.what()); + } + }); + return TextResult(mcp::json({ + { "action", "set" }, + { "shortName", shortName }, + { "queued", true }, + { "enqueued_at_frame", enqueuedFrame }, + }) + .dump()); + } + if (action == "reset") { + // Same marshaling rationale as feature(set): RestoreDefaultSettings + // touches state that the render/UI threads read concurrently. + auto* task = SKSE::GetTaskInterface(); + if (!task) { + return ErrorResult("SKSE TaskInterface unavailable"); + } + const uint enqueuedFrame = globals::state ? globals::state->frameCountAtomic.load(std::memory_order_relaxed) : 0u; + task->AddTask([feature, shortName]() { + try { + feature->RestoreDefaultSettings(); + logger::info("Remote Control: feature(reset, {}) applied", shortName); + } catch (const std::exception& e) { + logger::error("Remote Control: feature(reset, {}) RestoreDefaultSettings threw: {}", + shortName, e.what()); + } + }); + return TextResult(mcp::json({ + { "action", "reset" }, + { "shortName", shortName }, + { "queued", true }, + { "enqueued_at_frame", enqueuedFrame }, + }) + .dump()); + } + + return ErrorResult("unknown action", + { { "action", action }, + { "supported", mcp::json::array({ "list", "get", "set", "reset", "toggle" }) } }); + }); +} + +void RemoteControl::RegisterAbtestTool() +{ + // Single tool for the entire A/B testing lifecycle. Action-dispatched + // rather than spawning start_abtest / stop_abtest / get_abtest_results + // / clear_abtest_snapshots / set_abtest_interval — fewer richer tools. + const auto tool = mcp::tool_builder("abtest") + .with_description( + "Drive the built-in A/B testing harness " + "(features/Performance Overlay/ABTesting). The " + "harness rotates between a USER configuration " + "(your current settings) and a TEST configuration " + "(typically a preset under test) on a fixed " + "interval, snapshots both in memory to avoid disk " + "I/O during swaps, and aggregates per-variant " + "frame timing so you can compare quality and perf.\n\n" + "Actions:\n" + " status — return enabled, usingTestConfig, " + "interval, hasCachedSnapshots.\n" + " start — Enable() the manager (begin rotating). " + "Optional `interval` parameter (seconds) is applied " + "first if provided.\n" + " stop — Disable() the manager. Snapshots are " + "retained.\n" + " clear — ClearCachedSnapshots(). Use to reset " + "before a fresh comparison.\n" + " diff — return the per-key diff list " + "(GetConfigDiffEntries) so callers know which " + "settings the rotation is actually toggling.\n\n" + "Setup of the TEST config itself lives in the " + "Performance Overlay UI — this tool only drives " + "the lifecycle, not the test-config authoring.") + .with_string_param("action", + "'status', 'start', 'stop', 'clear', or 'diff'.") + .with_number_param("interval", + "Seconds per variant when action='start'. " + "Default 0 (no change).", + /*required=*/false) + .build(); + server->register_tool(tool, + [this](const mcp::json& params, const std::string& session_id) -> mcp::json { + RecordToolCall(session_id, "abtest"); + const std::string action = params.value("action", std::string{}); + if (action.empty()) { + return ErrorResult("missing required parameter 'action'"); + } + auto* mgr = ABTestingManager::GetSingleton(); + if (!mgr) { + return ErrorResult("ABTestingManager singleton unavailable"); + } + + const auto statusBlob = [&]() { + return mcp::json({ + { "enabled", mgr->IsEnabled() }, + { "usingTestConfig", mgr->IsUsingTestConfig() }, + { "interval", mgr->GetTestInterval() }, + { "hasCachedSnapshots", mgr->HasCachedSnapshots() }, + }); + }; + + if (action == "status") { + // Read-only — safe from the listener thread; the only state we + // touch is the manager's atomic-ish status getters. + return TextResult(statusBlob().dump()); + } + + // Lifecycle actions (start/stop/clear) marshal onto the main thread: + // Enable/Disable swap configs via State::Load → JSON, and Menu::Load + // touches settings the menu/render thread also reads. Doing that + // from the listener thread is a race against the next frame's UI. + auto* task = SKSE::GetTaskInterface(); + if (!task) { + return ErrorResult("SKSE TaskInterface unavailable"); + } + const uint enqueuedFrame = globals::state ? globals::state->frameCountAtomic.load(std::memory_order_relaxed) : 0u; + const auto queuedResult = [&](const std::string& act) { + auto blob = statusBlob(); + blob["action"] = act; + blob["queued"] = true; + blob["enqueued_at_frame"] = enqueuedFrame; + return TextResult(blob.dump()); + }; + + if (action == "start") { + std::optional interval; + if (params.contains("interval") && params["interval"].is_number()) { + const auto secs = params["interval"].get(); + if (secs > 0) { + interval = static_cast(secs); + } + } + task->AddTask([mgr, interval]() { + if (interval) { + mgr->SetTestInterval(*interval); + } + mgr->Enable(); + logger::info("Remote Control: abtest(start) applied"); + }); + return queuedResult("start"); + } + if (action == "stop") { + task->AddTask([mgr]() { + mgr->Disable(); + logger::info("Remote Control: abtest(stop) applied"); + }); + return queuedResult("stop"); + } + if (action == "clear") { + task->AddTask([mgr]() { + mgr->ClearCachedSnapshots(); + logger::info("Remote Control: abtest(clear) applied"); + }); + return queuedResult("clear"); + } + if (action == "diff") { + mcp::json entries = mcp::json::array(); + for (const auto& entry : mgr->GetConfigDiffEntries()) { + // SettingsDiffEntry uses generic a/b labels (see + // Utils/FileSystem.h). For A/B testing semantics here, + // `a` is USER and `b` is TEST. + entries.push_back({ + { "path", entry.path }, + { "userValue", entry.aValue }, + { "testValue", entry.bValue }, + }); + } + return TextResult(mcp::json({ + { "hasCachedSnapshots", mgr->HasCachedSnapshots() }, + { "entries", std::move(entries) }, + }) + .dump()); + } + return ErrorResult("unknown action", + { { "action", action }, + { "supported", mcp::json::array({ "status", "start", "stop", "clear", "diff" }) } }); + }); +} + +void RemoteControl::RegisterCaptureTool() +{ + // One tool for all frame-capture kinds, kind-dispatched. Adding new + // capture types later (e.g. tracy snapshot, video clip) extends this + // tool's `kind` enum rather than spawning new top-level tools. + const auto tool = mcp::tool_builder("capture") + .with_description( + "Trigger a frame capture on the next render. Kind-" + "dispatched so all capture flavors live behind one " + "tool — see the agentic-renderdoc design notes.\n\n" + "Supported kinds:\n" + " renderdoc — RenderDoc multi-frame capture via " + "the in-application API. Honors the `frames` " + "parameter (default 1, max 120). RenderDoc must " + "be attached or the in-app DLL loaded; check " + "feature(action='list') for RenderDoc loaded=true. Output " + "lands in RenderDoc's configured captures dir.\n" + " screenshot — Lossless screenshot via the " + "Screenshot feature's non-blocking capture path. " + "The `frames` parameter is ignored. Output lands " + "in the game's Screenshots/ folder.\n\n" + "Fire-and-forget: the trigger flag is set " + "immediately and the render loop consumes it on " + "the next frame. No artifact path is returned " + "synchronously — for renderdoc, inspect the " + "captures directory; for screenshots, watch the " + "Screenshots folder.") + .with_string_param("kind", + "'renderdoc' or 'screenshot'.") + .with_number_param("frames", + "RenderDoc only: number of consecutive frames to " + "capture (1-120). Default 1. Ignored for " + "screenshot.", + /*required=*/false) + .build(); + server->register_tool(tool, + [this](const mcp::json& params, const std::string& session_id) -> mcp::json { + RecordToolCall(session_id, "capture"); + const std::string kind = params.value("kind", std::string{}); + if (kind.empty()) { + return ErrorResult("missing required parameter 'kind'"); + } + const uint enqueuedFrame = globals::state ? globals::state->frameCountAtomic.load(std::memory_order_relaxed) : 0u; + + if (kind == "renderdoc") { + auto* renderDoc = &globals::features::renderDoc; + if (!renderDoc->loaded) { + return ErrorResult("RenderDoc feature is not loaded", + { { "hint", "feature(action='list') shows RenderDoc.loaded" } }); + } + if (!renderDoc->IsAvailable()) { + return ErrorResult( + "RenderDoc API not available — attach RenderDoc or " + "load the in-app DLL"); + } + uint32_t frameCount = 1; + if (params.contains("frames") && params["frames"].is_number()) { + const auto raw = params["frames"].get(); + frameCount = static_cast(std::clamp(raw, 1, 120)); + } + if (frameCount == 1) { + renderDoc->TriggerCapture(); + } else { + renderDoc->TriggerMultiFrameCapture(frameCount); + } + logger::info("Remote Control: capture(renderdoc, {}) at frame {}", + frameCount, enqueuedFrame); + return TextResult(mcp::json({ + { "queued", true }, + { "kind", "renderdoc" }, + { "frames", frameCount }, + { "enqueued_at_frame", enqueuedFrame }, + }) + .dump()); + } + + if (kind == "screenshot") { + auto* shot = &globals::features::screenshotFeature; + if (!shot->loaded) { + return ErrorResult("Screenshot feature is not loaded"); + } + shot->captureRequested.store(true, std::memory_order_release); + logger::info("Remote Control: capture(screenshot) at frame {}", + enqueuedFrame); + return TextResult(mcp::json({ + { "queued", true }, + { "kind", "screenshot" }, + { "enqueued_at_frame", enqueuedFrame }, + }) + .dump()); + } + + return ErrorResult("unknown kind", + { { "kind", kind }, + { "supported", mcp::json::array({ "renderdoc", "screenshot" }) } }); + }); +} + +void RemoteControl::RegisterConsoleTool() +{ + // Singular tool for the entire console concern. Future console-related + // capabilities (history readout, command lookup, etc.) get added as + // optional parameters / additional response fields here rather than as + // separate tools — per the "fewer, semantically rich tools" philosophy. + const auto tool = mcp::tool_builder("console") + .with_description( + "Execute a Skyrim console command. Fire-and-forget: " + "the command is queued onto the main game thread via " + "SKSE's TaskInterface and runs on the next tick. " + "Returns immediately with the frame counter at the " + "moment of enqueue.\n\n" + "RE::Console::ExecuteCommand is `void` — there is " + "no per-command return value. RE::ConsoleLog is a " + "shared sink (engine + every SKSE plugin) with no " + "command-to-output correlation, and many useful " + "commands are silent (tcl, tfc, tg, tm, tlb…), so " + "scraping console output is unreliable and " + "intentionally NOT exposed.\n\n" + "To verify a state change, poll inspect(kind='state') " + "until frame_count > enqueued_at_frame (at least one tick " + "elapsed), then observe via side channels: tracy " + "captures for perf-affecting changes, " + "capture(kind='renderdoc'|'screenshot') for visual " + "confirmation, or future feature-specific get_* " + "tools that read RE:: state directly.\n\n" + "Common A/B-relevant commands:\n" + " tcl — toggle player collision\n" + " tfc [1] — free camera (1 = pause game)\n" + " tg — toggle grass\n" + " tm — toggle menus / HUD\n" + " tll <0..15> — toggle land LOD level\n" + " setweather — force weather (persistent)\n" + " fw — force weather (temporary)\n" + " coc — teleport to cell\n" + " set timescale to N — game-time multiplier\n") + .with_string_param("command", + "The console command, exactly as typed after the ~ key.") + .build(); + server->register_tool(tool, + [this](const mcp::json& params, const std::string& session_id) -> mcp::json { + RecordToolCall(session_id, "console"); + std::string command = params.value("command", std::string{}); + if (command.empty()) { + return ErrorResult("missing required parameter 'command'"); + } + auto* task = SKSE::GetTaskInterface(); + if (!task) { + return ErrorResult("SKSE TaskInterface unavailable"); + } + const uint enqueuedFrame = globals::state ? globals::state->frameCountAtomic.load(std::memory_order_relaxed) : 0u; + // Capture by value so the string outlives this lambda's scope. + task->AddTask([command]() { + RE::Console::ExecuteCommand(command.c_str()); + }); + logger::info("Remote Control: console({}) queued at frame {}", + command, enqueuedFrame); + return TextResult(mcp::json({ + { "queued", true }, + { "command", std::move(command) }, + { "enqueued_at_frame", enqueuedFrame }, + }) + .dump()); + }); +} diff --git a/src/Features/RemoteControl.h b/src/Features/RemoteControl.h new file mode 100644 index 0000000000..0c88ad8107 --- /dev/null +++ b/src/Features/RemoteControl.h @@ -0,0 +1,118 @@ +#pragma once + +#include "Feature.h" + +#include +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +// Forward declare cpp-mcp types so we don't leak its vendored +// httplib / json headers into consumers of this header. +namespace mcp +{ + class server; + struct tool; + // cpp-mcp's tool_handler is std::function + // where `json` is an alias for ordered_json — that can't be forward-declared + // cleanly without dragging the full vendored nlohmann/json header into this + // public header. Tool registration therefore stays in the .cpp where the + // real signature is in scope; only opaque pointers are exposed here. +} + +class RemoteControl : public Feature +{ +public: + static RemoteControl* GetSingleton(); + + // Feature overrides — see Feature.h for contracts. + std::string GetName() override { return "Remote Control"; } + std::string GetShortName() override { return "RemoteControl"; } + std::string_view GetCategory() const override { return FeatureCategories::kUtility; } + bool IsCore() const override { return true; } + bool IsInMenu() const override { return true; } + bool SupportsVR() override { return true; } + std::string_view GetShaderDefineName() override { return ""; } + bool HasShaderDefine(RE::BSShader::Type) override { return false; } + + std::pair> GetFeatureSummary() override + { + return { + "Expose Community Shaders to AI assistants over Model Context Protocol (MCP).", + { + "Loopback-only JSON-RPC server, off by default", + "Pair with Claude Code / Cursor / Continue for A/B testing", + "One-click clipboard copy of MCP client config", + } + }; + } + + // Lifecycle + void Load() override; + void Reset() override; + + // Settings persistence + void DrawSettings() override; + void RestoreDefaultSettings() override; + void LoadSettings(json& o_json) override; + void SaveSettings(json& o_json) override; + + struct Settings + { + bool enabled = false; // opt-in + int port = 8910; // arbitrary high port + std::string bindAddress = "127.0.0.1"; // loopback by default + } settings; + + RemoteControl(); + ~RemoteControl(); + + RemoteControl(const RemoteControl&) = delete; + RemoteControl& operator=(const RemoteControl&) = delete; + RemoteControl(RemoteControl&&) = delete; + RemoteControl& operator=(RemoteControl&&) = delete; + + // Session bookkeeping for the ImGui "Connected clients" table. + // Updated on every tool invocation (listener thread) and on session + // cleanup (cpp-mcp callback). Read from the main thread when drawing. + struct SessionInfo + { + std::string id; + std::chrono::system_clock::time_point connected; + std::chrono::system_clock::time_point lastSeen; + uint64_t requestCount = 0; + std::string lastTool; + }; + +private: + void StartServer(); + void StopServer(); + bool IsRunning() const noexcept { return server != nullptr; } + std::string BuildClientConfig() const; + void RegisterTools(); + void RegisterInspectTool(); + void RegisterFeatureTool(); + void RegisterConsoleTool(); + void RegisterCaptureTool(); + void RegisterAbtestTool(); + + // Records a tool invocation against the per-session table. + // Safe to call from the cpp-mcp listener thread. + void RecordToolCall(const std::string& sessionId, const std::string& toolName); + // Drops a session from the table on disconnect. + void DropSession(const std::string& sessionId); + // Draws the connected-clients ImGui table. + void DrawClientsTable(); + + std::unique_ptr server; + int activePort = 0; + std::string lastError; + + mutable std::mutex sessionMutex; + std::unordered_map sessions; +}; diff --git a/src/Globals.cpp b/src/Globals.cpp index 850d0f9dce..d216a05b74 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -17,6 +17,7 @@ #include "Features/LightLimitFix.h" #include "Features/LinearLighting.h" #include "Features/PerformanceOverlay.h" +#include "Features/RemoteControl.h" #include "Features/RenderDoc.h" #include "Features/ScreenSpaceGI.h" #include "Features/ScreenSpaceShadows.h" @@ -85,6 +86,7 @@ namespace globals Upscaling upscaling{}; HDRDisplay hdrDisplay{}; RenderDoc renderDoc{}; + RemoteControl remoteControl{}; ScreenshotFeature screenshotFeature{}; WeatherEditor weatherEditor{}; ExponentialHeightFog exponentialHeightFog{}; diff --git a/src/Globals.h b/src/Globals.h index a5e262a768..4556d5ab18 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -41,6 +41,7 @@ class State; class Deferred; struct TruePBR; class RenderDoc; +class RemoteControl; class Menu; namespace SIE @@ -92,6 +93,7 @@ namespace globals extern Upscaling upscaling; extern HDRDisplay hdrDisplay; extern RenderDoc renderDoc; + extern RemoteControl remoteControl; extern ScreenshotFeature screenshotFeature; extern WeatherEditor weatherEditor; extern ExponentialHeightFog exponentialHeightFog; diff --git a/src/State.cpp b/src/State.cpp index 681a6996cc..b9ebfdd05f 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -186,6 +186,8 @@ void State::Reset() lastVertexDescriptor = 0; std::memset(&permutationDataPrevious, 0xFF, sizeof(PermutationCB)); frameCount++; + // Publish for off-thread readers (e.g. the MCP listener thread). + frameCountAtomic.store(frameCount, std::memory_order_relaxed); if (auto* imageSpaceManager = RE::ImageSpaceManager::GetSingleton()) { GET_INSTANCE_MEMBER(BSImagespaceShaderApplyReflections, imageSpaceManager); diff --git a/src/State.h b/src/State.h index fdddd770fa..5d0987e0c2 100644 --- a/src/State.h +++ b/src/State.h @@ -268,6 +268,10 @@ class State Util::FrameChecker frameChecker; uint frameCount = 0; + // Thread-safe mirror of frameCount maintained by the render thread. + // Off-thread readers (MCP listener, future telemetry) must read this + // instead of touching frameCount directly to avoid a data race. + std::atomic frameCountAtomic{ 0 }; // Skyrim constants float2 screenSize = {}; From 23c04e3aef992c41b620a9abb186bfdcba1ecf09 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 21 May 2026 23:11:36 -0700 Subject: [PATCH 09/24] refactor(menu): regroup Advanced tabs by purpose (#28) Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Claude Opus 4.7 --- docs/development/vscode-setup.md | 2 +- src/FeatureIssues.cpp | 120 +++-- src/Menu/AdvancedSettingsRenderer.cpp | 604 +++++++++++++++----------- src/Menu/AdvancedSettingsRenderer.h | 18 +- src/Menu/SettingsTabRenderer.cpp | 20 +- 5 files changed, 430 insertions(+), 334 deletions(-) diff --git a/docs/development/vscode-setup.md b/docs/development/vscode-setup.md index 7122622582..8dac26867a 100644 --- a/docs/development/vscode-setup.md +++ b/docs/development/vscode-setup.md @@ -60,7 +60,7 @@ Automatically deploy shaders when you save `.hlsl` or `.hlsli` files. **Interaction with built-in filewatcher:** -Community Shaders has a built-in filewatcher (**Settings → Advanced → Shader Compilation → Enable File Watcher**) that hot-reloads shaders when files change in the game's `Data/Shaders/` directory. The workflow is: +Community Shaders has a built-in filewatcher (**Settings → Advanced → Shaders → Cache & File Watcher → Enable File Watcher**) that hot-reloads shaders when files change in the game's `Data/Shaders/` directory. The workflow is: 1. Edit shader in VSCode 2. Save → RunOnSave deploys to `Data/Shaders/` diff --git a/src/FeatureIssues.cpp b/src/FeatureIssues.cpp index e744cad4db..64362979d5 100644 --- a/src/FeatureIssues.cpp +++ b/src/FeatureIssues.cpp @@ -1462,74 +1462,70 @@ namespace FeatureIssues auto* menu = Menu::GetSingleton(); const auto& themeSettings = menu->GetTheme(); - if (ImGui::CollapsingHeader("Testing", ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - { - auto sectionWrapper = Util::SectionWrapper("Feature Issue Testing", - "These tools create test INI files to trigger all known feature issue types for testing purposes.", - themeSettings.Palette.Text); - - if (sectionWrapper) { - const bool hasActiveTests = HasActiveTestInis(); - if (hasActiveTests) { // Warning section using theme colors - ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.RestartNeeded); - ImGui::TextWrapped("Test INI files are currently active. Restart CS to see feature issues."); - ImGui::PopStyleColor(); // Show detailed test state information - ImGui::Spacing(); - ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.RestartNeeded); - ImGui::TextWrapped(GetTestStateDescription().c_str()); - ImGui::PopStyleColor(); - ImGui::Spacing(); - } - - // Create Test INIs button - { - auto disableGuard = Util::DisableGuard(hasActiveTests); - auto buttonStyle = Util::StyledButtonWrapper( - themeSettings.Palette.FrameBorder, - themeSettings.StatusPalette.RestartNeeded, - themeSettings.StatusPalette.CurrentHotkey); - - if (ImGui::Button("Create Test Inis", { -1, 0 })) { - auto testInis = CreateTestInis(); - logger::info("Created {} test INI files for feature issue testing", testInis.size()); - } - } + auto sectionWrapper = Util::SectionWrapper("Feature Issue Testing", + "These tools create test INI files to trigger all known feature issue types for testing purposes.", + themeSettings.Palette.Text); + + if (sectionWrapper) { + const bool hasActiveTests = HasActiveTestInis(); + if (hasActiveTests) { // Warning section using theme colors + ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.RestartNeeded); + ImGui::TextWrapped("Test INI files are currently active. Restart CS to see feature issues."); + ImGui::PopStyleColor(); // Show detailed test state information + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.RestartNeeded); + ImGui::TextWrapped(GetTestStateDescription().c_str()); + ImGui::PopStyleColor(); + ImGui::Spacing(); + } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Creates test INI files that trigger all known feature issue cases:\n" - "- Obsolete features (ComplexParallaxMaterials, TerrainBlending, etc.)\n" - "- Unknown features (fake non-existent features)\n" - "- Version mismatch (modifies existing feature version)\n" - "Restart CS after creating to see the issues in action."); - } + // Create Test INIs button + { + auto disableGuard = Util::DisableGuard(hasActiveTests); + auto buttonStyle = Util::StyledButtonWrapper( + themeSettings.Palette.FrameBorder, + themeSettings.StatusPalette.RestartNeeded, + themeSettings.StatusPalette.CurrentHotkey); + + if (ImGui::Button("Create Test INIs", { -1, 0 })) { + auto testInis = CreateTestInis(); + logger::info("Created {} test INI files for feature issue testing", testInis.size()); + } + } - // Restore button - { - auto disableGuard = Util::DisableGuard(!hasActiveTests); - auto buttonStyle = Util::StyledButtonWrapper( - themeSettings.Palette.FrameBorder, - themeSettings.StatusPalette.Error, - themeSettings.StatusPalette.CurrentHotkey); - - if (ImGui::Button("Restore", { -1, 0 })) { - auto& testInis = GetCurrentTestInis(); - if (RestoreOriginalState(testInis)) { - logger::info("Successfully restored original state"); - } else { - logger::warn("Some restoration operations failed"); - } - } - } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Creates test INI files that trigger all known feature issue cases:\n" + "- Obsolete features (ComplexParallaxMaterials, TerrainBlending, etc.)\n" + "- Unknown features (fake non-existent features)\n" + "- Version mismatch (modifies existing feature version)\n" + "Restart CS after creating to see the issues in action."); + } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Removes all test INI files and restores any modified INI files to their original state.\n" - "This undoes all changes made by 'Create Test Inis'.\n" - "Restart CS after restoring to see normal operation."); + // Restore button + { + auto disableGuard = Util::DisableGuard(!hasActiveTests); + auto buttonStyle = Util::StyledButtonWrapper( + themeSettings.Palette.FrameBorder, + themeSettings.StatusPalette.Error, + themeSettings.StatusPalette.CurrentHotkey); + + if (ImGui::Button("Restore", { -1, 0 })) { + auto& testInis = GetCurrentTestInis(); + if (RestoreOriginalState(testInis)) { + logger::info("Successfully restored original state"); + } else { + logger::warn("Some restoration operations failed"); } } } + + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Removes all test INI files and restores any modified INI files to their original state.\n" + "This undoes all changes made by 'Create Test INIs'.\n" + "Restart CS after restoring to see normal operation."); + } } } bool RefreshTestState() diff --git a/src/Menu/AdvancedSettingsRenderer.cpp b/src/Menu/AdvancedSettingsRenderer.cpp index 8bd217b20d..0738816f32 100644 --- a/src/Menu/AdvancedSettingsRenderer.cpp +++ b/src/Menu/AdvancedSettingsRenderer.cpp @@ -20,18 +20,20 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( const std::function& drawDisableAtBootSettings) { - // Use TabBar system - tabs sorted alphabetically + // Tabs ordered alphabetically; each tab is grouped by purpose, not audience. + // Shaders = configure & inspect shader compilation + // Diagnostics = log/inspect runtime state & block individual shaders + // Disable at Boot = user-facing failsafe toggles + // Testing = A/B harness + dev-mode test scaffolding if (ImGui::BeginTabBar("##AdvancedSettingsTabs", ImGuiTabBarFlags_None)) { - // Developer Tab - if (MenuFonts::BeginTabItemWithFont("Developer", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##DeveloperContent", ImVec2(0, 0), false)) { - RenderDeveloperSection(); + if (MenuFonts::BeginTabItemWithFont("Diagnostics", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##DiagnosticsContent", ImVec2(0, 0), false)) { + RenderDiagnosticsSection(); } ImGui::EndChild(); ImGui::EndTabItem(); } - // Disable at Boot Tab if (MenuFonts::BeginTabItemWithFont("Disable at Boot", Menu::FontRole::Subheading)) { if (ImGui::BeginChild("##DisableAtBootContent", ImVec2(0, 0), false)) { RenderDisableAtBootSection(drawDisableAtBootSettings); @@ -40,27 +42,16 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( ImGui::EndTabItem(); } - // Logging Tab - if (MenuFonts::BeginTabItemWithFont("Logging", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##LoggingContent", ImVec2(0, 0), false)) { - RenderLoggingSection(); + if (MenuFonts::BeginTabItemWithFont("Shaders", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##ShadersContent", ImVec2(0, 0), false)) { + RenderShadersSection(); } ImGui::EndChild(); ImGui::EndTabItem(); } - // Shader Debug Tab - if (MenuFonts::BeginTabItemWithFont("Shader Debug", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##ShaderDebugContent", ImVec2(0, 0), false)) { - RenderShaderDebugSection(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - // Testing Tab (for A/B Testing and related settings) if (MenuFonts::BeginTabItemWithFont("Testing", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##Testing", ImVec2(0, 0), false)) { + if (ImGui::BeginChild("##TestingContent", ImVec2(0, 0), false)) { RenderTestingSection(); } ImGui::EndChild(); @@ -71,29 +62,44 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( } } -void AdvancedSettingsRenderer::RenderLoggingSection() +// ----------------------------------------------------------------------------- +// Shaders tab +// ----------------------------------------------------------------------------- + +void AdvancedSettingsRenderer::RenderShadersSection() +{ + RenderShaderCompileFlags(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + RenderShaderThreading(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + RenderShaderCacheControls(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + RenderShaderReplacementTable(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + RenderShaderCompileStatistics(); +} + +void AdvancedSettingsRenderer::RenderShaderCompileFlags() { auto shaderCache = globals::shaderCache; - // Log Level selection - spdlog::level::level_enum logLevel = globals::state->GetLogLevel(); - const char* items[] = { - "trace", - "debug", - "info", - "warn", - "err", - "critical", - "off" - }; - static int item_current = static_cast(logLevel); - if (ImGui::Combo("Log Level", &item_current, items, IM_ARRAYSIZE(items))) { - ImGui::SameLine(); - globals::state->SetLogLevel(static_cast(item_current)); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Log level. Trace is most verbose. Default is info."); - } + Util::DrawSectionHeader("Compile Flags"); // Shader Defines input auto& shaderDefines = globals::state->shaderDefinesString; @@ -110,46 +116,97 @@ void AdvancedSettingsRenderer::RenderLoggingSection() ImGui::Text("Defines for Shader Compiler. Semicolon \";\" separated. Clear with space. Rebuild shaders after making change. Compute Shaders require a restart to recompile."); } - ImGui::Spacing(); + // Half-precision (partial precision) shader compile flag + bool partialPrecision = globals::state->enablePartialPrecision.load(std::memory_order_relaxed); + if (ImGui::Checkbox("Half Precision (Partial Precision)", &partialPrecision)) { + globals::state->enablePartialPrecision.store(partialPrecision, std::memory_order_relaxed); + // Force a recompile so the flag actually takes effect on subsequent shader builds. + shaderCache->Clear(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Adds D3DCOMPILE_PARTIAL_PRECISION to the shader compiler flags.\n" + "Lets fxc downgrade unmarked float ops to FP16 where it can prove safety, " + "on top of the existing min16float type hints.\n" + "On FP16-capable GPUs (Pascal+ / GCN+ / Skylake+) this can halve register " + "pressure and double ALU throughput, but it can also introduce minor visual " + "differences in shaders that haven't been audited for precision sensitivity.\n" + "Toggling this clears the shader cache and triggers a full recompile."); + } - // Compiler Thread controls - ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); + // Avoid flow control compiler flag (transient — not saved to config because the + // right setting depends on the current scene, not the user). + bool avoidFlowControl = globals::state->enableAvoidFlowControl.load(std::memory_order_relaxed); + if (ImGui::Checkbox("Avoid Flow Control", &avoidFlowControl)) { + globals::state->enableAvoidFlowControl.store(avoidFlowControl, std::memory_order_relaxed); + // Force a recompile so the flag actually takes effect on subsequent shader builds. + shaderCache->Clear(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Adds D3DCOMPILE_AVOID_FLOW_CONTROL to the shader compiler flags.\n" + "Forces fxc to flatten branches into predicated ops rather than emitting " + "dynamic flow control. Often a win for short branch bodies and uniformly-" + "taken branches; usually a loss for long divergent branches that vanilla " + "flow control would skip entirely.\n" + "Resets every launch. Toggling this clears the shader cache and triggers a " + "full recompile."); + } +} + +void AdvancedSettingsRenderer::RenderShaderThreading() +{ + auto shaderCache = globals::shaderCache; + + Util::DrawSectionHeader("Threading"); + + // hardware_concurrency() is permitted to return 0 if the implementation can't + // detect it. Fall back to the actual compile-pool thread count we ended up + // using at startup (which itself defaults to a sensible value when the OS + // query fails), then clamp to at least 1 so the slider range (min=1, max=N) + // stays valid and ImGui doesn't assert. + const uint32_t hwThreads = std::thread::hardware_concurrency(); + const int32_t poolThreads = static_cast(shaderCache->compilationPool.get_thread_count()); + const int32_t maxThreads = std::max({ 1, poolThreads, static_cast(hwThreads) }); + + // Snap the persisted values back into the valid range — a stale config can + // otherwise leave compilationThreadCount above maxThreads, which would + // render the slider in an out-of-range state. + shaderCache->compilationThreadCount = std::clamp(shaderCache->compilationThreadCount, 1, maxThreads); + shaderCache->backgroundCompilationThreadCount = std::clamp(shaderCache->backgroundCompilationThreadCount, 1, maxThreads); + + ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, maxThreads); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "Number of threads used to compile shaders at startup. " "Defaults to all logical cores minus one for OS headroom (E-cores included). " "Higher values finish compilation faster but may make the system less responsive."); } - ImGui::SliderInt("Background Compiler Threads", &shaderCache->backgroundCompilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); + ImGui::SliderInt("Background Compiler Threads", &shaderCache->backgroundCompilationThreadCount, 1, maxThreads); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "Number of threads used to compile shaders during gameplay. " "Defaults to half of performance cores to avoid impacting the render thread. " "Higher values finish compilation faster but may cause stuttering."); } - - ImGui::Columns(2, nullptr, false); - - // Dump Ini Settings button - if (ImGui::Button("Dump Ini Settings", { -1, 0 })) { - Util::DumpSettingsOptions(); - } - - ImGui::NextColumn(); - - // Open Logs button - std::filesystem::path logPath = Util::PathHelpers::GetLogPath(); - if (!logPath.empty() && ImGui::Button("Open Logs", { -1, 0 })) { - ShellExecuteA(NULL, "open", logPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); - } - - ImGui::Columns(1); } -void AdvancedSettingsRenderer::RenderShaderDebugSection() +void AdvancedSettingsRenderer::RenderShaderCacheControls() { auto shaderCache = globals::shaderCache; - auto state = globals::state; + + Util::DrawSectionHeader("Cache & File Watcher"); + + // File Watcher option + bool useFileWatcher = shaderCache->UseFileWatcher(); + if (ImGui::Checkbox("Enable File Watcher", &useFileWatcher)) { + shaderCache->SetFileWatcher(useFileWatcher); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Automatically recompile shaders on file change. " + "Intended for development."); + } // Dump Shaders option bool useDump = shaderCache->IsDump(); @@ -167,12 +224,12 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Clear all compiled shaders from memory. Forces recompilation of all shaders on next use."); } +} - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); +void AdvancedSettingsRenderer::RenderShaderReplacementTable() +{ + auto state = globals::state; - // Shader Replacement section Util::DrawSectionHeader("Replace Original Shaders"); if (ImGui::BeginTable("##ReplaceToggles", 3, ImGuiTableFlags_SizingStretchSame)) { @@ -213,16 +270,212 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() } ImGui::EndTable(); } +} + +void AdvancedSettingsRenderer::RenderShaderCompileStatistics() +{ + auto shaderCache = globals::shaderCache; - // Only show shader blocking section in developer mode - if (!globals::state->IsDeveloperMode()) { + if (!ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { return; } + ImGui::Text("Shader Compiler : %s", shaderCache->GetShaderStatsString().c_str()); + + // Derived parallelism metrics are computed lazily on demand and only shown + // once compilation has completed to avoid per-frame analysis while compiling. + if (!shaderCache->IsCompiling()) { + auto parallelism = shaderCache->GetParallelismStats(); + if (parallelism.has_value()) { + const auto& p = parallelism.value(); + ImGui::Spacing(); + ImGui::TextDisabled("Parallelism (derived from %zu compiled tasks)", p.sampleCount); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Computed lazily from the last completed build."); + ImGui::Text("Only evaluated when this Statistics section is open."); + } + ImGui::Text("Work (W, sum of task wall times): %s", Util::FormatDuration(p.workMs).c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Total compile work: sum of all per-shader wall-clock compile times."); + ImGui::Text("This is not CPU time; it is accumulated task elapsed time."); + ImGui::Text("Equivalent serial time on one worker if overhead stayed the same."); + } + ImGui::Text("Span (S, longest): %s", Util::FormatDuration(p.spanMs).c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Critical-path lower bound, approximated by the single slowest shader."); + ImGui::Text("Even infinite cores cannot finish faster than this."); + } + ImGui::Text("Makespan (T_p): %s", Util::FormatDuration(p.makespanMs).c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Observed wall-clock duration for the full shader build."); + } + ImGui::Text("Queue wait (avg/max): %s / %s", + Util::FormatDuration(p.avgQueueWaitMs).c_str(), + Util::FormatDuration(p.maxQueueWaitMs).c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Time spent waiting in the ready queue before a worker started compilation."); + ImGui::Text("Useful for identifying scheduler-induced delay separate from compile cost."); + } + ImGui::Text("Average parallelism (W/S): %.2fx", p.avgParallelism); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Average useful concurrency in this workload."); + ImGui::Text("Roughly the worker count where adding more cores gives diminishing returns."); + } + ImGui::Text("Infinite-core efficiency (S/T_p): %.1f%%", 100.0 * p.infiniteCoreEfficiency); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("How close runtime is to the infinite-core lower bound."); + ImGui::Text("100%% means T_p == S."); + } + ImGui::Text("Infinite-core gap: %.1f%%", p.infiniteCoreGapPercent); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Distance from ideal infinite-core time."); + ImGui::Text("Defined as 100 * (1 - S / T_p). Lower is better."); + } + + ImGui::Spacing(); + ImGui::TextDisabled("Infinite-core efficiency"); + float efficiency = static_cast(std::clamp(p.infiniteCoreEfficiency, 0.0, 1.0)); + ImGui::ProgressBar(efficiency, ImVec2(-1.0f, 0.0f), std::format("{:.1f}% efficient / {:.1f}% gap", 100.0 * p.infiniteCoreEfficiency, p.infiniteCoreGapPercent).c_str()); + + ImGui::Spacing(); + ImGui::TextDisabled("Relative durations (normalized)"); + double maxMs = std::max({ p.workMs, p.spanMs, p.makespanMs, 1.0 }); + auto drawRelativeBar = [maxMs](const char* label, double value) { + float ratio = static_cast(std::clamp(value / maxMs, 0.0, 1.0)); + ImGui::TextUnformatted(label); + ImGui::SameLine(); + ImGui::ProgressBar(ratio, ImVec2(-1.0f, 0.0f), std::format("{} ({:.1f}%)", Util::FormatDuration(value), 100.0 * ratio).c_str()); + }; + drawRelativeBar("Span (S)", p.spanMs); + drawRelativeBar("Makespan (T_p)", p.makespanMs); + drawRelativeBar("Work (W)", p.workMs); + } + } + + // Top-3 slowest shaders from the last build + auto topSlow = shaderCache->GetTopSlowTasks(3); + if (!topSlow.empty()) { + ImGui::Spacing(); + ImGui::TextDisabled("Top %zu Slowest Shaders (last build)", topSlow.size()); + for (size_t i = 0; i < topSlow.size(); ++i) { + const auto& rec = topSlow[i]; + ImGui::Text("#%zu %s (weight %d)", i + 1, + Util::FormatDuration(rec.elapsedMs).c_str(), rec.priority); + ImGui::SameLine(); + ImGui::TextDisabled("%s", rec.key.c_str()); + if (ImGui::IsItemHovered()) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", rec.key.c_str()); + } + } + // Allow copying the full key with a right-click + if (ImGui::BeginPopupContextItem(std::format("##slowcopy{}", i).c_str())) { + if (ImGui::MenuItem("Copy key")) { + ImGui::SetClipboardText(rec.key.c_str()); + } + ImGui::EndPopup(); + } + } + } + + ImGui::TreePop(); +} + +// ----------------------------------------------------------------------------- +// Diagnostics tab +// ----------------------------------------------------------------------------- + +void AdvancedSettingsRenderer::RenderDiagnosticsSection() +{ + RenderLoggingControls(); + ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); + RenderRuntimeDebugControls(); + + // Shader blocking only meaningful in developer mode (matches prior behavior). + if (globals::state->IsDeveloperMode()) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + RenderShaderBlockingPanel(); + } +} + +void AdvancedSettingsRenderer::RenderLoggingControls() +{ + Util::DrawSectionHeader("Logging"); + + // Log Level selection. Resync from state every frame so external changes + // (config reload, console command, another caller of SetLogLevel) don't + // leave the combo displaying a stale selection. + spdlog::level::level_enum logLevel = globals::state->GetLogLevel(); + const char* items[] = { + "trace", + "debug", + "info", + "warn", + "err", + "critical", + "off" + }; + int item_current = static_cast(logLevel); + if (ImGui::Combo("Log Level", &item_current, items, IM_ARRAYSIZE(items))) { + globals::state->SetLogLevel(static_cast(item_current)); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Log level. Trace is most verbose. Default is info."); + } + + ImGui::Columns(2, nullptr, false); + + // Dump Ini Settings button + if (ImGui::Button("Dump Ini Settings", { -1, 0 })) { + Util::DumpSettingsOptions(); + } + + ImGui::NextColumn(); + + // Open Logs button + std::filesystem::path logPath = Util::PathHelpers::GetLogPath(); + if (!logPath.empty() && ImGui::Button("Open Logs", { -1, 0 })) { + ShellExecuteA(NULL, "open", logPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); + } + + ImGui::Columns(1); +} + +void AdvancedSettingsRenderer::RenderRuntimeDebugControls() +{ + Util::DrawSectionHeader("Runtime Debug"); + + // Frame annotations toggle + ImGui::Checkbox("Frame Annotations", &globals::state->frameAnnotations); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Enable detailed frame annotations for debugging render passes and draw calls."); + } + + // Debug addresses section + if (ImGui::TreeNodeEx("Addresses")) { + auto Renderer = globals::game::renderer; + auto BSShaderAccumulator = *globals::game::currentAccumulator.get(); + auto RendererShadowState = globals::game::shadowState; + ADDRESS_NODE(Renderer) + ADDRESS_NODE(BSShaderAccumulator) + ADDRESS_NODE(RendererShadowState) + ImGui::TreePop(); + } +} + +void AdvancedSettingsRenderer::RenderShaderBlockingPanel() +{ + auto shaderCache = globals::shaderCache; + + Util::DrawSectionHeader("Shader Blocking"); + // Show blocked shader status as a regular section if (!shaderCache->blockedKey.empty()) { // Create a visually distinct box for the blocked shader info with rounded corners and border @@ -286,8 +539,8 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() ImGui::PopStyleColor(); // ChildBg } - // Shader Debug section - if (ImGui::CollapsingHeader("Shader Debug")) { + // Blocking hotkeys + enable toggle + { auto menu = globals::menu; auto& menuSettings = menu->GetSettings(); auto& themeSettings = menuSettings.Theme; @@ -338,9 +591,11 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() } } - // Active shaders list - if (ImGui::CollapsingHeader("Active Shaders", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("Active Shaders (Used Recently)"); + // Active shaders list — rendered inline; the parent panel already says + // "Shader Blocking", so a nested CollapsingHeader was redundant noise. + { + ImGui::Spacing(); + Util::DrawSectionHeader("Active Shaders (Used Recently)"); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "List of shaders that have been used in recent frames. " @@ -504,189 +759,31 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() } } +// ----------------------------------------------------------------------------- +// Disable at Boot tab +// ----------------------------------------------------------------------------- + void AdvancedSettingsRenderer::RenderDisableAtBootSection(const std::function& drawDisableAtBootSettings) { drawDisableAtBootSettings(); } -void AdvancedSettingsRenderer::RenderDeveloperSection() -{ - auto shaderCache = globals::shaderCache; - - // File Watcher option (moved from Advanced/Logging) - bool useFileWatcher = shaderCache->UseFileWatcher(); - if (ImGui::Checkbox("Enable File Watcher", &useFileWatcher)) { - shaderCache->SetFileWatcher(useFileWatcher); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Automatically recompile shaders on file change. " - "Intended for developing."); - } - - // Debug addresses section (moved from Advanced/Logging) - if (ImGui::TreeNodeEx("Addresses")) { - auto Renderer = globals::game::renderer; - auto BSShaderAccumulator = *globals::game::currentAccumulator.get(); - auto RendererShadowState = globals::game::shadowState; - ADDRESS_NODE(Renderer) - ADDRESS_NODE(BSShaderAccumulator) - ADDRESS_NODE(RendererShadowState) - ImGui::TreePop(); - } - - // Statistics section (moved from Advanced/Logging) - if (ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text(std::format("Shader Compiler : {}", shaderCache->GetShaderStatsString()).c_str()); - - // Derived parallelism metrics are computed lazily on demand and only shown - // once compilation has completed to avoid per-frame analysis while compiling. - if (!shaderCache->IsCompiling()) { - auto parallelism = shaderCache->GetParallelismStats(); - if (parallelism.has_value()) { - const auto& p = parallelism.value(); - ImGui::Spacing(); - ImGui::TextDisabled("Parallelism (derived from %zu compiled tasks)", p.sampleCount); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Computed lazily from the last completed build."); - ImGui::Text("Only evaluated when this Statistics section is open."); - } - ImGui::Text("Work (W, sum of task wall times): %s", Util::FormatDuration(p.workMs).c_str()); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Total compile work: sum of all per-shader wall-clock compile times."); - ImGui::Text("This is not CPU time; it is accumulated task elapsed time."); - ImGui::Text("Equivalent serial time on one worker if overhead stayed the same."); - } - ImGui::Text("Span (S, longest): %s", Util::FormatDuration(p.spanMs).c_str()); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Critical-path lower bound, approximated by the single slowest shader."); - ImGui::Text("Even infinite cores cannot finish faster than this."); - } - ImGui::Text("Makespan (T_p): %s", Util::FormatDuration(p.makespanMs).c_str()); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Observed wall-clock duration for the full shader build."); - } - ImGui::Text("Queue wait (avg/max): %s / %s", - Util::FormatDuration(p.avgQueueWaitMs).c_str(), - Util::FormatDuration(p.maxQueueWaitMs).c_str()); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Time spent waiting in the ready queue before a worker started compilation."); - ImGui::Text("Useful for identifying scheduler-induced delay separate from compile cost."); - } - ImGui::Text("Average parallelism (W/S): %.2fx", p.avgParallelism); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Average useful concurrency in this workload."); - ImGui::Text("Roughly the worker count where adding more cores gives diminishing returns."); - } - ImGui::Text("Infinite-core efficiency (S/T_p): %.1f%%", 100.0 * p.infiniteCoreEfficiency); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("How close runtime is to the infinite-core lower bound."); - ImGui::Text("100%% means T_p == S."); - } - ImGui::Text("Infinite-core gap: %.1f%%", p.infiniteCoreGapPercent); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Distance from ideal infinite-core time."); - ImGui::Text("Defined as 100 * (1 - S / T_p). Lower is better."); - } - - ImGui::Spacing(); - ImGui::TextDisabled("Infinite-core efficiency"); - float efficiency = static_cast(std::clamp(p.infiniteCoreEfficiency, 0.0, 1.0)); - ImGui::ProgressBar(efficiency, ImVec2(-1.0f, 0.0f), std::format("{:.1f}% efficient / {:.1f}% gap", 100.0 * p.infiniteCoreEfficiency, p.infiniteCoreGapPercent).c_str()); - - ImGui::Spacing(); - ImGui::TextDisabled("Relative durations (normalized)"); - double maxMs = std::max({ p.workMs, p.spanMs, p.makespanMs, 1.0 }); - auto drawRelativeBar = [maxMs](const char* label, double value) { - float ratio = static_cast(std::clamp(value / maxMs, 0.0, 1.0)); - ImGui::TextUnformatted(label); - ImGui::SameLine(); - ImGui::ProgressBar(ratio, ImVec2(-1.0f, 0.0f), std::format("{} ({:.1f}%)", Util::FormatDuration(value), 100.0 * ratio).c_str()); - }; - drawRelativeBar("Span (S)", p.spanMs); - drawRelativeBar("Makespan (T_p)", p.makespanMs); - drawRelativeBar("Work (W)", p.workMs); - } - } - - // Top-3 slowest shaders from the last build - auto topSlow = shaderCache->GetTopSlowTasks(3); - if (!topSlow.empty()) { - ImGui::Spacing(); - ImGui::TextDisabled("Top %zu Slowest Shaders (last build)", topSlow.size()); - for (size_t i = 0; i < topSlow.size(); ++i) { - const auto& rec = topSlow[i]; - ImGui::Text("#%zu %s (weight %d)", i + 1, - Util::FormatDuration(rec.elapsedMs).c_str(), rec.priority); - ImGui::SameLine(); - ImGui::TextDisabled("%s", rec.key.c_str()); - if (ImGui::IsItemHovered()) { - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("%s", rec.key.c_str()); - } - } - // Allow copying the full key with a right-click - if (ImGui::BeginPopupContextItem(std::format("##slowcopy{}", i).c_str())) { - if (ImGui::MenuItem("Copy key")) { - ImGui::SetClipboardText(rec.key.c_str()); - } - ImGui::EndPopup(); - } - } - } - - ImGui::TreePop(); - } - - // Frame annotations toggle (moved from Advanced/Logging) - ImGui::Checkbox("Frame Annotations", &globals::state->frameAnnotations); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enable detailed frame annotations for debugging render passes and draw calls."); - } +// ----------------------------------------------------------------------------- +// Testing tab +// ----------------------------------------------------------------------------- - // Half-precision (partial precision) shader compile flag - bool partialPrecision = globals::state->enablePartialPrecision.load(std::memory_order_relaxed); - if (ImGui::Checkbox("Half Precision (Partial Precision)", &partialPrecision)) { - globals::state->enablePartialPrecision.store(partialPrecision, std::memory_order_relaxed); - // Force a recompile so the flag actually takes effect on subsequent shader builds. - globals::shaderCache->Clear(); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Adds D3DCOMPILE_PARTIAL_PRECISION to the shader compiler flags.\n" - "Lets fxc downgrade unmarked float ops to FP16 where it can prove safety, " - "on top of the existing min16float type hints.\n" - "On FP16-capable GPUs (Pascal+ / GCN+ / Skylake+) this can halve register " - "pressure and double ALU throughput, but it can also introduce minor visual " - "differences in shaders that haven't been audited for precision sensitivity.\n" - "Toggling this clears the shader cache and triggers a full recompile."); - } - - // Avoid flow control compiler flag (transient — not saved to config because the - // right setting depends on the current scene, not the user). - bool avoidFlowControl = globals::state->enableAvoidFlowControl.load(std::memory_order_relaxed); - if (ImGui::Checkbox("Avoid Flow Control", &avoidFlowControl)) { - globals::state->enableAvoidFlowControl.store(avoidFlowControl, std::memory_order_relaxed); - // Force a recompile so the flag actually takes effect on subsequent shader builds. - globals::shaderCache->Clear(); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Adds D3DCOMPILE_AVOID_FLOW_CONTROL to the shader compiler flags.\n" - "Forces fxc to flatten branches into predicated ops rather than emitting " - "dynamic flow control. Often a win for short branch bodies and uniformly-" - "taken branches; usually a loss for long divergent branches that vanilla " - "flow control would skip entirely.\n" - "Resets every launch. Toggling this clears the shader cache and triggers a " - "full recompile."); - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); +void AdvancedSettingsRenderer::RenderTestingSection() +{ + // A/B Testing settings + auto* abTestingManager = ABTestingManager::GetSingleton(); + abTestingManager->DrawSettingsUI(); - // Developer Mode Testing Section + // Developer Mode Testing UI + scene-prep button (previously on the "Developer" tab) if (globals::state->IsDeveloperMode()) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + FeatureIssues::Test::DrawDeveloperModeTestingUI(); ImGui::Spacing(); @@ -704,10 +801,3 @@ void AdvancedSettingsRenderer::RenderDeveloperSection() } } } - -void AdvancedSettingsRenderer::RenderTestingSection() -{ - // A/B Testing settings - auto* abTestingManager = ABTestingManager::GetSingleton(); - abTestingManager->DrawSettingsUI(); -} diff --git a/src/Menu/AdvancedSettingsRenderer.h b/src/Menu/AdvancedSettingsRenderer.h index 086486c246..eaf6cac0e4 100644 --- a/src/Menu/AdvancedSettingsRenderer.h +++ b/src/Menu/AdvancedSettingsRenderer.h @@ -13,9 +13,19 @@ class AdvancedSettingsRenderer const std::function& drawDisableAtBootSettings); private: - static void RenderLoggingSection(); - static void RenderShaderDebugSection(); + static void RenderShadersSection(); + static void RenderDiagnosticsSection(); static void RenderDisableAtBootSection(const std::function& drawDisableAtBootSettings); - static void RenderDeveloperSection(); static void RenderTestingSection(); -}; \ No newline at end of file + + // Helpers used by the sections above + static void RenderShaderCompileFlags(); + static void RenderShaderThreading(); + static void RenderShaderCacheControls(); + static void RenderShaderReplacementTable(); + static void RenderShaderCompileStatistics(); + + static void RenderLoggingControls(); + static void RenderRuntimeDebugControls(); + static void RenderShaderBlockingPanel(); +}; diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 61c85958c3..d639d3ffd7 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -249,16 +249,6 @@ void SettingsTabRenderer::RenderShadersTab() ImGui::Text("Skips a shader being replaced if it hasn't been compiled yet. Also makes compilation blazingly fast!"); } - // Skip confirmation when clearing shader cache - auto& menuSettings = globals::menu->GetSettings(); - bool skipConfirmation = menuSettings.SkipClearCacheConfirmation; - if (ImGui::Checkbox("Skip Clear Cache Dialogue", &skipConfirmation)) { - menuSettings.SkipClearCacheConfirmation = skipConfirmation; - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("When checked, the shader cache will be cleared immediately without asking for confirmation."); - } - if (shaderCache->GetTotalTasks() > 0) { ImGui::Text("Last shader cache build duration: %s", shaderCache->GetShaderStatsString(true, true).c_str()); @@ -463,6 +453,16 @@ void SettingsTabRenderer::RenderBehaviorTab() ImGui::TextUnformatted("Time in seconds to wait before a tooltip appears when hovering over an item."); } + // Skip confirmation when clearing shader cache (UI behavior, not a shader setting). + auto& menuSettings = globals::menu->GetSettings(); + bool skipConfirmation = menuSettings.SkipClearCacheConfirmation; + if (ImGui::Checkbox("Skip Clear Cache Confirmation", &skipConfirmation)) { + menuSettings.SkipClearCacheConfirmation = skipConfirmation; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("When checked, the shader cache will be cleared immediately without asking for confirmation."); + } + SeparatorTextWithFont("Visual Effects", Menu::FontRole::Subheading); if (ImGui::Checkbox("Background Blur", &themeSettings.BackgroundBlurEnabled)) { From 97556f0750bea7f7faa5b06c35e4b852a5e4c621 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Fri, 22 May 2026 00:37:34 -0700 Subject: [PATCH 10/24] refactor(utils): add stereo support to Util::Subrect::Controller (#23) Co-authored-by: YtzyFvra <59631290+YtzyFvra@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 --- CMakeLists.txt | 19 ++ src/Utils/Subrect.cpp | 142 ++++++++++++- src/Utils/Subrect.h | 58 +++++- tests/cpp/CMakeLists.txt | 79 ++++++++ tests/cpp/test_main.cpp | 9 + tests/cpp/test_subrect.cpp | 406 +++++++++++++++++++++++++++++++++++++ 6 files changed, 711 insertions(+), 2 deletions(-) create mode 100644 tests/cpp/CMakeLists.txt create mode 100644 tests/cpp/test_main.cpp create mode 100644 tests/cpp/test_subrect.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d225f0eac3..1d61d07fb7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1314,7 +1314,26 @@ if(BUILD_SHADER_TESTS) message(STATUS "Adding shader tests subdirectory") enable_testing() # Enable CTest integration for shader tests add_subdirectory(tests/shaders) +endif() + +# C++ unit tests for plugin utility code (separate from HLSL shader tests). +# Gated on its own flag so it can be enabled/disabled independently. +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/cpp/CMakeLists.txt") + enable_testing() + add_subdirectory(tests/cpp) + if(TARGET cpp_tests) + add_custom_target( + run_cpp_tests + COMMAND $ --reporter compact + DEPENDS cpp_tests + WORKING_DIRECTORY $ + COMMENT "Running C++ unit tests..." + VERBATIM + ) + endif() +endif() +if(BUILD_SHADER_TESTS) # Add a custom target that runs the shader tests # Users can run this manually with: cmake --build --target run_shader_tests # Runs the test executable directly (not via CTest) to show discovery count diff --git a/src/Utils/Subrect.cpp b/src/Utils/Subrect.cpp index 2a9fb590ef..61512fdbe2 100644 --- a/src/Utils/Subrect.cpp +++ b/src/Utils/Subrect.cpp @@ -1,6 +1,8 @@ #include "Utils/Subrect.h" #include +#include +#include #include namespace @@ -39,6 +41,16 @@ namespace return ClampUV(uv); } + Util::Subrect::UVRegion MirrorUVHorizontal(const Util::Subrect::UVRegion& uv) + { + // HMD nose-side overlap: left-eye nose-side region is on the right + // half of the eye texture; mirror around x=0.5 maps it to the + // right-eye's left half. + Util::Subrect::UVRegion mirrored = uv; + mirrored.x = 1.0f - uv.x - uv.w; + return ClampUV(mirrored); + } + json SaveUVToJson(const Util::Subrect::UVRegion& uv) { return { uv.x, uv.y, uv.w, uv.h }; @@ -70,6 +82,30 @@ namespace Util::Subrect if (a_json.contains("CropH")) currentUV.h = a_json["CropH"]; + const bool hasExplicitLeft = + a_json.contains("CropX") || a_json.contains("CropY") || + a_json.contains("CropW") || a_json.contains("CropH"); + // Require the full quartet before declaring the right-eye UV explicit. + // A partial config (e.g. only CropRightW present) would otherwise reuse + // stale values for the missing components and silently suppress the + // left→right auto-mirror fallback. With AND semantics, partial keys + // behave as "not explicit" and the mirror still runs. + const bool hasExplicitRight = + a_json.contains("CropRightX") && a_json.contains("CropRightY") && + a_json.contains("CropRightW") && a_json.contains("CropRightH"); + if (a_json.contains("CropRightX")) + currentRightUV.x = a_json["CropRightX"]; + if (a_json.contains("CropRightY")) + currentRightUV.y = a_json["CropRightY"]; + if (a_json.contains("CropRightW")) + currentRightUV.w = a_json["CropRightW"]; + if (a_json.contains("CropRightH")) + currentRightUV.h = a_json["CropRightH"]; + // Reset every load — a later LoadSettings without CropRight* keys + // should let SetStereoEnabled(true) auto-mirror again rather than + // preserving stale state from a prior load. + rightUVLoadedFromJson = hasExplicitRight; + if (a_json.contains("CropPresets") && a_json["CropPresets"].is_array()) { presets.clear(); for (auto& entry : a_json["CropPresets"]) { @@ -78,6 +114,19 @@ namespace Util::Subrect if (entry.contains("uv")) { preset.uv = LoadUVArray(entry["uv"]); } + // Right-eye UV is optional in JSON; leave nullopt when absent so + // ApplyPreset auto-mirrors the left eye on demand. Explicit + // right_uv in JSON wins over any mirror — but only when it + // looks structurally valid. LoadUVArray falls back to a + // full-frame UV on malformed input, so without this guard a + // bad `right_uv` payload would suppress auto-mirroring AND + // land the right eye as full-frame, which is the worst of + // both worlds. + if (entry.contains("right_uv") && + entry["right_uv"].is_array() && + entry["right_uv"].size() == 4) { + preset.rightUV = LoadUVArray(entry["right_uv"]); + } presets.push_back(std::move(preset)); } } @@ -85,6 +134,14 @@ namespace Util::Subrect EnsureDefaultPreset(); ClampCurrentUV(); + // Legacy upgrade: if the JSON has the mono crop keys but no right-eye + // keys, mirror left → right so existing user settings transition + // cleanly. If neither side is present, leave currentRightUV alone so + // EnsureDefaultPreset's seeded right-eye value survives. + if (stereoEnabled && hasExplicitLeft && !hasExplicitRight) { + SyncRightUV(); + } + if (a_json.contains("SelectedPresetIndex")) { selectedPresetIndex = a_json["SelectedPresetIndex"]; if (selectedPresetIndex >= 0 && selectedPresetIndex < static_cast(presets.size())) { @@ -102,11 +159,32 @@ namespace Util::Subrect a_json["CropW"] = currentUV.w; a_json["CropH"] = currentUV.h; + if (stereoEnabled) { + a_json["CropRightX"] = currentRightUV.x; + a_json["CropRightY"] = currentRightUV.y; + a_json["CropRightW"] = currentRightUV.w; + a_json["CropRightH"] = currentRightUV.h; + } else { + // Caller may pass a JSON object with prior stereo keys (e.g. a + // host that re-saves into the same in-memory config). Drop them + // so the next load doesn't look like it had explicit stereo data. + a_json.erase("CropRightX"); + a_json.erase("CropRightY"); + a_json.erase("CropRightW"); + a_json.erase("CropRightH"); + } + json presetsJson = json::array(); for (const auto& preset : presets) { json entry; entry["name"] = preset.name; entry["uv"] = SaveUVToJson(preset.uv); + // Only serialize right_uv when stereo is enabled AND we have an + // explicit value to persist. A nullopt preset implicitly means + // "auto-mirror at apply time" and shouldn't be locked into JSON. + if (stereoEnabled && preset.rightUV.has_value()) { + entry["right_uv"] = SaveUVToJson(*preset.rightUV); + } presetsJson.push_back(std::move(entry)); } a_json["CropPresets"] = presetsJson; @@ -118,6 +196,21 @@ namespace Util::Subrect seededDefaults = std::move(defaults); } + void Controller::SetStereoEnabled(bool enabled) + { + if (stereoEnabled == enabled) { + return; + } + stereoEnabled = enabled; + // Only auto-mirror left→right when the right-eye UV hasn't been + // explicitly loaded from JSON. Otherwise a caller that does + // `LoadSettings` (stereo off) then `SetStereoEnabled(true)` would + // silently overwrite a deliberate persisted right-eye crop. + if (stereoEnabled && !rightUVLoadedFromJson) { + SyncRightUV(); + } + } + void Controller::DrawEditor(ID3D11ShaderResourceView* previewSrv, ID3D11Texture2D* previewTexture, float uvVisibleWidth, float uvStartX, ImDrawCallback imageRenderCallback) { // Hosts that render without first calling LoadSettings would otherwise @@ -148,7 +241,18 @@ namespace Util::Subrect if (ImGui::Button("Save Preset")) { std::string presetName = newPresetName; if (!presetName.empty()) { - presets.push_back(Preset{ .name = presetName, .uv = currentUV }); + // Preserve the right-eye UV only when stereo is on. In mono + // mode currentRightUV is not tracked against currentUV, so + // snapshotting it would falsely mark the preset as having an + // explicit right eye and disable the auto-mirror fallback + // once stereo is later enabled. Leave rightUV as nullopt in + // mono — ApplyPreset will mirror left at apply time. + // (CodeRabbit Major @ scs#2356 for the stereo-side fix.) + Preset newPreset{ .name = presetName, .uv = currentUV }; + if (stereoEnabled) { + newPreset.rightUV = currentRightUV; + } + presets.push_back(std::move(newPreset)); selectedPresetIndex = static_cast(presets.size()) - 1; newPresetName[0] = '\0'; } @@ -177,6 +281,9 @@ namespace Util::Subrect if (changed) { selectedPresetIndex = -1; ClampCurrentUV(); + if (stereoEnabled) { + SyncRightUV(); + } } ImGui::Spacing(); @@ -235,6 +342,9 @@ namespace Util::Subrect currentUV.w = maxX - minX; currentUV.h = maxY - minY; ClampCurrentUV(); + if (stereoEnabled) { + SyncRightUV(); + } if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { isDraggingCrop = false; @@ -253,6 +363,23 @@ namespace Util::Subrect return UVToPixelRegion(currentUV, width, height); } + StereoPixelRegions Controller::GetStereoPixelRegions(uint32_t fullWidth, uint32_t fullHeight) const + { + // Degenerate inputs would underflow UVToPixelRegion's `width - 1` / + // `height - 1` computations into huge values. Fail safe with empty + // regions so callers can detect the bad-input case via .w == 0. + if (fullWidth < 2 || fullHeight == 0) { + return { PixelRegion{ 0, 0, 0, 0 }, PixelRegion{ 0, 0, 0, 0 } }; + } + // Each eye occupies half the SBS texture width. In mono mode, both + // eyes report the same region so callers don't need to branch. + const uint32_t eyeWidth = fullWidth / 2; + StereoPixelRegions regions; + regions.leftEye = UVToPixelRegion(currentUV, eyeWidth, fullHeight); + regions.rightEye = UVToPixelRegion(stereoEnabled ? currentRightUV : currentUV, eyeWidth, fullHeight); + return regions; + } + void Controller::EnsureDefaultPreset() { if (!presets.empty()) { @@ -263,6 +390,9 @@ namespace Util::Subrect // currentUV must match what the combo shows as selected; otherwise // the first preset appears chosen but the crop region stays full-frame. currentUV = presets[0].uv; + // nullopt rightUV means "auto-mirror" — match the same fallback + // ApplyPreset uses below. + currentRightUV = presets[0].rightUV.value_or(MirrorUVHorizontal(currentUV)); selectedPresetIndex = 0; } else { presets.push_back(Preset{ .name = "Full Frame", .uv = DefaultUV() }); @@ -272,6 +402,7 @@ namespace Util::Subrect void Controller::ClampCurrentUV() { currentUV = ClampUV(currentUV); + currentRightUV = ClampUV(currentRightUV); } void Controller::ApplyPreset(int index) @@ -279,6 +410,15 @@ namespace Util::Subrect EnsureDefaultPreset(); selectedPresetIndex = std::clamp(index, 0, static_cast(presets.size()) - 1); currentUV = presets[selectedPresetIndex].uv; + // Nullopt right-UV → mirror left around x=0.5. This is the safe default + // for presets created without a stereo-specific intent (e.g. via + // SeedDefaultPresets with only .name + .uv specified). + currentRightUV = presets[selectedPresetIndex].rightUV.value_or(MirrorUVHorizontal(currentUV)); ClampCurrentUV(); } + + void Controller::SyncRightUV() + { + currentRightUV = MirrorUVHorizontal(currentUV); + } } // namespace Util::Subrect diff --git a/src/Utils/Subrect.h b/src/Utils/Subrect.h index f05934c8be..c5c0a4705b 100644 --- a/src/Utils/Subrect.h +++ b/src/Utils/Subrect.h @@ -1,8 +1,22 @@ #pragma once +#include #include +#include +#include +#include #include +// Forward-declared so the header doesn't drag in . The plugin's PCH +// brings the real types into scope at definition sites. +struct ID3D11ShaderResourceView; +struct ID3D11Texture2D; + +// Mirrors the global `using json = nlohmann::json;` from the plugin PCH so +// the header builds standalone (e.g. in unit-test targets that don't +// precompile PCH). Identical aliases in the same scope are well-defined. +using json = nlohmann::json; + namespace Util::Subrect { struct UVRegion @@ -21,15 +35,33 @@ namespace Util::Subrect uint32_t h = 1; }; + struct StereoPixelRegions + { + PixelRegion leftEye; + PixelRegion rightEye; + }; + struct Preset { std::string name; - UVRegion uv; + UVRegion uv; // Left-eye UV when stereo is enabled; sole UV otherwise. + // Right-eye UV. `std::nullopt` means "no explicit right eye — auto-mirror + // left around x=0.5 when stereo is enabled". A default-constructed + // `UVRegion{}` (full frame) would otherwise be ambiguous: it could mean + // "the user wants full frame" or "the caller didn't supply one", and + // the silent-full-frame case bites SeedDefaultPresets callers that only + // fill `.name` and `.uv`. + std::optional rightUV{}; }; // "User picks a sub-rectangle of an image" controller. Crop UV is in [0,1] // of the source the caller passes to GetPixelRegion(). Hosts that want // preset-based eye selection seed Left/Right/Full Frame via SeedDefaultPresets. + // + // Stereo: hosts that consume a side-by-side stereo texture call + // SetStereoEnabled(true) to track a separate right-eye UV. Right-eye UV + // auto-mirrors left around x=0.5 unless explicitly edited; this matches + // HMD nose-side overlap symmetry. class Controller { public: @@ -40,6 +72,12 @@ namespace Util::Subrect // CropPresets entry yet. Empty-case only - user edits/deletions persist. void SeedDefaultPresets(std::vector defaults); + // Toggles right-eye UV tracking. Off by default (mono). + // When enabled, edits to the primary UV auto-mirror to the right-eye + // UV (around x=0.5), and SaveSettings emits the extra right-eye keys. + void SetStereoEnabled(bool enabled); + bool IsStereoEnabled() const { return stereoEnabled; } + // uvStartX/uvVisibleWidth window the preview onto a sub-region of the // texture; crop UV stays in [0,1] of that window. imageRenderCallback, // when non-null, is queued via ImDrawList::AddCallback around the @@ -52,7 +90,17 @@ namespace Util::Subrect // Resolves the crop UV against an arbitrary pixel size. PixelRegion GetPixelRegion(uint32_t width, uint32_t height) const; + // In stereo mode, resolves both eyes' UVs against an SBS texture by + // dividing width by 2. In mono mode, both eyes resolve from currentUV. + // + // Coordinate space: both leftEye.x and rightEye.x are in PER-EYE + // space (i.e. x in [0, fullWidth/2)) — the right eye is NOT + // pre-offset by eyeWidth. Callers that draw into the full SBS + // texture must add `fullWidth / 2` to rightEye.x themselves. + StereoPixelRegions GetStereoPixelRegions(uint32_t fullWidth, uint32_t fullHeight) const; + const UVRegion& GetUV() const { return currentUV; } + const UVRegion& GetRightEyeUV() const { return stereoEnabled ? currentRightUV : currentUV; } private: std::vector presets; @@ -61,6 +109,13 @@ namespace Util::Subrect char newPresetName[64] = ""; UVRegion currentUV{}; + UVRegion currentRightUV{}; + bool stereoEnabled = false; + // True once LoadSettings sees an explicit CropRight* key. Suppresses + // the auto-mirror in SetStereoEnabled(true) so a deliberate JSON + // right-eye crop survives a mono→stereo transition that happens + // after the load. + bool rightUVLoadedFromJson = false; bool isDraggingCrop = false; float dragStartUV[2] = { 0.0f, 0.0f }; @@ -68,5 +123,6 @@ namespace Util::Subrect void EnsureDefaultPreset(); void ClampCurrentUV(); void ApplyPreset(int index); + void SyncRightUV(); }; } // namespace Util::Subrect diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt new file mode 100644 index 0000000000..9ec4f08996 --- /dev/null +++ b/tests/cpp/CMakeLists.txt @@ -0,0 +1,79 @@ +# C++ unit tests for plugin utility code. +# +# Catch2-based. Disjoint from tests/shaders (which tests HLSL via +# ShaderTestFramework). Add test sources here for any plugin code that +# doesn't require a live DirectX device or ImGui context. + +cmake_minimum_required(VERSION 4.2) + +option(BUILD_CPP_TESTS "Build C++ unit tests for plugin utilities" ON) + +if(NOT BUILD_CPP_TESTS) + message(STATUS "C++ unit tests disabled") + return() +endif() + +message(STATUS "Configuring C++ unit tests...") + +include(FetchContent) + +# Reused by tests/shaders if that target is also enabled. FetchContent dedupes +# by name, so the same Catch2 source tree serves both. +FetchContent_Declare( + catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.11.0 +) +FetchContent_MakeAvailable(Catch2) + +# MSVC 14.50+ (VS 2026) ICE workaround copied from tests/shaders. +if(MSVC AND MSVC_VERSION GREATER_EQUAL 1950) + if(TARGET Catch2) + target_compile_options(Catch2 PRIVATE /Od) + endif() +endif() + +find_package(nlohmann_json CONFIG REQUIRED) +find_package(imgui CONFIG REQUIRED) + +add_executable(cpp_tests + test_main.cpp + test_subrect.cpp + # Compile the unit-under-test directly into the test binary so we don't + # depend on the plugin DLL build (which pulls in FFX/Streamline/etc.). + "${CMAKE_SOURCE_DIR}/src/Utils/Subrect.cpp" +) + +set_property(TARGET cpp_tests PROPERTY CXX_STANDARD 23) +set_property(TARGET cpp_tests PROPERTY CXX_STANDARD_REQUIRED ON) + +target_include_directories(cpp_tests PRIVATE + "${CMAKE_SOURCE_DIR}/src" +) + +target_link_libraries(cpp_tests PRIVATE + Catch2::Catch2 + nlohmann_json::nlohmann_json + imgui::imgui +) + +# windows.h (pulled in via d3d11.h) defines min/max macros that break the +# std::min/std::max calls in Subrect.cpp. The plugin's PCH suppresses +# this implicitly; the test target needs it explicitly. +target_compile_definitions(cpp_tests PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN +) + +enable_testing() +add_test( + NAME CppUtilTests + COMMAND cpp_tests --reporter compact + WORKING_DIRECTORY $ +) +set_tests_properties(CppUtilTests PROPERTIES + TIMEOUT 60 + LABELS "Cpp;UnitTests" +) + +message(STATUS "C++ unit tests configured successfully") diff --git a/tests/cpp/test_main.cpp b/tests/cpp/test_main.cpp new file mode 100644 index 0000000000..2c8aefc944 --- /dev/null +++ b/tests/cpp/test_main.cpp @@ -0,0 +1,9 @@ +// Explicit main() for C++ unit tests. +// Catch2WithMain has linker issues with CMake 4.0 (see tests/shaders/minimal_test.cpp); +// we provide our own session entry point. +#include + +int main(int argc, char* argv[]) +{ + return Catch::Session().run(argc, argv); +} diff --git a/tests/cpp/test_subrect.cpp b/tests/cpp/test_subrect.cpp new file mode 100644 index 0000000000..fdfc1ce1e1 --- /dev/null +++ b/tests/cpp/test_subrect.cpp @@ -0,0 +1,406 @@ +// Unit tests for Util::Subrect::Controller. +// +// Focus: API contract for the stereo extension (PR-1 of the DLSS-PR-PLAN decomposition). +// Cannot exercise DrawEditor (needs an ImGui context); covers everything else: +// JSON load/save round-trips, mirror math, preset apply, mono/stereo back-compat. + +#include +using json = nlohmann::json; + +#include // ImDrawCallback declared in Subrect.h signature + +#include "Utils/Subrect.h" + +#include + +#include // std::abs +#include // std::pair +#include // std::vector — degenerate-dimensions cases enumerate (w,h) pairs + +using Util::Subrect::Controller; +using Util::Subrect::Preset; +using Util::Subrect::UVRegion; + +namespace +{ + bool UVApprox(const UVRegion& a, const UVRegion& b, float eps = 1e-5f) + { + return std::abs(a.x - b.x) < eps && std::abs(a.y - b.y) < eps && + std::abs(a.w - b.w) < eps && std::abs(a.h - b.h) < eps; + } +} + +TEST_CASE("Controller defaults to mono mode", "[subrect]") +{ + Controller c; + REQUIRE_FALSE(c.IsStereoEnabled()); + // Right-eye accessor folds onto primary UV in mono mode. + REQUIRE(UVApprox(c.GetUV(), c.GetRightEyeUV())); +} + +TEST_CASE("SaveSettings in mono mode emits no right-eye keys", "[subrect][backcompat]") +{ + // Pre-stereo screenshot JSON shape must round-trip bit-identically: this is + // the core back-compat contract for the existing ScreenshotFeature consumer. + // EnsureDefaultPreset is lazy (only runs in LoadSettings/ApplyPreset/DrawEditor), + // so prime it with an empty load to realize the placeholder preset. + Controller c; + c.LoadSettings(json::object()); + json out; + c.SaveSettings(out); + + REQUIRE(out.contains("CropX")); + REQUIRE(out.contains("CropY")); + REQUIRE(out.contains("CropW")); + REQUIRE(out.contains("CropH")); + REQUIRE_FALSE(out.contains("CropRightX")); + REQUIRE_FALSE(out.contains("CropRightY")); + REQUIRE_FALSE(out.contains("CropRightW")); + REQUIRE_FALSE(out.contains("CropRightH")); + + REQUIRE(out["CropPresets"].is_array()); + REQUIRE(out["CropPresets"].size() >= 1); + for (const auto& entry : out["CropPresets"]) { + REQUIRE(entry.contains("uv")); + REQUIRE_FALSE(entry.contains("right_uv")); + } +} + +TEST_CASE("LoadSettings reads legacy mono JSON unchanged", "[subrect][backcompat]") +{ + // Replicates the screenshot feature's existing on-disk schema. + json in = { + { "CropX", 0.25f }, + { "CropY", 0.10f }, + { "CropW", 0.5f }, + { "CropH", 0.8f }, + { "CropPresets", json::array({ + { + { "name", "Full Frame" }, + { "uv", { 0.0f, 0.0f, 1.0f, 1.0f } }, + }, + }) }, + { "SelectedPresetIndex", -1 }, + }; + + Controller c; + c.LoadSettings(in); + + REQUIRE(UVApprox(c.GetUV(), { 0.25f, 0.10f, 0.5f, 0.8f })); + REQUIRE_FALSE(c.IsStereoEnabled()); +} + +TEST_CASE("SetStereoEnabled toggles mirror sync", "[subrect][stereo]") +{ + // Stereo enable on a fresh controller mirrors the default UV (which is + // {0,0,1,1}); the mirror of a full-frame is still full-frame. + Controller c; + c.SetStereoEnabled(true); + REQUIRE(c.IsStereoEnabled()); + + // Re-enable is a no-op (no double-sync, no state corruption). + c.SetStereoEnabled(true); + REQUIRE(c.IsStereoEnabled()); + + c.SetStereoEnabled(false); + REQUIRE_FALSE(c.IsStereoEnabled()); +} + +TEST_CASE("Stereo save then load round-trips right-eye keys", "[subrect][stereo]") +{ + // Seed presets with explicit asymmetric right-eye to confirm the on-disk + // schema preserves it (rather than always mirroring on load). + Controller src; + src.SetStereoEnabled(true); + src.SeedDefaultPresets({ + Preset{ .name = "Asym", .uv = { 0.1f, 0.0f, 0.5f, 1.0f }, .rightUV = UVRegion{ 0.3f, 0.0f, 0.4f, 0.9f } }, + }); + // Realize the seed and copy it into currentUV/currentRightUV. + src.LoadSettings(json::object()); + + json saved; + src.SaveSettings(saved); + + REQUIRE(saved.contains("CropRightX")); + REQUIRE(saved["CropPresets"][0].contains("right_uv")); + + Controller dst; + dst.SetStereoEnabled(true); + dst.LoadSettings(saved); + + // After load, applying the asymmetric preset must restore both eyes + // exactly (not mirror-overwrite the right eye). + REQUIRE(UVApprox(dst.GetUV(), { 0.1f, 0.0f, 0.5f, 1.0f })); + REQUIRE(UVApprox(dst.GetRightEyeUV(), { 0.3f, 0.0f, 0.4f, 0.9f })); +} + +TEST_CASE("Stereo load mirrors right-eye when JSON lacks CropRight keys", "[subrect][stereo][backcompat]") +{ + // A user upgrading from a mono build has CropX/Y/W/H but no CropRight*. + // In stereo mode, the controller mirrors the primary UV around x=0.5. + json legacy = { + { "CropX", 0.2f }, + { "CropY", 0.0f }, + { "CropW", 0.5f }, + { "CropH", 1.0f }, + }; + + Controller c; + c.SetStereoEnabled(true); + c.LoadSettings(legacy); + + REQUIRE(UVApprox(c.GetUV(), { 0.2f, 0.0f, 0.5f, 1.0f })); + // Mirror: x = 1 - 0.2 - 0.5 = 0.3 + REQUIRE(UVApprox(c.GetRightEyeUV(), { 0.3f, 0.0f, 0.5f, 1.0f })); +} + +TEST_CASE("GetStereoPixelRegions splits SBS width per eye", "[subrect][stereo]") +{ + // Stereo regions resolve against half-width per eye (the texture is SBS). + // Caller passes the full stereo texture size; controller divides W by 2. + Controller c; + c.SetStereoEnabled(true); + + // Make eyes asymmetric so we can tell them apart. + c.SeedDefaultPresets({ + Preset{ + .name = "Asym", + .uv = { 0.0f, 0.0f, 1.0f, 1.0f }, // full left eye + .rightUV = UVRegion{ 0.0f, 0.0f, 0.5f, 1.0f }, // left half of right eye + }, + }); + // Realize the seed so currentUV/currentRightUV pick up the preset values. + c.LoadSettings(json::object()); + + const auto regions = c.GetStereoPixelRegions(2000, 1000); + // Left eye spans the full 1000-wide left half. + REQUIRE(regions.leftEye.x == 0); + REQUIRE(regions.leftEye.w == 1000); + REQUIRE(regions.leftEye.h == 1000); + // Right eye spans only half the 1000-wide right half = 500 px. + REQUIRE(regions.rightEye.w == 500); +} + +TEST_CASE("GetStereoPixelRegions in mono mode returns identical eyes", "[subrect][stereo]") +{ + // Mono callers can use the stereo accessor and both eyes will resolve from + // the primary UV — lets DLSS consumers stay agnostic of stereo state. + Controller c; + json in = { + { "CropX", 0.0f }, + { "CropY", 0.0f }, + { "CropW", 1.0f }, + { "CropH", 1.0f }, + }; + c.LoadSettings(in); + + const auto regions = c.GetStereoPixelRegions(2000, 1000); + REQUIRE(regions.leftEye.x == regions.rightEye.x); + REQUIRE(regions.leftEye.w == regions.rightEye.w); +} + +TEST_CASE("Stereo SaveSettings emits right_uv for every preset", "[subrect][stereo][regression]") +{ + // Regression for CodeRabbit Major @ scs#2356: the Save Preset button used + // to drop currentRightUV, so re-applying a saved preset would zero out the + // right eye. We can't drive the ImGui Save Preset button from a test, but + // we can stage the same end state (a Controller with stereo enabled and an + // in-memory preset whose rightUV differs from a mirror of left) and verify + // it round-trips both eyes through SaveSettings → LoadSettings. + Controller src; + src.SetStereoEnabled(true); + + // Stage a preset with an explicit asymmetric right_uv that doesn't equal + // MirrorUVHorizontal(uv) — otherwise we couldn't distinguish "right_uv was + // preserved" from "default fallback mirrored it back". + const UVRegion leftUV{ 0.10f, 0.0f, 0.40f, 1.0f }; + const UVRegion rightUV{ 0.55f, 0.0f, 0.35f, 1.0f }; + json staged = { + { "CropX", leftUV.x }, + { "CropY", leftUV.y }, + { "CropW", leftUV.w }, + { "CropH", leftUV.h }, + { "CropRightX", rightUV.x }, + { "CropRightY", rightUV.y }, + { "CropRightW", rightUV.w }, + { "CropRightH", rightUV.h }, + { "CropPresets", json::array({ json{ + { "name", "Asymmetric" }, + { "uv", json::array({ leftUV.x, leftUV.y, leftUV.w, leftUV.h }) }, + { "right_uv", json::array({ rightUV.x, rightUV.y, rightUV.w, rightUV.h }) } }) } + }, + { "SelectedPresetIndex", 0 } + }; + src.LoadSettings(staged); + + json saved; + src.SaveSettings(saved); + REQUIRE(saved["CropPresets"].is_array()); + REQUIRE_FALSE(saved["CropPresets"].empty()); + for (const auto& entry : saved["CropPresets"]) { + REQUIRE(entry.contains("right_uv")); + } + + // Round-trip into a fresh controller and confirm the asymmetric right UV + // survived. Before the fix the saved preset's right_uv would be missing + // and LoadSettings would mirror left → right, equalizing the eyes. + Controller dst; + dst.SetStereoEnabled(true); + dst.LoadSettings(saved); + REQUIRE(UVApprox(dst.GetRightEyeUV(), rightUV)); +} + +TEST_CASE("MirrorUVHorizontal symmetry via SetStereoEnabled", "[subrect][stereo][math]") +{ + // {0.4, *, 0.6, *} mirrors to {0, *, 0.6, *} — the nose-side overlap case. + // Exercised through SetStereoEnabled since MirrorUVHorizontal is private. + Controller c; + json in = { + { "CropX", 0.4f }, + { "CropY", 0.0f }, + { "CropW", 0.6f }, + { "CropH", 1.0f }, + }; + c.LoadSettings(in); + REQUIRE_FALSE(c.IsStereoEnabled()); + + c.SetStereoEnabled(true); + // x = 1 - 0.4 - 0.6 = 0.0 + REQUIRE(UVApprox(c.GetRightEyeUV(), { 0.0f, 0.0f, 0.6f, 1.0f })); +} + +TEST_CASE("SetStereoEnabled preserves explicit right UV loaded earlier", "[subrect][stereo][regression]") +{ + // Regression for the call-order trap: LoadSettings (mono) → SetStereoEnabled(true) + // must NOT overwrite an explicit CropRight* value with the mirror of the left eye. + Controller c; + const UVRegion explicitRight{ 0.62f, 0.10f, 0.30f, 0.80f }; + json in = { + { "CropX", 0.10f }, + { "CropY", 0.00f }, + { "CropW", 0.40f }, + { "CropH", 1.00f }, + { "CropRightX", explicitRight.x }, + { "CropRightY", explicitRight.y }, + { "CropRightW", explicitRight.w }, + { "CropRightH", explicitRight.h }, + }; + c.LoadSettings(in); + REQUIRE_FALSE(c.IsStereoEnabled()); + + c.SetStereoEnabled(true); + REQUIRE(UVApprox(c.GetRightEyeUV(), explicitRight)); +} + +TEST_CASE("Reload without CropRight* re-enables auto-mirror", "[subrect][stereo][regression]") +{ + // The rightUVLoadedFromJson flag must reset every LoadSettings — otherwise + // once a config with CropRight* was loaded, later loads without those + // keys would keep suppressing the mirror. + Controller c; + const UVRegion explicitRight{ 0.62f, 0.10f, 0.30f, 0.80f }; + json withRight = { + { "CropX", 0.10f }, { "CropY", 0.00f }, { "CropW", 0.40f }, { "CropH", 1.00f }, + { "CropRightX", explicitRight.x }, { "CropRightY", explicitRight.y }, + { "CropRightW", explicitRight.w }, { "CropRightH", explicitRight.h } + }; + c.LoadSettings(withRight); + + // Now load a fresh config WITHOUT CropRight*. The flag should reset to + // false so a subsequent SetStereoEnabled(true) auto-mirrors. + json withoutRight = { + { "CropX", 0.20f }, { "CropY", 0.00f }, { "CropW", 0.60f }, { "CropH", 1.00f } + }; + c.LoadSettings(withoutRight); + c.SetStereoEnabled(true); + // Mirror of {0.20, 0, 0.60, 1.0} is {1 - 0.20 - 0.60, *, 0.60, *} = {0.20, *, 0.60, *}. + REQUIRE(UVApprox(c.GetRightEyeUV(), { 0.20f, 0.0f, 0.60f, 1.0f })); +} + +TEST_CASE("SaveSettings erases stale CropRight* in mono mode", "[subrect][stereo][regression]") +{ + // When a host re-uses an in-memory JSON object that previously held + // stereo keys, the mono save must clear them or the next load looks + // like it still had explicit stereo data. + json carry = { + { "CropRightX", 0.5f }, { "CropRightY", 0.0f }, + { "CropRightW", 0.5f }, { "CropRightH", 1.0f } + }; + Controller c; + REQUIRE_FALSE(c.IsStereoEnabled()); + c.SaveSettings(carry); + REQUIRE_FALSE(carry.contains("CropRightX")); + REQUIRE_FALSE(carry.contains("CropRightY")); + REQUIRE_FALSE(carry.contains("CropRightW")); + REQUIRE_FALSE(carry.contains("CropRightH")); +} + +TEST_CASE("GetStereoPixelRegions returns empty for degenerate dimensions", "[subrect][stereo][edge]") +{ + // fullWidth/2 == 0 for widths 0 or 1; UVToPixelRegion's `width - 1` + // would underflow into a huge coord without the guard. + Controller c; + c.SetStereoEnabled(true); + for (auto [w, h] : std::vector>{ { 0, 100 }, { 1, 100 }, { 100, 0 } }) { + const auto regions = c.GetStereoPixelRegions(w, h); + REQUIRE(regions.leftEye.w == 0); + REQUIRE(regions.rightEye.w == 0); + } +} + +TEST_CASE("Seeded preset without explicit rightUV auto-mirrors in stereo", "[subrect][stereo][regression]") +{ + // Regression for the silent-full-frame bug: a Preset built with only + // .name + .uv (rightUV omitted, defaulting to std::nullopt) should + // auto-mirror the left eye when stereo is enabled, NOT show full frame + // for the right eye. + Controller c; + c.SetStereoEnabled(true); + c.SeedDefaultPresets({ + Preset{ .name = "Left Half", .uv = { 0.0f, 0.0f, 0.5f, 1.0f } }, + }); + c.LoadSettings(json::object()); + + // Mirror of {0, 0, 0.5, 1.0} around x=0.5 is {0.5, 0, 0.5, 1.0}. + REQUIRE(UVApprox(c.GetRightEyeUV(), { 0.5f, 0.0f, 0.5f, 1.0f })); +} + +TEST_CASE("Partial CropRight* keys still allow auto-mirror", "[subrect][stereo][regression]") +{ + // Only one of the four right-eye keys was provided. The old OR semantics + // would mark this as "explicit right eye" and suppress the mirror on + // SetStereoEnabled(true), leaving currentRightUV with mixed stale + loaded + // components. The fixed AND semantics treat partial as not-explicit so + // the mirror still runs. + Controller c; + json partial = { + { "CropX", 0.20f }, { "CropY", 0.00f }, { "CropW", 0.60f }, { "CropH", 1.00f }, + { "CropRightW", 0.50f } // a single right-eye key — incomplete quartet + }; + c.LoadSettings(partial); + c.SetStereoEnabled(true); + // Mirror of {0.20, 0, 0.60, 1.0} is {0.20, 0, 0.60, 1.0} — confirms the + // mirror ran rather than landing the half-loaded right UV. + REQUIRE(UVApprox(c.GetRightEyeUV(), { 0.20f, 0.0f, 0.60f, 1.0f })); +} + +TEST_CASE("Malformed preset right_uv falls back to auto-mirror", "[subrect][stereo][regression]") +{ + // LoadUVArray returns a default full-frame UV on malformed input. Without + // shape validation, a bad `right_uv` payload would land the right eye as + // full-frame AND suppress the auto-mirror — the worst of both worlds. + // With validation, malformed input is ignored and the mirror takes over. + Controller c; + c.SetStereoEnabled(true); + json bad = { + { "CropPresets", json::array({ json{ + { "name", "Bad" }, + { "uv", { 0.10f, 0.0f, 0.40f, 1.0f } }, + { "right_uv", "not an array" } // malformed + } }) }, + { "SelectedPresetIndex", 0 } + }; + c.LoadSettings(bad); + // Auto-mirror of {0.10, 0, 0.40, 1.0} = {0.50, 0, 0.40, 1.0}, NOT {0,0,1,1}. + REQUIRE(UVApprox(c.GetRightEyeUV(), { 0.50f, 0.0f, 0.40f, 1.0f })); +} From dc968783d9a622af274a1ae7bf485e60192db2d2 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Fri, 22 May 2026 02:06:23 -0700 Subject: [PATCH 11/24] test: fix cpp_tests build and add CI gate (#32) Co-authored-by: Claude Opus 4.7 --- .github/workflows/_shared-build.yaml | 72 ++++++++++++++++++++++++++++ .github/workflows/pr-checks.yaml | 8 ++++ tests/cpp/test_subrect.cpp | 32 +++++++++---- 3 files changed, 102 insertions(+), 10 deletions(-) diff --git a/.github/workflows/_shared-build.yaml b/.github/workflows/_shared-build.yaml index ecf4319149..45af3d43a6 100644 --- a/.github/workflows/_shared-build.yaml +++ b/.github/workflows/_shared-build.yaml @@ -23,6 +23,10 @@ on: description: "Run shader unit tests" type: boolean default: true + run-cpp-tests: + description: "Run C++ unit tests (tests/cpp)" + type: boolean + default: true hlsl-should-build: description: "Passed to check-hlsl-changes; 'true' forces shader steps to run" type: string @@ -31,6 +35,10 @@ on: description: "Passed to check-hlsl-changes for unit tests" type: string default: "true" + cpp-tests-should-build: + description: "Forces cpp_tests build+run when 'true'; lets PR-checks skip it otherwise" + type: string + default: "true" cache-key-suffix: description: "Optional suffix to invalidate the build cache" type: string @@ -259,3 +267,67 @@ jobs: build/ALL/Testing/** retention-days: 7 if-no-files-found: ignore + + cpp-unit-tests: + name: Run C++ Unit Tests + if: inputs.run-cpp-tests + runs-on: windows-2025 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository }} + submodules: recursive + + # Inline gate (no composite action — the logic is one branch). + # PR events skip when no relevant files changed; push / dispatch / + # release always run. + - name: Check if cpp_tests should run + id: check-cpp + shell: bash + run: | + if [ "${{ github.event_name }}" != "pull_request_target" ]; then + echo "Non-PR event, proceeding." + echo "skip=false" >> $GITHUB_OUTPUT + elif [ "${{ inputs.cpp-tests-should-build }}" != "true" ]; then + echo "No cpp_tests-related changes detected, skipping." + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "cpp_tests-related changes detected, proceeding." + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Setup Build Environment + id: setup + if: steps.check-cpp.outputs.skip != 'true' + uses: ./.github/actions/setup-build-environment + with: + cache-key-suffix: "cpp-tests${{ inputs.cache-key-suffix }}" + cmake-preset: "ALL" + build-dir: "build/ALL" + + - name: Build cpp_tests + if: steps.check-cpp.outputs.skip != 'true' + uses: lukka/run-cmake@v10 + with: + configurePreset: ALL + buildPreset: ALL + buildPresetAdditionalArgs: "['--target cpp_tests']" + + - name: Run C++ unit tests + if: steps.check-cpp.outputs.skip != 'true' + run: | + ctest --test-dir build/ALL -C Release --output-on-failure -R CppUtilTests --timeout 60 + + - name: Upload test results on failure + if: failure() && steps.check-cpp.outputs.skip != 'true' + uses: actions/upload-artifact@v7 + with: + name: cpp-test-results + path: | + build/ALL/Testing/** + retention-days: 7 + if-no-files-found: ignore diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index 683e73ef5c..26e255be6f 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -54,6 +54,10 @@ jobs: should-build: ${{ steps.changed-files.outputs.build_any_changed == 'true' || steps.changed-files.outputs.cpp_any_changed == 'true' || steps.changed-files.outputs.build_ci_any_changed == 'true' || steps.changed-files.outcome == 'failure' }} hlsl-should-build: ${{ steps.changed-files.outputs.hlsl_any_changed == 'true' || steps.changed-files.outputs.cmake_any_changed == 'true' || steps.changed-files.outputs.build_ci_any_changed == 'true' || steps.changed-files.outcome == 'failure' }} shader-tests-should-build: ${{ steps.changed-files.outputs.shader_tests_any_changed == 'true' || steps.changed-files.outputs.hlsl_any_changed == 'true' || steps.changed-files.outputs.cmake_any_changed == 'true' || steps.changed-files.outputs.build_ci_any_changed == 'true' || steps.changed-files.outcome == 'failure' }} + # cpp_tests target compiles src/Utils/Subrect.cpp directly (see tests/cpp/CMakeLists.txt), + # so any cpp_any change is conservative-but-correct. cpp_tests_any catches changes to + # the test sources themselves. cmake/build_ci cover CMake + CI plumbing. + cpp-tests-should-build: ${{ steps.changed-files.outputs.cpp_tests_any_changed == 'true' || steps.changed-files.outputs.cpp_any_changed == 'true' || steps.changed-files.outputs.cmake_any_changed == 'true' || steps.changed-files.outputs.build_ci_any_changed == 'true' || steps.changed-files.outcome == 'failure' }} steps: - uses: actions/checkout@v6 with: @@ -104,6 +108,8 @@ jobs: - 'features/**/Shaders/**' shader_tests: - 'tests/shaders/**' + cpp_tests: + - 'tests/cpp/**' base_sha: ${{ github.event.pull_request.base.sha }} sha: ${{ github.event.pull_request.head.sha }} @@ -125,8 +131,10 @@ jobs: run-cpp: ${{ needs.check-changes.outputs.should-build != 'false' }} run-shader-validation: ${{ needs.check-changes.outputs.hlsl-should-build != 'false' }} run-shader-tests: ${{ needs.check-changes.outputs.shader-tests-should-build != 'false' }} + run-cpp-tests: ${{ needs.check-changes.outputs.cpp-tests-should-build != 'false' }} hlsl-should-build: ${{ needs.check-changes.outputs.hlsl-should-build || 'true' }} shader-tests-should-build: ${{ needs.check-changes.outputs.shader-tests-should-build || 'true' }} + cpp-tests-should-build: ${{ needs.check-changes.outputs.cpp-tests-should-build || 'true' }} # Security: this job uses GITHUB_TOKEN with write permissions but does NOT execute # fork code — it only downloads pre-built artifacts from the build job above. diff --git a/tests/cpp/test_subrect.cpp b/tests/cpp/test_subrect.cpp index fdfc1ce1e1..b4908c8632 100644 --- a/tests/cpp/test_subrect.cpp +++ b/tests/cpp/test_subrect.cpp @@ -215,6 +215,18 @@ TEST_CASE("Stereo SaveSettings emits right_uv for every preset", "[subrect][ster // preserved" from "default fallback mirrored it back". const UVRegion leftUV{ 0.10f, 0.0f, 0.40f, 1.0f }; const UVRegion rightUV{ 0.55f, 0.0f, 0.35f, 1.0f }; + + // Build the preset object via direct mutation rather than a single nested + // initializer list. MSVC's C++23 module-aware parse cannot disambiguate + // `json::array({ json{ {k,v}, {k,v} } })` from the + // `initializer_list>` overload of `json`'s + // constructor, producing a misleading C3329 at the inner closing `)`. + // Building element-by-element sidesteps the ambiguity entirely. + json preset = json::object(); + preset["name"] = "Asymmetric"; + preset["uv"] = json::array({ leftUV.x, leftUV.y, leftUV.w, leftUV.h }); + preset["right_uv"] = json::array({ rightUV.x, rightUV.y, rightUV.w, rightUV.h }); + json staged = { { "CropX", leftUV.x }, { "CropY", leftUV.y }, @@ -224,11 +236,7 @@ TEST_CASE("Stereo SaveSettings emits right_uv for every preset", "[subrect][ster { "CropRightY", rightUV.y }, { "CropRightW", rightUV.w }, { "CropRightH", rightUV.h }, - { "CropPresets", json::array({ json{ - { "name", "Asymmetric" }, - { "uv", json::array({ leftUV.x, leftUV.y, leftUV.w, leftUV.h }) }, - { "right_uv", json::array({ rightUV.x, rightUV.y, rightUV.w, rightUV.h }) } }) } - }, + { "CropPresets", json::array({ preset }) }, { "SelectedPresetIndex", 0 } }; src.LoadSettings(staged); @@ -392,12 +400,16 @@ TEST_CASE("Malformed preset right_uv falls back to auto-mirror", "[subrect][ster // With validation, malformed input is ignored and the mirror takes over. Controller c; c.SetStereoEnabled(true); + + // Same C++23-modules-friendly construction as above (see the regression + // test for the rationale). + json badPreset = json::object(); + badPreset["name"] = "Bad"; + badPreset["uv"] = json::array({ 0.10f, 0.0f, 0.40f, 1.0f }); + badPreset["right_uv"] = "not an array"; // malformed + json bad = { - { "CropPresets", json::array({ json{ - { "name", "Bad" }, - { "uv", { 0.10f, 0.0f, 0.40f, 1.0f } }, - { "right_uv", "not an array" } // malformed - } }) }, + { "CropPresets", json::array({ badPreset }) }, { "SelectedPresetIndex", 0 } }; c.LoadSettings(bad); From a3e68eaaf87acea7da625e2ee2725b954be9b67d Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Fri, 22 May 2026 02:07:05 -0700 Subject: [PATCH 12/24] build: drop CORE marker from per-feature AIO copy to avoid race (#33) Co-authored-by: Claude Opus 4.7 --- .claude/CLAUDE.md | 33 ++++++++++++++++++++++++++++++--- CMakeLists.txt | 10 +++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index f6d340354d..1a4f89c619 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -460,13 +460,40 @@ Follow conventional commit format for consistency: - **Format**: `type(scope): description` - **Title Limit**: 50 characters maximum - **Body Wrap**: 72 characters per line -- **Types**: `feat`, `fix`, `refactor`, `docs`, `style`, `test`, `chore` - **Examples**: - `feat(menu): extract DrawMenuVisitor helper methods` - `fix(imgui): resolve orphaned TableNextColumn calls` - `refactor(constants): centralize UI constants in ThemeManager` - -Conventional commits drive semantic-release. `feat:` triggers a minor bump, `fix:` triggers a patch bump, `feat!:` or `BREAKING CHANGE:` triggers a major bump. `chore:`, `docs:`, `style:`, `test:`, `refactor:` produce no release on their own. Pick the type with the version impact in mind — a refactor mislabeled `feat:` will force a minor bump on the next release. + - `ci: gate cpp_tests build on changed files` + - `build: drop CORE marker from per-feature AIO copy` + - `test: fix cpp_tests build under MSVC C++23 modules` + +**Squash-merge note.** PRs are squash-merged, so the **PR title** becomes the commit message that semantic-release reads. Getting the title's type right matters more than per-commit messages on the PR branch — those get discarded. Use `gh pr edit --title "..."` to fix a stale title before merge. + +**Type → release impact** (the full set accepted by `amannn/action-semantic-pull-request@v5` + `@semantic-release/commit-analyzer` defaults): + +| Type | Use for | Release impact | +| ---------- | --------------------------------------------------------- | ----------------------- | +| `feat` | New user-facing feature or capability | **minor** (1.X.0) | +| `fix` | Bug fix to user-facing behavior | **patch** (1.5.X) | +| `perf` | Performance improvement to user-facing behavior | **patch** (1.5.X) | +| `revert` | Revert of a prior commit | follows reverted commit | +| `build` | Build system, packaging, dependencies (CMake, vcpkg, AIO) | none | +| `chore` | Maintenance, misc tooling, repo hygiene | none | +| `ci` | CI workflows, GitHub Actions, lint configs | none | +| `docs` | Documentation, comments, READMEs, CLAUDE.md | none | +| `refactor` | Code restructuring with no behavior change | none | +| `style` | Formatting, whitespace, missing semicolons | none | +| `test` | Tests, test fixtures, test infrastructure | none | + +Append `!` to the type (or add a `BREAKING CHANGE:` footer) for **major** (X.0.0). + +**Pick the type with version impact in mind.** Common traps: + +- A pure build/CI/test change mislabeled `fix:` will burn a patch release on a non-user-visible change. Use `build:`, `ci:`, or `test:` instead. +- A refactor mislabeled `feat:` will force a minor bump. +- A perf win on internal code (not exposed to users) is `refactor:`, not `perf:`. +- `chore:` is a catch-all; prefer the specific type when one fits. ### Release Branch Model diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d61d07fb7..85d99e83d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -605,6 +605,12 @@ if(AUTO_PLUGIN_DEPLOYMENT OR AIO_ZIP_TO_DIST) # Copy feature folders (files only; copy_shaders.stamp owns Shaders/ # to avoid racing). All features copied — autoupload filter applies # only at archive time. + # + # The CORE marker is excluded from the per-feature copy: every feature + # that has one would write to the same `${AIO_DIR}/CORE` path, and the + # trailing `remove` below deletes it anyway. When MSBuild runs the copy + # commands in parallel two features can race for the write and one + # fails with "Permission denied", taking PREPARE_AIO down with it. foreach(_fpath IN LISTS FEATURE_PATHS) if(EXISTS "${_fpath}") file( @@ -613,11 +619,13 @@ if(AUTO_PLUGIN_DEPLOYMENT OR AIO_ZIP_TO_DIST) "${_fpath}/*" ) list(FILTER _feature_files EXCLUDE REGEX "/Shaders/") + list(FILTER _feature_files EXCLUDE REGEX "/CORE$") append_copy_if_different(_prepare_aio_cmds _feature_files "${_fpath}" "${AIO_DIR}") endif() endforeach() - # Remove CORE from AIO if it exists (keep rest intact) + # Remove CORE from AIO if it's left over from a previous build that pre- + # dated the per-feature exclusion above. Cheap, idempotent. list( APPEND _prepare_aio_cmds COMMAND From 37dfdeebaf419db6d15a1680f44d17fb044f66c0 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 23 May 2026 21:07:49 -0700 Subject: [PATCH 13/24] fix(vr): full-resolution underwater mask (#34) Co-authored-by: ParticleTroned <248299730+ParticleTroned@users.noreply.github.com> --- src/Features/Upscaling.cpp | 70 +++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index b3a0190b6c..8f41bfcd91 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -1819,7 +1819,20 @@ void Upscaling::UpscaleDepth() // 3) Resource copies are skipped for aliased src/dst to reduce copy churn. // (1) Early validation exits - if (!IsUpscalingActive()) { + const bool depthUpscaleActive = IsUpscalingActive(); + const auto upscaleMethod = GetUpscaleMethod(); + const bool isVR = globals::game::isVR; + const bool vendorUpscaler = upscaleMethod == UpscaleMethod::kDLSS || upscaleMethod == UpscaleMethod::kFSR; + const bool fullResolutionMaskPath = + upscaleMethod == UpscaleMethod::kNONE || + upscaleMethod == UpscaleMethod::kTAA || + (vendorUpscaler && settings.qualityMode == 0); + const bool repairVRFullResolutionMask = + isVR && + fullResolutionMaskPath && + !depthUpscaleActive; + + if (!depthUpscaleActive && !repairVRFullResolutionMask) { return; } @@ -1827,7 +1840,8 @@ void Upscaling::UpscaleDepth() auto renderer = globals::game::renderer; auto context = globals::d3d::context; auto deferred = globals::deferred; - if (!state || !renderer || !context || !deferred || !deferred->linearSampler || !jitterCB || !upscaleRasterizerState || !upscaleBlendState || !upscaleDepthStencilState) { + if (!state || !renderer || !context || !deferred || !deferred->linearSampler || !jitterCB || !upscaleRasterizerState || !upscaleBlendState || + (depthUpscaleActive && !upscaleDepthStencilState)) { return; } @@ -1842,24 +1856,39 @@ void Upscaling::UpscaleDepth() auto& saoCameraZ = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kSAO_CAMERAZ]; auto& underwaterMask = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kUNDERWATER_MASK]; - if (!depth.texture || !depth.views[0] || !depthCopy.texture || !depthCopy.depthSRV || - !refractionNormals.texture || !refractionNormals.textureCopy || !refractionNormals.SRVCopy || !refractionNormals.RTV || !saoCameraZ.RTV || + if (!depth.texture || !depthCopy.texture || !depthCopy.depthSRV || !underwaterMask.texture || !underwaterMask.textureCopy || !underwaterMask.SRVCopy || !underwaterMask.RTV) { return; } - if (globals::game::isVR && (!depthCopy.views[0] || !depthCopy.stencilSRV)) { + if (depthUpscaleActive && + (!depth.views[0] || !refractionNormals.texture || !refractionNormals.textureCopy || !refractionNormals.SRVCopy || !refractionNormals.RTV || !saoCameraZ.RTV)) { + return; + } + // stencilSRV + views[0] are both upscale-path-only: the depth-upscale + // draw binds depthCopy as a stencil SRV input and depth.views[0] as DSV. + // The full-resolution mask repair never touches either, so don't disable + // the VR fix on setups where stencil SRV creation is unavailable. + if (depthUpscaleActive && isVR && (!depthCopy.stencilSRV || !depthCopy.views[0])) { return; } auto* fullscreenVS = GetUpscaleVS(); - auto* depthUpscalePS = GetDepthRefractionUpscalePS(); + auto* depthUpscalePS = depthUpscaleActive ? GetDepthRefractionUpscalePS() : nullptr; auto* underwaterMaskPS = GetUnderwaterMaskUpscalePS(); - if (!fullscreenVS || !depthUpscalePS || !underwaterMaskPS) { + if (!fullscreenVS || !underwaterMaskPS || (depthUpscaleActive && !depthUpscalePS)) { return; } state->BeginPerfEvent("Render Target Upscaling"); + // Unbind any prior render targets before issuing CopyResource on depth/ + // depthCopy. Upscale() does this for the standard upscale path, but + // UpscaleDepth() can now be invoked standalone from Main_PostProcessing + // (kNONE/kTAA VR path) without going through Upscale() first — match the + // same precondition here to avoid a debug-layer hazard when depth happens + // to still be bound as a DSV from a prior pass. + context->OMSetRenderTargets(0, nullptr, nullptr); + // Set up Input Assembler for fullscreen triangle (no vertex/index buffers needed) context->IASetInputLayout(nullptr); context->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); @@ -1887,10 +1916,10 @@ void Upscaling::UpscaleDepth() context->PSSetSamplers(0, ARRAYSIZE(samplers), samplers); // Set up jitter/depth-kernel constant buffer for upscaling - JitterCB jitterData; + JitterCB jitterData{}; jitterData.jitter = jitter; // (2) Wide-kernel hysteresis - { + if (depthUpscaleActive) { constexpr float kEnterWideKernelRatio = 1.55f; constexpr float kExitWideKernelRatio = 1.45f; const float minScale = std::max(std::min(resolutionScale.x, resolutionScale.y), FLT_EPSILON); @@ -1907,7 +1936,6 @@ void Upscaling::UpscaleDepth() } jitterData.useWideKernel = depthUpscaleUseWideKernel ? 1.0f : 0.0f; - jitterData.pad0 = 0.0f; } jitterCB->Update(jitterData); @@ -1921,7 +1949,7 @@ void Upscaling::UpscaleDepth() } }; - { + if (depthUpscaleActive) { TracyD3D11Zone(globals::state->tracyCtx, "Upscaling - Depth Upscale"); // Sometimes this is not already copied e.g. map menu. @@ -1929,7 +1957,7 @@ void Upscaling::UpscaleDepth() copyIfNonAliased(depthCopy.texture, depth.texture); // Clear stencil to be 0xFF - if (globals::game::isVR) { + if (isVR) { context->ClearDepthStencilView(depthCopy.views[0], D3D11_CLEAR_STENCIL, 1.0f, 0xFF); } @@ -1944,11 +1972,16 @@ void Upscaling::UpscaleDepth() // kSAO_CAMERAZ is at quarter-stereo resolution in VR; the full-stereo viewport would // corrupt only the top-left quarter. The engine's ISSAOCameraZ pass populates it correctly. ID3D11RenderTargetView* rtvs[] = { refractionNormals.RTV, - globals::game::isVR ? nullptr : saoCameraZ.RTV }; + isVR ? nullptr : saoCameraZ.RTV }; context->OMSetRenderTargets(2, rtvs, depth.views[0]); context->PSSetShader(depthUpscalePS, nullptr, 0); context->Draw(3, 0); + } else { + TracyD3D11Zone(globals::state->tracyCtx, "Upscaling - Full Resolution Underwater Mask Depth Copy"); + + // Full-resolution paths only need to refresh the underwater mask depth source. + copyIfNonAliased(depthCopy.texture, depth.texture); } { @@ -1974,8 +2007,10 @@ void Upscaling::UpscaleDepth() context->Draw(3, 0); } - // Now propagate the upscaled depth to kMAIN_COPY so downstream VR passes see it. - if (globals::game::isVR) { + // Propagate the upscaled depth to kMAIN_COPY so downstream VR passes see + // it. Skipped on the full-resolution path because the else branch above + // already refreshed depthCopy from depth and nothing has touched it since. + if (isVR && depthUpscaleActive) { TracyD3D11Zone(globals::state->tracyCtx, "Upscaling - Depth VR Propagate"); copyIfNonAliased(depthCopy.texture, depth.texture); } @@ -2045,8 +2080,11 @@ void Upscaling::Main_PostProcessing::thunk(RE::ImageSpaceManager* a_this, uint32 if (upscaling.ShouldUseFrameGenerationThisFrame()) upscaling.CopySharedD3D12Resources(); - if (upscaleMethod != UpscaleMethod::kNONE && upscaleMethod != UpscaleMethod::kTAA) + if (upscaleMethod != UpscaleMethod::kNONE && upscaleMethod != UpscaleMethod::kTAA) { upscaling.PerformUpscaling(); + } else if (globals::game::isVR) { + upscaling.UpscaleDepth(); + } if (upscaleMethod == UpscaleMethod::kDLSS) upscaling.ApplySharpening(); From b84222bea6b2a1b58d4d771d371b7dec910c99f6 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 23 May 2026 23:27:13 -0700 Subject: [PATCH 14/24] feat(llf): restore contact shadows with VR-aware noise (#36) Co-authored-by: jiayev Co-authored-by: ParticleTroned <248299730+ParticleTroned@users.noreply.github.com> --- .../Shaders/Features/LightLimitFix.ini | 2 +- .../Shaders/LightLimitFix/LightLimitFix.hlsli | 71 +++++++++++++++++++ package/Shaders/Common/SharedData.hlsli | 3 +- package/Shaders/Lighting.hlsl | 42 ++++++++++- src/Features/LightLimitFix.cpp | 11 ++- src/Features/LightLimitFix.h | 4 +- 6 files changed, 127 insertions(+), 6 deletions(-) diff --git a/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini b/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini index 7b3c6c9135..21a23ad267 100644 --- a/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini +++ b/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini @@ -1,5 +1,5 @@ [Info] -Version = 3-0-3 +Version = 3-1-0 [Nexus] autoupload = false diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli b/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli index c30b35ed57..3527bef030 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli +++ b/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli @@ -37,6 +37,77 @@ namespace LightLimitFix return true; } + bool IsSaturated(float value) + { + return value == saturate(value); + } + + bool IsSaturated(float2 value) + { + return IsSaturated(value.x) && IsSaturated(value.y); + } + + // Chooses the contact-shadow noise sample coordinate. In VR we derive it + // from screenUV (which FrameBuffer::ViewToUV already returns per-eye via + // CameraProj[eye]) so both eyes sample the same noise pattern at the same + // world position — using the raw rasterized pixel position in VR makes + // each eye hash a different value, producing per-eye jitter that reads as + // flicker on contact-shadow recipients. + // + // BufferDim.x is the full packed stereo width (State::UpdateSharedData + // reads it from the kMAIN texture, which spans both eyes side-by-side), + // so we halve X in VR to match the per-eye pixel grid. Without the + // halving, the per-eye sample steps by ~2 pixels in X — still stereo- + // consistent, but at half the effective noise resolution. Flat keeps the + // raw pixel position to match the original implementation byte-for-byte. + float2 GetContactShadowNoiseCoord(float2 screenPosition, float2 screenUV) + { +#if defined(VR) + return screenUV * float2(SharedData::BufferDim.x * 0.5, SharedData::BufferDim.y); +#else + return screenPosition; +#endif + } + + float ContactShadows(float3 viewPosition, float noise2D, float3 lightDirectionVS, uint contactShadowSteps, uint a_eyeIndex = 0) + { + if (contactShadowSteps == 0) + return 1.0; + + float2 depthDeltaMult = float2(0.20, 0.05); + + // Extend contact shadow distance + lightDirectionVS *= 2.0; + + // Offset starting position with interleaved gradient noise + viewPosition += lightDirectionVS * noise2D; + + // Accumulate samples + float contactShadow = 0.0; + for (uint i = 0; i < contactShadowSteps; i++) { + // Step the ray + viewPosition += lightDirectionVS; + + float2 rayUV = FrameBuffer::ViewToUV(viewPosition, true, a_eyeIndex); + + // Ensure the UV coordinates are inside the screen + if (!IsSaturated(rayUV)) + break; + + // Compute the difference between the ray's and the camera's depth + float rayDepth = SharedData::GetScreenDepth(rayUV, a_eyeIndex); + + // Difference between the current ray distance and the marched light + float depthDelta = viewPosition.z - rayDepth; + if (rayDepth > 16.5) // First person + contactShadow = max(contactShadow, saturate(depthDelta * depthDeltaMult.x) - saturate(depthDelta * depthDeltaMult.y)); + if (contactShadow == 1.0) + break; + } + + return 1.0 - saturate(contactShadow); + } + bool IsLightIgnored(Light light) { if (light.lightFlags & LightLimitFix::LightFlags::Shadow) { diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 8168507571..13bb01e239 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -75,9 +75,10 @@ namespace SharedData struct LightLimitFixSettings { + uint EnableContactShadows; uint EnableLightsVisualisation; uint LightsVisualisationMode; - float2 pad0; + float pad0; uint4 ClusterSize; }; diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 953b09e13d..55b1c21975 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -2678,6 +2678,24 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lightOffset = LightLimitFix::lightGrid[clusterIndex].offset; } +# if defined(DEFERRED) + // Contact-shadow setup, gated on the runtime toggle so we don't pay the + // noise hash + step-count math for every pixel when the feature is off + // (it defaults off). The step count and noise are reused across every + // clustered light in this pixel so we hoist them out of the per-light loop. + uint contactShadowSteps = 0; + float contactShadowNoise = 0.0; + [branch] if (SharedData::lightLimitFixSettings.EnableContactShadows) + { + contactShadowSteps = round(4.0 * (1.0 - saturate(viewPosition.z / 1024.0))); + // The helper stays stereo-stable in VR — see + // LightLimitFix::GetContactShadowNoiseCoord for the eye-buffer math. + contactShadowNoise = Random::InterleavedGradientNoise( + LightLimitFix::GetContactShadowNoiseCoord(input.Position.xy, screenUV), + SharedData::FrameCount); + } +# endif + [loop] for (uint lightIndex = 0; lightIndex < totalLightCount; lightIndex++) { LightLimitFix::Light light; @@ -2720,6 +2738,25 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 normalizedLightDirection = normalize(lightDirection); float lightAngle = dot(worldNormal.xyz, normalizedLightDirection.xyz); + float contactShadow = 1.0; + +# if defined(DEFERRED) + [branch] if ( + SharedData::lightLimitFixSettings.EnableContactShadows && + !(light.lightFlags & LightLimitFix::LightFlags::Simple) && + shadowComponent != 0.0 && + lightAngle > 0.0) + { + // The current LightLimitFix Light struct stores positionWS only; derive view-space + // from CameraView so the raymarch direction matches viewPosition. The pre-removal + // call site referenced light.positionVS, but that field did not exist on the Light + // struct even then — the original code was commented out and unreachable. + float3 lightPositionVS = mul(FrameBuffer::CameraView[eyeIndex], float4(light.positionWS[eyeIndex].xyz, 1)).xyz; + float3 normalizedLightDirectionVS = normalize(lightPositionVS - viewPosition.xyz); + contactShadow = LightLimitFix::ContactShadows(viewPosition, contactShadowNoise, normalizedLightDirectionVS, contactShadowSteps, eyeIndex); + } +# endif + float3 refractedLightDirection = normalizedLightDirection; # if defined(TRUE_PBR) && !defined(LANDSCAPE) && !defined(LODLANDSCAPE) [branch] if ((PBRFlags & PBR::Flags::InterlayerParallax) != 0) @@ -2736,7 +2773,8 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) SharedData::extendedMaterialSettings.EnableShadows && !(light.lightFlags & LightLimitFix::LightFlags::Simple) && lightAngle > 0.0 && - shadowComponent != 0.0) + shadowComponent != 0.0 && + contactShadow != 0.0) { float3 lightDirectionTS = normalize(mul(refractedLightDirection, tbn).xyz); # if defined(PARALLAX) @@ -2765,7 +2803,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) DirectContext pointLightContext; DirectLightingOutput pointLightOutput; - float pointLightShadow = lightShadow * parallaxShadow; + float pointLightShadow = lightShadow * parallaxShadow * contactShadow; # if defined(TRUE_PBR) pointLightContext = CreateDirectLightingContext(worldNormal.xyz, coatWorldNormal, vertexNormal.xyz, refractedViewDirection, viewDirection, refractedLightDirection, normalizedLightDirection, lightColor, pointLightShadow, pointLightShadow); # else diff --git a/src/Features/LightLimitFix.cpp b/src/Features/LightLimitFix.cpp index c7528fe44c..a18f52361c 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -3,10 +3,10 @@ #include "LinearLighting.h" #include "Menu/ThemeManager.h" -#include "Utils/ExternalEmittance.h" #include "Shadercache.h" #include "State.h" #include "Util.h" +#include "Utils/ExternalEmittance.h" static constexpr uint CLUSTER_MAX_LIGHTS = 128; static constexpr uint MAX_LIGHTS = 1024; @@ -21,6 +21,14 @@ void LightLimitFix::DrawSettings() ImGui::TreePop(); } + /////////////////////////////// + ImGui::SeparatorText("Shadows"); + + ImGui::Checkbox("Enable Contact Shadows", &settings.EnableContactShadows); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("All point lights (strict and clustered, except simple lights) cast short screen-space shadows. Performance impact."); + } + /////////////////////////////// ImGui::SeparatorText("Debug"); @@ -66,6 +74,7 @@ void LightLimitFix::DrawOverlay() LightLimitFix::PerFrame LightLimitFix::GetCommonBufferData() { PerFrame perFrame{}; + perFrame.EnableContactShadows = settings.EnableContactShadows; perFrame.EnableLightsVisualisation = settings.EnableLightsVisualisation; perFrame.LightsVisualisationMode = settings.LightsVisualisationMode; std::copy(clusterSize, clusterSize + 3, perFrame.ClusterSize); diff --git a/src/Features/LightLimitFix.h b/src/Features/LightLimitFix.h index b4a2dcd3dc..a1569f2de9 100644 --- a/src/Features/LightLimitFix.h +++ b/src/Features/LightLimitFix.h @@ -94,9 +94,10 @@ struct LightLimitFix : OverlayFeature struct alignas(16) PerFrame { + uint EnableContactShadows; uint EnableLightsVisualisation; uint LightsVisualisationMode; - float pad0[2]; + float pad0; uint ClusterSize[4]; }; STATIC_ASSERT_ALIGNAS_16(PerFrame); @@ -169,6 +170,7 @@ struct LightLimitFix : OverlayFeature struct Settings { + bool EnableContactShadows = false; bool EnableLightsVisualisation = false; uint LightsVisualisationMode = 0; }; From 77ae5342e3789dcd91a65f0146ed35d2416faf94 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 24 May 2026 11:00:37 -0700 Subject: [PATCH 15/24] feat(VR): run post-process at DLSS internal resolution (DLSSperf) (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds **DLSSperf** — a VR-only opt-in mode of the Upscaling feature. In Skyrim VR's standard DLSS pipeline, the engine allocates its render-target chain (kMAIN, depth, motion vectors, refraction, etc.) at the HMD's full native resolution. DLSS upscales the small render content into those large targets, and **the entire post-process chain — HDR, bloom, refraction, tonemap — then runs at the upscaled HMD resolution**. The post-process work is the most expensive part of the chain after world rendering, and most of it doesn't visibly benefit from being computed at the upscaled resolution. DLSSperf shrinks the engine's render targets to match DLSS's own internal (smaller) resolution. DLSS still reconstructs to HMD-native via a private `testTexture` used for OpenVR submit, but everything the engine itself draws and post-processes stays at the smaller working resolution. Decomposed from upstream PR-2096 (`YtzyFvra/feature/DLSSenhancer`). Co-authored-by: YtzyFvra <59631290+YtzyFvra@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 --- .../Upscaling/DLSSperf/BoxDownscalePS.hlsl | 35 + .../Upscaling/DLSSperf/MenuBGBlitPS.hlsl | 28 + src/Features/Upscaling.cpp | 269 +++- src/Features/Upscaling.h | 24 +- src/Features/Upscaling/DLSSperf.cpp | 1269 +++++++++++++++++ src/Features/Upscaling/DLSSperf.h | 384 +++++ src/Features/Upscaling/Streamline.cpp | 42 +- src/Globals.cpp | 11 + src/Hooks.cpp | 31 + src/Utils/Subrect.cpp | 1 - src/Utils/UI.cpp | 7 + src/Utils/UI.h | 2 + 12 files changed, 2073 insertions(+), 30 deletions(-) create mode 100644 features/Upscaling/Shaders/Upscaling/DLSSperf/BoxDownscalePS.hlsl create mode 100644 features/Upscaling/Shaders/Upscaling/DLSSperf/MenuBGBlitPS.hlsl create mode 100644 src/Features/Upscaling/DLSSperf.cpp create mode 100644 src/Features/Upscaling/DLSSperf.h diff --git a/features/Upscaling/Shaders/Upscaling/DLSSperf/BoxDownscalePS.hlsl b/features/Upscaling/Shaders/Upscaling/DLSSperf/BoxDownscalePS.hlsl new file mode 100644 index 0000000000..755a5778f6 --- /dev/null +++ b/features/Upscaling/Shaders/Upscaling/DLSSperf/BoxDownscalePS.hlsl @@ -0,0 +1,35 @@ +// BoxDownscalePS.hlsl — DLSSperf downscale pass +// Box 3×3 filter: testTexture (3k) → kMAIN (1k). +// For 3:1 downscale, each output pixel averages the 3×3 source region, +// ensuring all DLSS output pixels contribute (vs bilinear's 2×2 coverage). +// Reuses UpscaleVS.hlsl for fullscreen triangle generation (SV_VertexID). + +#include "Upscaling/UpscaleVS.hlsl" + +#if defined(PSHADER) + +typedef VS_OUTPUT PS_INPUT; + +SamplerState LinearSampler : register(s0); +Texture2D SourceTex : register(t0); + +float4 main(PS_INPUT input) : SV_Target +{ + float2 srcSize; + SourceTex.GetDimensions(srcSize.x, srcSize.y); + float2 texelSize = 1.0 / srcSize; + + // Clamp tap UVs to [0,1] so border pixels don't read across edges if the + // sampler ever gets created with wrap/mirror addressing instead of clamp. + // On clamp samplers this saturate() is a no-op the compiler can elide. + float4 sum = 0; + [unroll] for (int y = -1; y <= 1; y++) + [unroll] for (int x = -1; x <= 1; x++) + { + float2 uv = saturate(input.TexCoord + float2(x, y) * texelSize); + sum += SourceTex.SampleLevel(LinearSampler, uv, 0); + } + return sum * (1.0 / 9.0); +} + +#endif diff --git a/features/Upscaling/Shaders/Upscaling/DLSSperf/MenuBGBlitPS.hlsl b/features/Upscaling/Shaders/Upscaling/DLSSperf/MenuBGBlitPS.hlsl new file mode 100644 index 0000000000..2e946df518 --- /dev/null +++ b/features/Upscaling/Shaders/Upscaling/DLSSperf/MenuBGBlitPS.hlsl @@ -0,0 +1,28 @@ +// MenuBGBlitPS.hlsl — DLSSperf main-menu / loading-screen BG blit. +// Fullscreen 1:1 sample of the source texture into kTOTAL/kMENUBG. The +// caller (MaybeBlitMenuBG) feeds DLSS-reconstructed testTexture (R16G16 +// B16A16_FLOAT, displayRes) and the destination kTOTAL is R8G8B8A8_UNORM +// at the same dims — CopyResource can't do this because the formats +// differ, so a draw-based blit handles the implicit float→unorm +// conversion via the RTV format. +// +// Reuses UpscaleVS.hlsl for the fullscreen triangle and a linear clamp +// sampler. saturate() on UV is defense-in-depth for callers binding non- +// clamp samplers. + +#include "Upscaling/UpscaleVS.hlsl" + +#if defined(PSHADER) + +typedef VS_OUTPUT PS_INPUT; + +SamplerState LinearSampler : register(s0); +Texture2D SourceTex : register(t0); + +float4 main(PS_INPUT input) : + SV_Target +{ + return SourceTex.SampleLevel(LinearSampler, saturate(input.TexCoord), 0); +} + +#endif diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 8f41bfcd91..bcb02a3b06 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -4,6 +4,7 @@ #include "HDRDisplay.h" #include "Hooks.h" #include "State.h" +#include "Upscaling/DLSSperf.h" #include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" #include "Upscaling/Streamline.h" @@ -32,7 +33,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( reflexLowLatencyBoost, reflexUseMarkersToOptimize, reflexUseFPSLimit, - reflexFPSLimit); + reflexFPSLimit, + enableDLSSperf); decltype(&D3D11CreateDeviceAndSwapChain) ptrD3D11CreateDeviceAndSwapChainUpscaling; @@ -212,6 +214,29 @@ void Upscaling::DrawSettings() // Check the current upscale method auto upscaleMethod = GetUpscaleMethod(); + // DLSSperf: BSOpenVR size hook + RT::Create run once at world load, so + // runtime reads of method/qualityMode route through the boot snapshot. + // The always-present explanation is plain text — only the staged-change + // diff uses the RestartNeeded color so users learn the cue means "you + // changed something that won't apply yet." + if (dlssPerf.IsHookActive()) { + ImGui::TextWrapped( + "DLSSperf is active: Method and Upscale Preset changes only take effect after a game restart. " + "Sharpness / model preset / Reflex remain live."); + + // Method pending-diff. Only fires when the user is editing the DLSS- + // path mode slot (upscaleMethod, not upscaleMethodNoDLSS), since + // that's the one the boot snapshot locked. + if (currentUpscaleMode == &settings.upscaleMethod && + settings.upscaleMethod != dlssPerf.GetBootUpscaleMethod()) { + const uint live = std::clamp(settings.upscaleMethod, 0u, availableModes); + const uint boot = std::clamp(dlssPerf.GetBootUpscaleMethod(), 0u, availableModes); + Util::Text::RestartNeeded( + "Pending restart: currently active method = %s (selected = %s).", + upscaleModes[boot].c_str(), upscaleModes[live].c_str()); + } + } + // Display warning for DLSS resolution limits (non-VR only; VR handles this automatically) if (!globals::game::isVR && upscaleMethod == UpscaleMethod::kDLSS) { auto screenSize = globals::state->screenSize; @@ -244,10 +269,25 @@ void Upscaling::DrawSettings() } if (baseLabel) { - // Format the label with preset name and resolution scale - std::string labelWithScale = std::format("{} ( {:.2f}x )", baseLabel, (resolutionScale.x + resolutionScale.y) * 0.5f); + // Derive scale from live `settings.qualityMode` — `resolution- + // Scale` is locked to the DLSSperf boot snapshot, so reusing it + // here would mismatch the slider position the user sees. + const float displayScale = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)std::clamp(settings.qualityMode, 0u, 4u)); + std::string labelWithScale = std::format("{} ( {:.2f}x )", baseLabel, displayScale); ImGui::SliderInt("Upscale Preset", (int*)&settings.qualityMode, 0, 4, labelWithScale.c_str()); + + // Pending-diff vs the boot snapshot the runtime upscaler is + // actually using. Without this the slider change looks like a + // no-op. + if (dlssPerf.HasBootSnapshot() && + settings.qualityMode != dlssPerf.GetBootQualityMode()) { + const uint bm = std::clamp(dlssPerf.GetBootQualityMode(), 0u, 4u); + const char* bootLabel = (upscaleMethod == UpscaleMethod::kDLSS) ? upscalePresetsDLSS[std::clamp(4 - (int)bm, 0, 4)] : upscalePresets[std::clamp(4 - (int)bm, 0, 4)]; + Util::Text::RestartNeeded( + "Pending restart: currently active = %s ( %.2fx ). Change applies after game restart.", + bootLabel, 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)bm)); + } } if (upscaleMethod == UpscaleMethod::kFSR) { @@ -264,6 +304,42 @@ void Upscaling::DrawSettings() ImGui::Text("Changing this setting requires a restart to take effect."); } } + + // VR DLSSperf: opt-in performance feature. Lives in the main + // upscaler section (not Backend Diagnostics) so users discover it + // alongside the rest of the upscaler controls. Restart-gated — + // the BSOpenVR size hook reads this at world load and sizes every + // engine RT off the boot value. + // + // The setting persists across method switches (we don't auto-flip + // it when the user picks FSR/TAA), but the checkbox itself is + // disabled outside the DLSS context since the install path triple- + // gates on DLSS being the resolved method. Keep visible-but-greyed + // so users see the option exists and understand why it isn't live. + if (globals::game::isVR) { + const bool dlssAvailable = upscaleMethod == UpscaleMethod::kDLSS; + if (!dlssAvailable) + ImGui::BeginDisabled(); + ImGui::Checkbox("Render engine at upscaled resolution (DLSSperf)", &settings.enableDLSSperf); + if (!dlssAvailable) + ImGui::EndDisabled(); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "When enabled, the engine pipeline allocates render targets at the upscaled-render\n" + "resolution instead of the HMD display resolution. DLSS writes its output to a private\n" + "DisplayRes texture. Substantial VRAM and bandwidth savings, especially at high HMD\n" + "resolutions.\n" + "\n" + "Requires the DLSS upscaler. Restart required to enable/disable. Method and Upscale\n" + "Preset changes also require a restart while this is active; sharpness / model preset\n" + "/ Reflex remain live."); + } + if (!dlssAvailable && settings.enableDLSSperf) + Util::Text::Disabled("DLSSperf requires DLSS — switch upscaler Method to DLSS to activate."); + if (dlssAvailable && settings.enableDLSSperf != globals::features::upscaling.dlssPerf.IsHookActive()) + Util::Text::RestartNeeded("Pending restart: DLSSperf will %s on next launch.", + settings.enableDLSSperf ? "enable" : "disable"); + } } const bool frameGenerationDx12PathActive = IsFrameGenerationDx12PathActive(); @@ -636,6 +712,11 @@ void Upscaling::PostPostLoad() Upscaling::UpscaleMethod Upscaling::GetUpscaleMethod() const { + // Lock runtime to the boot upscaler under DLSSperf — engine RTs are + // sized for it, and routing a different method through testTexture/ + // renderRes paths breaks the HMD. + if (globals::features::upscaling.dlssPerf.HasBootSnapshot()) + return (UpscaleMethod)globals::features::upscaling.dlssPerf.GetBootUpscaleMethod(); if (streamline.featureDLSS) return (UpscaleMethod)settings.upscaleMethod; return (UpscaleMethod)settings.upscaleMethodNoDLSS; @@ -1015,8 +1096,15 @@ void Upscaling::EnsureVRIntermediateTextures() auto screenSize = globals::state->screenSize; auto renderSize = Util::ConvertToDynamic(screenSize); - uint32_t eyeWidthOut = (uint32_t)(screenSize.x / 2); - uint32_t eyeHeightOut = (uint32_t)screenSize.y; + // DLSSperf: state->screenSize is polluted to RenderRes (the BSOpenVR size + // hook spoofs HMD-recommended size). DLSS output needs to land at real + // DisplayRes, so size the OUTPUT intermediates from dlssPerf's snapshot + // of the true HMD resolution. Input intermediates stay at renderSize. + const bool dlssperfActive = dlssPerf.IsHookActive() && dlssPerf.GetTestTexture(); + const float2 outputSize = dlssperfActive ? dlssPerf.GetDisplayScreenSize() : screenSize; + + uint32_t eyeWidthOut = (uint32_t)(outputSize.x / 2); + uint32_t eyeHeightOut = (uint32_t)outputSize.y; uint32_t eyeWidthIn = (uint32_t)(renderSize.x / 2); uint32_t eyeHeightIn = (uint32_t)renderSize.y; @@ -1218,24 +1306,57 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) auto screenHeight = static_cast(screenSize.y); if (upscaleMethod != UpscaleMethod::kNONE && upscaleMethod != UpscaleMethod::kTAA) { - float resolutionScaleBase = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)settings.qualityMode); + // DLSSperf: when the BSOpenVR size hook is live, every engine RT was + // already allocated at RenderRes — so the DRS-style scale is identity. + // Jitter is still computed at the real DisplayRes phase ratio so DLSS + // has enough sub-pixel diversity for the upscale. + // + // The upscaleMethod here comes from GetUpscaleMethod(), which under + // DLSSperf+hookActive is locked to the boot snapshot — so this gate + // reads the value the user had selected at game start, not what they + // later moved the slider to. Engine RTs were sized off that boot + // choice (irreversible — the size hook can't un-allocate them); the + // boot-snapshot lock keeps the runtime DLSS evaluate consistent with + // those allocations. UI staged-change banners explain the restart + // requirement for method/quality edits. + if (dlssPerf.IsHookActive() && upscaleMethod == UpscaleMethod::kDLSS) { + resolutionScale = { 1.0f, 1.0f }; + + auto renderWidth = static_cast(dlssPerf.GetRenderEyeWidth()); + auto displayWidth = static_cast(dlssPerf.GetDisplayEyeWidth()); + + auto phaseCount = GetJitterPhaseCount(renderWidth, displayWidth); + GetJitterOffset(&jitter.x, &jitter.y, state->frameCount, phaseCount); + + if (globals::game::isVR) + a_viewport->projectionPosScaleX = -jitter.x / renderWidth; + else + a_viewport->projectionPosScaleX = -2.0f * jitter.x / renderWidth; - auto renderWidth = static_cast(screenWidth * resolutionScaleBase); - auto renderHeight = static_cast(screenHeight * resolutionScaleBase); + a_viewport->projectionPosScaleY = 2.0f * jitter.y / static_cast(dlssPerf.GetRenderEyeHeight()); + } else { + // Boot qualityMode under DLSSperf so projection stays coherent + // with the engine RTs sized at install. + const uint32_t qm = globals::features::upscaling.dlssPerf.HasBootSnapshot() ? globals::features::upscaling.dlssPerf.GetBootQualityMode() : settings.qualityMode; + float resolutionScaleBase = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)qm); - resolutionScale.x = static_cast(renderWidth) / static_cast(screenWidth); - resolutionScale.y = static_cast(renderHeight) / static_cast(screenHeight); + auto renderWidth = static_cast(screenWidth * resolutionScaleBase); + auto renderHeight = static_cast(screenHeight * resolutionScaleBase); - auto phaseCount = GetJitterPhaseCount(renderWidth, screenWidth); + resolutionScale.x = static_cast(renderWidth) / static_cast(screenWidth); + resolutionScale.y = static_cast(renderHeight) / static_cast(screenHeight); - GetJitterOffset(&jitter.x, &jitter.y, state->frameCount, phaseCount); + auto phaseCount = GetJitterPhaseCount(renderWidth, screenWidth); - if (globals::game::isVR) - a_viewport->projectionPosScaleX = -jitter.x / renderWidth; - else - a_viewport->projectionPosScaleX = -2.0f * jitter.x / renderWidth; + GetJitterOffset(&jitter.x, &jitter.y, state->frameCount, phaseCount); - a_viewport->projectionPosScaleY = 2.0f * jitter.y / renderHeight; + if (globals::game::isVR) + a_viewport->projectionPosScaleX = -jitter.x / renderWidth; + else + a_viewport->projectionPosScaleX = -2.0f * jitter.x / renderWidth; + + a_viewport->projectionPosScaleY = 2.0f * jitter.y / renderHeight; + } } else { resolutionScale = { 1.0f, 1.0f }; @@ -2021,6 +2142,101 @@ void Upscaling::UpscaleDepth() state->EndPerfEvent(); } +void Upscaling::RunUnderwaterMaskRepair() +{ + ZoneScoped; + TracyD3D11Zone(globals::state->tracyCtx, "Upscaling - Underwater Mask (Standalone)"); + + if (!globals::game::isVR) + return; + + auto state = globals::state; + auto renderer = globals::game::renderer; + auto context = globals::d3d::context; + auto deferred = globals::deferred; + if (!state || !renderer || !context || !deferred || !deferred->linearSampler || !jitterCB) { + return; + } + + auto screenSize = state->screenSize; + if (screenSize.x <= 0.0f || screenSize.y <= 0.0f) { + return; + } + + auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + auto& depthCopy = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN_COPY]; + auto& underwaterMask = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kUNDERWATER_MASK]; + if (!depth.texture || !depthCopy.texture || !depthCopy.depthSRV || + !underwaterMask.texture || !underwaterMask.textureCopy || !underwaterMask.SRVCopy || !underwaterMask.RTV) { + return; + } + + auto* fullscreenVS = GetUpscaleVS(); + auto* underwaterMaskPS = GetUnderwaterMaskUpscalePS(); + if (!fullscreenVS || !underwaterMaskPS) { + return; + } + + state->BeginPerfEvent("Underwater Mask Repair (Standalone)"); + + // Unbind RTs/DSV before the CopyResource calls below — if the caller + // still has depth bound as a DSV the copy is a debug-layer hazard. + // Mirrors UpscaleDepth's entry-time precondition. The caller's save/ + // restore (FullscreenPassScope) restores the original binding on exit. + context->OMSetRenderTargets(0, nullptr, nullptr); + + // Fullscreen triangle setup — pipeline state is the caller's + // responsibility to save/restore; we do not touch the existing OM + // bindings beyond the explicit binds below. + context->IASetInputLayout(nullptr); + context->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); + context->IASetIndexBuffer(nullptr, DXGI_FORMAT_UNKNOWN, 0); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + context->VSSetShader(fullscreenVS, nullptr, 0); + context->GSSetShader(nullptr, nullptr, 0); + context->HSSetShader(nullptr, nullptr, 0); + context->DSSetShader(nullptr, nullptr, 0); + + context->RSSetState(nullptr); + context->OMSetBlendState(nullptr, nullptr, 0xffffffff); + context->OMSetDepthStencilState(nullptr, 0x00); + + ID3D11SamplerState* samplers[] = { deferred->linearSampler }; + context->PSSetSamplers(0, ARRAYSIZE(samplers), samplers); + + // jitterCB is shared with the depth-upscale path; the mask shader only + // reads .jitter (de-jitter sampling). useWideKernel is depth-only. + JitterCB jitterData{}; + jitterData.jitter = jitter; + jitterCB->Update(jitterData); + auto bufferArray = jitterCB->CB(); + context->PSSetConstantBuffers(0, 1, &bufferArray); + + // Refresh depthCopy + underwater mask copy before sampling. + if (depthCopy.texture != depth.texture) + context->CopyResource(depthCopy.texture, depth.texture); + if (underwaterMask.textureCopy != underwaterMask.texture) + context->CopyResource(underwaterMask.textureCopy, underwaterMask.texture); + + D3D11_VIEWPORT viewport = {}; + viewport.Width = screenSize.x * 0.5f; + viewport.Height = screenSize.y * 0.5f; + viewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &viewport); + + ID3D11ShaderResourceView* srvs[] = { underwaterMask.SRVCopy, depthCopy.depthSRV }; + context->PSSetShaderResources(0, ARRAYSIZE(srvs), srvs); + ID3D11RenderTargetView* rtvs[] = { underwaterMask.RTV }; + context->OMSetRenderTargets(ARRAYSIZE(rtvs), rtvs, nullptr); + context->PSSetShader(underwaterMaskPS, nullptr, 0); + context->Draw(3, 0); + + ID3D11ShaderResourceView* nullPSResources[2] = { nullptr, nullptr }; + context->PSSetShaderResources(0, ARRAYSIZE(nullPSResources), nullPSResources); + + state->EndPerfEvent(); +} + void Upscaling::ApplySharpening() { ZoneScoped; @@ -2100,7 +2316,24 @@ void Upscaling::Main_PostProcessing::thunk(RE::ImageSpaceManager* a_this, uint32 if (hdrLoaded) globals::features::hdrDisplay.RedirectFramebuffer(); - func(a_this, a3, a_target, a_4, a_5); + // DLSSperf: hybrid Post — HandlePostProcessing performs a two-layer + // struct swap around the engine's func() so tonemap + refraction read + // the DisplayRes testTexture instead of the small kMAIN. The supplied + // lambda is the engine call we'd normally make directly. + // + // Upscaler gate: testTexture is only populated by DLSS's evaluate path + // (Streamline routes its colorOut there when DLSSperf is active). Under + // DLSSperf+hookActive, GetUpscaleMethod() returns the boot snapshot so + // this kDLSS check evaluates against the install-time choice — staged + // UI method changes don't reach here until restart. ShouldHandlePost() + // covers the partial-init case (post resources missing). + if (upscaleMethod == UpscaleMethod::kDLSS && globals::features::upscaling.dlssPerf.ShouldHandlePost()) { + globals::features::upscaling.dlssPerf.HandlePostProcessing([&]() { + func(a_this, a3, a_target, a_4, a_5); + }); + } else { + func(a_this, a3, a_target, a_4, a_5); + } // Restore kFRAMEBUFFER after ISHDR — hdrTexture now has the HDR scene if (hdrLoaded) diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index 2cc4b1297f..cc9abab8d7 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -1,6 +1,7 @@ #pragma once #include "Feature.h" +#include "Upscaling/DLSSperf.h" #include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" #include "Upscaling/RCAS/RCAS.h" @@ -68,6 +69,13 @@ struct Upscaling : Feature bool reflexUseMarkersToOptimize = false; bool reflexUseFPSLimit = false; float reflexFPSLimit = 60.0f; + + // VR DLSSperf: opt-in. When set, BSShaderRenderTargets::Create installs + // the BSOpenVR render-target-size hook at engine init so the entire + // engine pipeline allocates render targets at upscaled-render resolution + // instead of display resolution. Saves VRAM/bandwidth proportional to + // the quality-mode scale ratio. Requires a game restart to take effect. + bool enableDLSSperf = false; }; Settings settings; @@ -199,7 +207,8 @@ struct Upscaling : Feature static inline Streamline streamline; static inline FidelityFX fidelityFX; ///< Only for frame generation static inline DX12SwapChain dx12SwapChain; - static inline RCAS rcas; ///< Standalone RCAS sharpening for DLSS + static inline RCAS rcas; ///< Standalone RCAS sharpening for DLSS + static inline DLSSperf dlssPerf; ///< VR-only: render engine at upscaled-render res winrt::com_ptr copyDepthToSharedBufferPS; @@ -224,6 +233,19 @@ struct Upscaling : Feature void PerformUpscaling(); void UpscaleDepth(); + /** + * @brief Standalone full-resolution underwater mask repair (VR). + * + * Same draw as UpscaleDepth's mask branch on the full-resolution path, + * extracted so callers that bypass the standard upscale flow (notably + * DLSSperf::HandlePostProcessing, where engine RTs are pre-shrunk to + * renderRes and DLSS targets a private displayRes texture) can drive + * the repair without going through UpscaleDepth's wider envelope. + * Sets and leaves D3D11 pipeline state dirty on exit — wrap in your + * own save/restore (DLSSperf uses its FullscreenPassScope). + */ + void RunUnderwaterMaskRepair(); + /** * @brief Applies RCAS sharpening to the main render target after DLSS upscaling. * diff --git a/src/Features/Upscaling/DLSSperf.cpp b/src/Features/Upscaling/DLSSperf.cpp new file mode 100644 index 0000000000..3983da4175 --- /dev/null +++ b/src/Features/Upscaling/DLSSperf.cpp @@ -0,0 +1,1269 @@ +#include "DLSSperf.h" + +#include +#include + +#include "../../State.h" +#include "../Upscaling.h" + +// Quality mode → render-scale resolution is supplied by the FFX SDK helper +// (same one Upscaling.cpp uses at ConfigureUpscaling), avoiding a duplicate +// scale table here. Decoupled from the original PR's DlssEnhancer::Bridge so +// DLSSperf can ship without the larger enhancer framework. +#include + +DLSSperf::FullscreenPassScope::FullscreenPassScope(ID3D11DeviceContext* a_context) : + ctx(a_context) +{ + ctx->OMGetRenderTargets(1, &savedRTV, &savedDSV); + ctx->RSGetViewports(&numVP, savedVP); + ctx->OMGetBlendState(&savedBlend, savedBlendFactor, &savedSampleMask); + ctx->OMGetDepthStencilState(&savedDSState, &savedStencilRef); + ctx->VSGetShader(&savedVS, nullptr, nullptr); + ctx->PSGetShader(&savedPS, nullptr, nullptr); + ctx->GSGetShader(&savedGS, nullptr, nullptr); + ctx->HSGetShader(&savedHS, nullptr, nullptr); + ctx->DSGetShader(&savedDS, nullptr, nullptr); + ctx->RSGetState(&savedRS); + ctx->PSGetSamplers(0, 1, &savedSampler0); + ctx->PSGetShaderResources(0, 1, &savedSRV0); + ctx->IAGetInputLayout(&savedIL); + ctx->IAGetVertexBuffers(0, D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT, savedVB, savedVBStride, savedVBOffset); + ctx->IAGetIndexBuffer(&savedIB, &savedIBFormat, &savedIBOffset); + ctx->IAGetPrimitiveTopology(&savedTopology); +} + +DLSSperf::FullscreenPassScope::~FullscreenPassScope() +{ + // Null the SRV slot before restoring to break any potential SRV-vs-RTV + // hazard from the pass we just ran (matches the explicit null-pass the + // previous inline code did). + ID3D11ShaderResourceView* nullSRV[] = { nullptr }; + ctx->PSSetShaderResources(0, 1, nullSRV); + ctx->PSSetShaderResources(0, 1, &savedSRV0); + + ctx->OMSetRenderTargets(1, &savedRTV, savedDSV); + if (numVP > 0) + ctx->RSSetViewports(numVP, savedVP); + ctx->OMSetBlendState(savedBlend, savedBlendFactor, savedSampleMask); + ctx->OMSetDepthStencilState(savedDSState, savedStencilRef); + ctx->VSSetShader(savedVS, nullptr, 0); + ctx->PSSetShader(savedPS, nullptr, 0); + ctx->GSSetShader(savedGS, nullptr, 0); + ctx->HSSetShader(savedHS, nullptr, 0); + ctx->DSSetShader(savedDS, nullptr, 0); + ctx->RSSetState(savedRS); + ctx->PSSetSamplers(0, 1, &savedSampler0); + ctx->IASetInputLayout(savedIL); + ctx->IASetVertexBuffers(0, D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT, savedVB, savedVBStride, savedVBOffset); + ctx->IASetIndexBuffer(savedIB, savedIBFormat, savedIBOffset); + ctx->IASetPrimitiveTopology(savedTopology); + + if (savedRTV) + savedRTV->Release(); + if (savedDSV) + savedDSV->Release(); + if (savedBlend) + savedBlend->Release(); + if (savedDSState) + savedDSState->Release(); + if (savedVS) + savedVS->Release(); + if (savedPS) + savedPS->Release(); + if (savedGS) + savedGS->Release(); + if (savedHS) + savedHS->Release(); + if (savedDS) + savedDS->Release(); + if (savedRS) + savedRS->Release(); + if (savedSampler0) + savedSampler0->Release(); + if (savedSRV0) + savedSRV0->Release(); + if (savedIL) + savedIL->Release(); + for (auto*& vb : savedVB) { + if (vb) + vb->Release(); + } + if (savedIB) + savedIB->Release(); +} + +void DLSSperf::InstallRenderTargetSizeHook() +{ + if (!globals::game::isVR) + return; + + if (hookActive) + return; + + // Eager capture — get real HMD resolution BEFORE installing hook + auto* openvr = RE::BSOpenVR::GetSingleton(); + if (!openvr || !openvr->vrSystem) { + logger::error("[DLSSperf] BSOpenVR or vrSystem not available — hook NOT installed"); + return; + } + + uint32_t w = 0, h = 0; + openvr->vrSystem->GetRecommendedRenderTargetSize(&w, &h); + if (w == 0 || h == 0) { + logger::error("[DLSSperf] GetRecommendedRenderTargetSize returned {}x{} — hook NOT installed", w, h); + return; + } + + displayEyeWidth = w; + displayEyeHeight = h; + + // BSShaderRenderTargets::Create runs after SKSE feature settings load, so + // upscaling.settings.qualityMode here reflects the user-saved value. We + // snapshot the corresponding scale at install time and never re-read it — + // the engine's RT allocations happen once, so a later UI quality change + // can't shrink/grow RTs anyway. (Requires a game restart, same as DLSS.) + // + // Validate before division: a bad/corrupt JSON could put qualityMode + // outside FFX's range, returning 0/inf/NaN; that would propagate to bogus + // renderEye dimensions and silently mis-size every engine RT. Fail closed + // — leave hookActive=false so the rest of DLSSperf is dormant and DLSS + // runs on dev's standard path. + const uint32_t qualityModeRaw = globals::features::upscaling.settings.qualityMode; + const uint32_t qualityMode = std::clamp(qualityModeRaw, 0, 4); // FfxFsr3QualityMode range + const float scale = ffxFsr3GetUpscaleRatioFromQualityMode(static_cast(qualityMode)); + if (!std::isfinite(scale) || scale <= 0.0f) { + logger::error("[DLSSperf] FFX returned invalid upscale ratio {} for qualityMode {} (raw {}); hook NOT installed", scale, qualityMode, qualityModeRaw); + return; + } + renderEyeWidth = std::max(1, (uint32_t)(w / scale)); + renderEyeHeight = std::max(1, (uint32_t)(h / scale)); + + // Boot snapshot — runtime upscaler paths read these; UI keeps editing + // live `settings` for JSON persistence. + bootUpscaleMethod = globals::features::upscaling.settings.upscaleMethod; + bootQualityMode = qualityMode; + + stl::write_vfunc<0x12, GetRenderTargetSize_Hook>(RE::VTABLE_BSOpenVR[0]); + + // Per-frame detours that used to live in Hooks.cpp. Both addresses are + // already detoured by core/other features; stl::detour_thunk chains + // (each new install wraps the prior thunk via its static func ptr). + if (!setDirtyStatesHookInstalled) { + stl::detour_thunk(REL::RelocationID(75580, 77386)); + setDirtyStatesHookInstalled = true; + } + if (!updateViewPortHookInstalled) { + stl::detour_thunk(REL::RelocationID(75455, 77240)); + updateViewPortHookInstalled = true; + } + + hookActive = true; +} + +void DLSSperf::GetRenderTargetSize_Hook::thunk(RE::BSOpenVR* a_this, uint32_t* a_width, uint32_t* a_height) +{ + // Call original to get real HMD resolution + func(a_this, a_width, a_height); + + auto& dlssPerf = globals::features::upscaling.dlssPerf; + + *a_width = dlssPerf.renderEyeWidth; + *a_height = dlssPerf.renderEyeHeight; +} + +void DLSSperf::SetupResources() +{ + if (!globals::game::isVR) + return; + + auto renderer = globals::game::renderer; + auto& mainRT = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + if (!mainRT.texture) { + logger::error("[DLSSperf] kMAIN texture not available in SetupResources"); + return; + } + + D3D11_TEXTURE2D_DESC mainDesc{}; + static_cast(mainRT.texture)->GetDesc(&mainDesc); + + D3D11_TEXTURE2D_DESC desc{}; + if (hookActive) { + desc.Width = displayEyeWidth * 2; + desc.Height = displayEyeHeight; + } else { + desc.Width = mainDesc.Width; + desc.Height = mainDesc.Height; + } + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = mainDesc.Format; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET | D3D11_BIND_UNORDERED_ACCESS; + + auto device = globals::d3d::device; + HRESULT hr = device->CreateTexture2D(&desc, nullptr, testTexture.put()); + if (FAILED(hr)) { + logger::error("[DLSSperf] Failed to create test texture: {:#x}", (uint32_t)hr); + return; + } + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{}; + srvDesc.Format = desc.Format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + srvDesc.Texture2D.MostDetailedMip = 0; + hr = device->CreateShaderResourceView(testTexture.get(), &srvDesc, testTextureSRV.put()); + if (FAILED(hr)) { + logger::error("[DLSSperf] Failed to create test texture SRV: {:#x}", (uint32_t)hr); + testTexture = nullptr; + testTextureUAV = nullptr; + return; + } + + // UAV for testTexture + { + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc{}; + uavDesc.Format = desc.Format; + uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; + uavDesc.Texture2D.MipSlice = 0; + hr = device->CreateUnorderedAccessView(testTexture.get(), &uavDesc, testTextureUAV.put()); + if (FAILED(hr)) { + logger::error("[DLSSperf] Failed to create testTexture UAV: {:#x}", (uint32_t)hr); + } + } + + // RTV for testTexture (ISRefraction output) + if (hookActive) { + D3D11_RENDER_TARGET_VIEW_DESC rtvDesc{}; + rtvDesc.Format = desc.Format; + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + rtvDesc.Texture2D.MipSlice = 0; + hr = device->CreateRenderTargetView(testTexture.get(), &rtvDesc, testTextureRTV.put()); + if (FAILED(hr)) { + logger::error("[DLSSperf] Failed to create testTexture RTV: {:#x}", (uint32_t)hr); + } + } + + // refraTempTex: copy of testTexture for ISRefraction input + if (hookActive) { + D3D11_TEXTURE2D_DESC refraDesc = desc; + refraDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + + hr = device->CreateTexture2D(&refraDesc, nullptr, refraTempTex.put()); + if (FAILED(hr)) { + logger::error("[DLSSperf] Failed to create refraTempTex: {:#x}", (uint32_t)hr); + } else { + D3D11_SHADER_RESOURCE_VIEW_DESC refraSrvDesc{}; + refraSrvDesc.Format = refraDesc.Format; + refraSrvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + refraSrvDesc.Texture2D.MipLevels = 1; + refraSrvDesc.Texture2D.MostDetailedMip = 0; + hr = device->CreateShaderResourceView(refraTempTex.get(), &refraSrvDesc, refraTempSRV.put()); + if (FAILED(hr)) { + logger::error("[DLSSperf] Failed to create refraTempSRV: {:#x}", (uint32_t)hr); + refraTempTex = nullptr; + } + } + } + + // Fake DepthStencil at DisplayRes, matching engine kMAIN DS format. + if (hookActive) { + auto& dsData = renderer->GetDepthStencilData(); + auto* mainDSTex = dsData.depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].texture; + if (mainDSTex) { + D3D11_TEXTURE2D_DESC dsDesc{}; + mainDSTex->GetDesc(&dsDesc); + + D3D11_TEXTURE2D_DESC fakeDesc = dsDesc; + fakeDesc.Width = displayEyeWidth * 2; + fakeDesc.Height = displayEyeHeight; + + HRESULT hr2 = device->CreateTexture2D(&fakeDesc, nullptr, fakeDS.put()); + if (FAILED(hr2)) { + logger::error("[DLSSperf] Failed to create fake DS texture: {:#x}", (uint32_t)hr2); + } else { + // Create DSV — format depends on typeless base format + D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc{}; + dsvDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D; + dsvDesc.Texture2D.MipSlice = 0; + + // Map typeless→typed DSV format + if (fakeDesc.Format == DXGI_FORMAT_R32G8X24_TYPELESS) + dsvDesc.Format = DXGI_FORMAT_D32_FLOAT_S8X24_UINT; + else if (fakeDesc.Format == DXGI_FORMAT_R24G8_TYPELESS) + dsvDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + else if (fakeDesc.Format == DXGI_FORMAT_R32_TYPELESS) + dsvDesc.Format = DXGI_FORMAT_D32_FLOAT; + else if (fakeDesc.Format == DXGI_FORMAT_R16_TYPELESS) + dsvDesc.Format = DXGI_FORMAT_D16_UNORM; + else + dsvDesc.Format = fakeDesc.Format; // fallback: hope it's already a DS format + + hr2 = device->CreateDepthStencilView(fakeDS.get(), &dsvDesc, fakeDSV.put()); + if (FAILED(hr2)) { + logger::error("[DLSSperf] Failed to create fake DSV: {:#x}", (uint32_t)hr2); + fakeDS = nullptr; + } + } + } else { + logger::warn("[DLSSperf] kMAIN DS texture not available, skipping fake DS creation"); + } + } + + if (hookActive && fakeDSV) { + auto* ctx = globals::d3d::context; + ctx->ClearDepthStencilView(fakeDSV.get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); + } + + // IS shader hooks (must be installed AFTER FrameAnnotations) + if (hookActive && !tonemapHookInstalled) { + stl::write_vfunc<0x1, TonemapRender_Hook>(RE::VTABLE_BSImagespaceShaderHDRTonemapBlendCinematic[3]); + tonemapHookInstalled = true; + } + + if (hookActive && !refractionHookInstalled && testTextureRTV && refraTempSRV) { + stl::write_vfunc<0x1, RefractionRender_Hook>(RE::VTABLE_BSImagespaceShaderRefraction[3]); + refractionHookInstalled = true; + } + + // Menu-background fix: ISCopy is the entire menu post-chain (verified via + // RenderDoc on a baseline frame — single ISCopy draw, source → kPROJECTED- + // MENU 2048² / kMENUBG). Hook the same vtable slot FrameAnnotations uses + // for its passthrough annotation, then replay with a stretched VP when + // dest > source. No resource dependencies — pure VP/Draw replay. + if (hookActive && !isCopyHookInstalled) { + stl::write_vfunc<0x1, ISCopyRender_Hook>(RE::VTABLE_BSImagespaceShaderCopy[3]); + isCopyHookInstalled = true; + } + + if (hookActive && !uiPassHookInstalled && fakeDSV) { + stl::write_vfunc<0x2A, UIPassDispatch_Hook>(RE::VTABLE_BSShaderAccumulator[0]); + uiPassHookInstalled = true; + } + + // PlayerView end hook: chains after FrameAnnotations' Main_RenderPlayerView. + // Clears postChainDone so Present-前 UI and next frame use normal VP. + if (hookActive && !playerViewHookInstalled) { + stl::detour_thunk(REL::RelocationID(35560, 36559)); + playerViewHookInstalled = true; + } + + // Downscale + blit shaders + if (hookActive && !boxDownscalePS) { + boxDownscalePS.attach(static_cast( + Util::CompileShader(L"Data/Shaders/Upscaling/DLSSperf/BoxDownscalePS.hlsl", { { "PSHADER", "" } }, "ps_5_0"))); + if (!boxDownscalePS) + logger::error("[DLSSperf] Failed to compile BoxDownscalePS"); + } + if (hookActive && !boxDownscaleVS) { + boxDownscaleVS.attach(static_cast( + Util::CompileShader(L"Data/Shaders/Upscaling/UpscaleVS.hlsl", { { "VSHADER", "" } }, "vs_5_0"))); + if (!boxDownscaleVS) + logger::error("[DLSSperf] Failed to compile BoxDownscale VS"); + } + if (hookActive && !menuBlitPS) { + menuBlitPS.attach(static_cast( + Util::CompileShader(L"Data/Shaders/Upscaling/DLSSperf/MenuBGBlitPS.hlsl", { { "PSHADER", "" } }, "ps_5_0"))); + if (!menuBlitPS) + logger::error("[DLSSperf] Failed to compile MenuBGBlitPS"); + } + if (hookActive && !linearSampler) { + D3D11_SAMPLER_DESC sd{}; + sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sd.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.MaxAnisotropy = 1; + sd.MaxLOD = D3D11_FLOAT32_MAX; + if (FAILED(device->CreateSamplerState(&sd, linearSampler.put()))) + logger::error("[DLSSperf] Failed to create linear sampler"); + } + + // Fail-closed pipeline-ready gate + // hookActive only means "BSOpenVR size hook is live + the engine has been + // sized at RenderRes." If any of the downstream resources we depend on at + // Post time failed to create, we'd previously still claim ShouldHandlePost + // and walk into a null deref inside HandlePostProcessing/Tonemap/Refraction. + // Compute the readiness flag once, here — every consumer keys off it. + // + // The minimum viable set for Post wrapping: + // testTexture + testTextureSRV — read by tonemap inner-swap (always) + // fakeDS + fakeDSV — bound as the 3k DS during Post + // boxDownscaleVS/PS + linearSampler — DownscaleToKMain needs these + // Refraction (refraTempTex/SRV + testTextureRTV) is optional: its hook + // gates on those resources at install time, so absence just means the + // refraction draw runs on the engine's 1k path — degraded but stable. + postPipelineReady = + hookActive && + testTexture && testTextureSRV && + fakeDS && fakeDSV && + boxDownscaleVS && boxDownscalePS && linearSampler; + + if (hookActive && !postPipelineReady) { + logger::error( + "[DLSSperf] Post pipeline failed to initialize fully — Post wrap " + "disabled, engine RTs remain at RenderRes. Check upstream resource " + "creation errors above."); + } +} + +// ============================================================================ +// TonemapRender_Hook: IS shader hook for ISHDRTonemapBlendCinematic +// ============================================================================ +// Installed via stl::write_vfunc<0x1> on vtable[3], chains after FrameAnnotations. +// Inner layer of two-layer swap: swaps kMAIN SRV → testTextureSRV and +// kMAIN DS → fakeDS before tonemap Render(), restores after. + +void DLSSperf::TonemapRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) +{ + auto& dlssPerf = globals::features::upscaling.dlssPerf; + + if (!dlssPerf.hookActive || !dlssPerf.testTextureSRV || !dlssPerf.fakeDSV) { + func(imageSpaceShader, shape, param); + return; + } + + // Menu/loading-screen path: the engine's bridge into kTOTAL assumes + // RT.size == kMAIN.size (true for DLAA, broken under DLSS presets where + // kMAIN is renderRes), so the BG ends up missing and OpenVR reprojects + // stale content as movement smears. Skip the gameplay SRV/DS hijack and + // let tonemap run untouched, then call MaybeBlitMenuBG to drive a + // one-shot Upscaling::Upscale() + MenuBGBlitPS blit of the resulting + // DLSS-reconstructed testTexture into kTOTAL. Per-frame guarded so it + // runs at most once per frame regardless of how many menu redraws fire. + if (globals::state && globals::state->IsMainOrLoadingMenuOpen()) { + func(imageSpaceShader, shape, param); + dlssPerf.MaybeBlitMenuBG(RE::RENDER_TARGETS::kTOTAL); + return; + } + + ZoneScoped; + TracyD3D11Zone(globals::state->tracyCtx, "DLSSperf::TonemapRender"); + + auto renderer = RE::BSGraphics::Renderer::GetSingleton(); + auto& rtData = renderer->GetRuntimeData(); + auto& dsData = renderer->GetDepthStencilData(); + + // --- Swap kMAIN SRV → testTextureSRV (so tonemap reads 3k upscaled color) --- + auto& kmainRT = rtData.renderTargets[RE::RENDER_TARGETS::kMAIN]; + dlssPerf.savedKMainSRV = kmainRT.SRV; + kmainRT.SRV = dlssPerf.testTextureSRV.get(); + + // --- Also swap kMAIN_COPY SRV (refraction path reads this instead of kMAIN) --- + auto& kmainCopyRT = rtData.renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; + dlssPerf.savedKMainCopySRV = kmainCopyRT.SRV; + kmainCopyRT.SRV = dlssPerf.testTextureSRV.get(); + + // --- Swap kMAIN DS views → fakeDS (so 3k RT doesn't mismatch 1k DS) --- + auto& kmainDS = dsData.depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + for (int i = 0; i < 8; i++) { + dlssPerf.savedKMainViews[i] = kmainDS.views[i]; + if (kmainDS.views[i]) + kmainDS.views[i] = dlssPerf.fakeDSV.get(); + } + for (int i = 0; i < 8; i++) { + dlssPerf.savedKMainReadOnlyViews[i] = kmainDS.readOnlyViews[i]; + if (kmainDS.readOnlyViews[i]) + kmainDS.readOnlyViews[i] = dlssPerf.fakeDSV.get(); + } + + // --- Call original (or FrameAnnotations chain) --- + func(imageSpaceShader, shape, param); + + // --- Restore kMAIN SRV --- + kmainRT.SRV = dlssPerf.savedKMainSRV; + dlssPerf.savedKMainSRV = nullptr; + + // --- Restore kMAIN_COPY SRV --- + kmainCopyRT.SRV = dlssPerf.savedKMainCopySRV; + dlssPerf.savedKMainCopySRV = nullptr; + + // --- Restore kMAIN DS views --- + for (int i = 0; i < 8; i++) + kmainDS.views[i] = dlssPerf.savedKMainViews[i]; + for (int i = 0; i < 8; i++) + kmainDS.readOnlyViews[i] = dlssPerf.savedKMainReadOnlyViews[i]; +} + +// ============================================================================ +// RefractionRender_Hook: IS shader hook for ISRefraction +// ============================================================================ +// Strategy: let func() run normally (1k refraction, kMAIN→kMAIN_COPY). +// After func() returns, D3D11 state is sticky (PS/CB/sampler/IA all still bound). +// We replay the draw with our own RT (testTexture 3k), VP (3k), and SRV (refraTempTex 3k). + +void DLSSperf::RefractionRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) +{ + auto& dlssPerf = globals::features::upscaling.dlssPerf; + + if (!dlssPerf.hookActive || !dlssPerf.testTextureRTV || !dlssPerf.refraTempSRV) { + func(imageSpaceShader, shape, param); + return; + } + + ZoneScoped; + TracyD3D11Zone(globals::state->tracyCtx, "DLSSperf::RefractionRender"); + + // --- Pass 1: engine's normal 1k refraction (untouched) --- + func(imageSpaceShader, shape, param); + + // --- Pass 2: our 3k refraction replay --- + // func() left PS/CB/sampler/IA/VB/IB all bound on the D3D context. + // We only change RT, VP, and t0 SRV, then DrawIndexed with the same geometry. + + auto* context = globals::d3d::context; + + // Save current RT so we can restore after our draw + ID3D11RenderTargetView* savedRTV = nullptr; + ID3D11DepthStencilView* savedDSV = nullptr; + context->OMGetRenderTargets(1, &savedRTV, &savedDSV); + + // Save the full viewport stack rather than a single VP — RSSetViewports/ + // RSGetViewports work on arrays sized up to D3D11_VIEWPORT_AND_SCISSORRECT_ + // OBJECT_COUNT_PER_PIPELINE (16). Truncating to one would silently drop + // extra bound viewports if a later engine pass relied on multi-VP. + UINT numVP = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; + D3D11_VIEWPORT savedVP[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE] = {}; + context->RSGetViewports(&numVP, savedVP); + + // Save current t0 SRV (kMAIN.SRV used by ISRefraction as scene input) + ID3D11ShaderResourceView* savedSRV0 = nullptr; + context->PSGetShaderResources(0, 1, &savedSRV0); + + // Set 3k output: testTexture RTV, no DS needed for fullscreen IS shader + ID3D11RenderTargetView* rtv3k = dlssPerf.testTextureRTV.get(); + context->OMSetRenderTargets(1, &rtv3k, nullptr); + + // Set 3k VP + D3D11_VIEWPORT vp3k = {}; + vp3k.TopLeftX = 0.0f; + vp3k.TopLeftY = 0.0f; + vp3k.Width = static_cast(dlssPerf.displayEyeWidth * 2); + vp3k.Height = static_cast(dlssPerf.displayEyeHeight); + vp3k.MinDepth = 0.0f; + vp3k.MaxDepth = 1.0f; + context->RSSetViewports(1, &vp3k); + + // Set 3k input: refraTempTex as t0 (scene color for refraction sampling) + ID3D11ShaderResourceView* srv3k = dlssPerf.refraTempSRV.get(); + context->PSSetShaderResources(0, 1, &srv3k); + + // Draw with the same geometry (BSTriShape fullscreen quad, IA still bound) + context->DrawIndexed(6, 0, 0); + + // --- Restore D3D state so engine continues normally --- + context->OMSetRenderTargets(1, &savedRTV, savedDSV); + // RSGetViewports may have returned 0 if the prior pass left no viewport + // bound; skip the restore in that case rather than pushing a zero-init VP. + if (numVP > 0) { + context->RSSetViewports(numVP, savedVP); + } + context->PSSetShaderResources(0, 1, &savedSRV0); + + // Release COM refs from Get calls + if (savedRTV) + savedRTV->Release(); + if (savedDSV) + savedDSV->Release(); + if (savedSRV0) + savedSRV0->Release(); +} + +// ============================================================================ +// ISCopyRender_Hook: stretch ISCopy when source < dest (menu compositor fix) +// ============================================================================ +// The VR menu compositor uses a single ISCopy draw to blit the rendered scene +// (kMAIN — RenderRes under DLSSperf) into a fixed-size projection surface +// (kPROJECTEDMENU 2048², or kMENUBG which DLSSperf enlarges to DisplayRes). +// Engine ISCopy uses a 1:1 viewport sized to the source, so the small source +// is stamped into the top-left of the larger dest. Symptom: "main menu image +// looks downscaled." +// +// Fix: after func() runs, if dest > current VP we replay the draw with the +// VP expanded to the dest's full dims. ISCopy's PS/CB/IA/sampler are sticky +// on the D3D context after func() returns (same pattern RefractionRender_Hook +// relies on), so the replay needs only a viewport change + DrawIndexed and +// the engine's clamp-sampler stretches the source naturally. +// +// In-game ISCopy (where source.w == dest.w under DLSSperf — kMAIN renderRes +// → kMAIN_COPY renderRes) takes the early-out branch and the engine's draw +// is the final pixel. + +void DLSSperf::ISCopyRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) +{ + auto& dlssPerf = globals::features::upscaling.dlssPerf; + + // Inactive / non-VR: passthrough. + if (!dlssPerf.hookActive) { + func(imageSpaceShader, shape, param); + return; + } + + // Let the engine draw first. After func() returns the IS shader's PS, CB, + // sampler, IA layout, vertex/index buffers, and topology are all still + // bound on the context (sticky D3D11 state). We only need to override the + // viewport for the replay draw. + func(imageSpaceShader, shape, param); + + auto* context = globals::d3d::context; + + // Inspect the current RTV's dest dimensions. + ID3D11RenderTargetView* curRTV = nullptr; + ID3D11DepthStencilView* curDSV = nullptr; + context->OMGetRenderTargets(1, &curRTV, &curDSV); + if (!curRTV) { + if (curDSV) + curDSV->Release(); + return; + } + + ID3D11Resource* rtRes = nullptr; + curRTV->GetResource(&rtRes); + if (!rtRes) { + curRTV->Release(); + if (curDSV) + curDSV->Release(); + return; + } + + D3D11_TEXTURE2D_DESC rtDesc{}; + static_cast(rtRes)->GetDesc(&rtDesc); + rtRes->Release(); + + // Current VP (the one func() used) — full array so we can restore exactly. + UINT numVP = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; + D3D11_VIEWPORT savedVP[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE] = {}; + context->RSGetViewports(&numVP, savedVP); + + // Only intervene when either axis of the engine's VP is smaller than the + // dest. The +1 guard avoids float-equality issues (VPs are floats, RT + // dims are uint32). Width-only would miss the case where the engine + // binds a taller-than-VP RT (e.g., a square 2048² panel against a + // renderRes-height VP). + bool needsStretch = numVP > 0 && + (rtDesc.Width > static_cast(savedVP[0].Width + 1.0f) || + rtDesc.Height > static_cast(savedVP[0].Height + 1.0f)); + + if (needsStretch) { + ZoneScoped; + TracyD3D11Zone(globals::state->tracyCtx, "DLSSperf::ISCopyStretch"); + + // Replay viewport: full dest extent, preserve depth range from the + // original so anything sampling depth (unlikely for ISCopy but safe) + // keeps the same Z behavior. + D3D11_VIEWPORT stretchVP = savedVP[0]; + stretchVP.TopLeftX = 0.0f; + stretchVP.TopLeftY = 0.0f; + stretchVP.Width = static_cast(rtDesc.Width); + stretchVP.Height = static_cast(rtDesc.Height); + context->RSSetViewports(1, &stretchVP); + + // ISCopy is a fullscreen quad drawn as a triangle list (6 indices). + // Same index count RefractionRender_Hook uses for the same reason — + // both replay the IS shader's standard fullscreen geometry. + context->DrawIndexed(6, 0, 0); + + // Restore engine's VP so any state inspector downstream sees what it + // expects. numVP guaranteed > 0 inside this branch. + context->RSSetViewports(numVP, savedVP); + } + + curRTV->Release(); + if (curDSV) + curDSV->Release(); +} + +// ============================================================================ +// UIPassDispatch_Hook: swap KMAIN DS → fakeDS for UI pass (renderMode==24) +// ============================================================================ +// UI pass draws VR HUD to kMENUBG (now 3k). Engine binds KMAIN(DS) as DS, +// which is still 1k → size mismatch. Swap to fakeDS (3k) before, restore after. + +void DLSSperf::UIPassDispatch_Hook::thunk(RE::BSGraphics::BSShaderAccumulator* shaderAccumulator, uint32_t renderFlags) +{ + auto& dlssPerf = globals::features::upscaling.dlssPerf; + + // Only intercept renderMode==24 (UI pass) when hook is active + auto& rtData = shaderAccumulator->GetRuntimeData(); + if (!dlssPerf.hookActive || !dlssPerf.fakeDSV || rtData.renderMode != 24) { + func(shaderAccumulator, renderFlags); + return; + } + + auto renderer = RE::BSGraphics::Renderer::GetSingleton(); + auto& dsData = renderer->GetDepthStencilData(); + auto& kmainDS = dsData.depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + + // Save original KMAIN DS views and swap to fakeDS + ID3D11DepthStencilView* savedViews[8] = {}; + ID3D11DepthStencilView* savedReadOnlyViews[8] = {}; + for (int i = 0; i < 8; i++) { + savedViews[i] = kmainDS.views[i]; + if (kmainDS.views[i]) + kmainDS.views[i] = dlssPerf.fakeDSV.get(); + } + for (int i = 0; i < 8; i++) { + savedReadOnlyViews[i] = kmainDS.readOnlyViews[i]; + if (kmainDS.readOnlyViews[i]) + kmainDS.readOnlyViews[i] = dlssPerf.fakeDSV.get(); + } + + // Force engine to re-bind DS from struct + globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); + + // Force 3k VP: engine may not call UpdateViewPort during UI pass, + // so we directly set shadowState viewport to DisplayRes and mark dirty. + auto* ss = globals::game::shadowState; + D3D11_VIEWPORT savedVP = {}; + if (ss) { + auto& vp = ss->GetVRRuntimeData().viewPort; + savedVP = vp; + vp.TopLeftX = 0.0f; + vp.TopLeftY = 0.0f; + vp.Width = static_cast(dlssPerf.displayEyeWidth * 2); + vp.Height = static_cast(dlssPerf.displayEyeHeight); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_VIEWPORT); + } + + // Skip VP compression in UpdateViewPort hook during UI pass + dlssPerf.postInterceptActive = true; + + func(shaderAccumulator, renderFlags); + + dlssPerf.postInterceptActive = false; + + // Restore viewport + if (ss) { + ss->GetVRRuntimeData().viewPort = savedVP; + globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_VIEWPORT); + } + + // Restore original KMAIN DS views + for (int i = 0; i < 8; i++) + kmainDS.views[i] = savedViews[i]; + for (int i = 0; i < 8; i++) + kmainDS.readOnlyViews[i] = savedReadOnlyViews[i]; + + // Re-dirty so subsequent passes get correct DS + globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); +} + +// ============================================================================ +// PlayerViewRender_Hook: clear postChainDone at PlayerView end +// ============================================================================ +// PlayerView covers the entire VR pipeline (World→Post→UI→Submit). +// After func() returns, clear postChainDone so the Present-前 UI chain +// and the next frame use normal VP compression. + +void DLSSperf::PlayerViewRender_Hook::thunk(void* a1, bool a2, bool a3) +{ + func(a1, a2, a3); + + globals::features::upscaling.dlssPerf.ClearPostChainDone(); +} + +// ============================================================================ +// BSGraphics_SetDirtyStates_Hook +// ============================================================================ +// Wraps DS swap around the engine's RT/DS flush so enlarged-RT draws don't +// rasterizer-clip to the smaller kMAIN DS bounds. +void DLSSperf::BSGraphics_SetDirtyStates_Hook::thunk(bool isCompute) +{ + bool swapped = false; + if (!isCompute) + swapped = globals::features::upscaling.dlssPerf.MaybeSwapDSForEnlargedRT(); + func(isCompute); + if (swapped) + globals::features::upscaling.dlssPerf.RestoreSwappedDS(); +} + +// ============================================================================ +// BSGraphics_Renderer_UpdateViewPort_Hook +// ============================================================================ +// Post-corrects the engine viewport when the bound RT and the requested VP +// don't agree about render-vs-display extent. Was originally in Hooks.cpp. +void DLSSperf::BSGraphics_Renderer_UpdateViewPort_Hook::thunk(RE::BSGraphics::Renderer* a_this, uint32_t a_width, uint32_t a_height, bool a_forceMatchRT) +{ + func(a_this, a_width, a_height, a_forceMatchRT); + + auto& dlssPerf = globals::features::upscaling.dlssPerf; + if (!dlssPerf.IsHookActive()) + return; + + // During Post intercept enlarged kTEMP/kTOTAL already get the right VP + // from func() because of their inflated RT dims — don't second-guess it. + if (dlssPerf.IsPostInterceptActive()) + return; + + auto* ss = globals::game::shadowState; + if (!ss) + return; + auto& vp = ss->GetVRRuntimeData().viewPort; + const uint32_t displayW = dlssPerf.GetDisplayEyeWidth() * 2; + const uint32_t displayH = dlssPerf.GetDisplayEyeHeight(); + const uint32_t renderW = dlssPerf.GetRenderEyeWidth() * 2; + const uint32_t renderH = dlssPerf.GetRenderEyeHeight(); + + // After the Post chain, UI / submit-prep draws target enlarged kTOTAL + // at displayRes — expand any renderRes VP the engine sets back up. + // The fade Draw(30) bypasses this path entirely (direct D3D RSSet- + // Viewports) and is handled by the Draw vfunc hook in Globals.cpp. + if (dlssPerf.IsPostChainDone()) { + if (static_cast(vp.Width) == renderW && + static_cast(vp.Height) == renderH) { + vp.Width = static_cast(displayW); + vp.Height = static_cast(displayH); + } + return; + } + + // Honor forceMatchRT for displayRes-enlarged RTs — shrinking VP there + // leaves menu content in a renderRes corner of kTOTAL. + if (a_forceMatchRT) + return; + + // Same risk on the non-forceMatchRT path: the menu compositor calls + // UpdateViewPort(displayW, displayH, false) directly with screen-space + // dims, and compressing those would clip the BG. + { + const uint32_t rtIdx = static_cast(ss->GetVRRuntimeData().renderTargets[0]); + if (rtIdx == RE::RENDER_TARGETS::kTOTAL || + rtIdx == RE::RENDER_TARGETS::kMENUBG || + rtIdx == RE::RENDER_TARGETS::kIMAGESPACE_TEMP_COPY) + return; + } + + // Normal world/depth path: compress displayRes → renderRes so draws + // stay inside the renderRes-sized kMAIN family. + if (static_cast(vp.Width) == displayW && + static_cast(vp.Height) == displayH) { + vp.Width = static_cast(renderW); + vp.Height = static_cast(renderH); + } +} + +// ============================================================================ +// BeginPostIntercept / EndPostIntercept +// ============================================================================ +// Outer layer of two-layer swap: swaps kMAIN_COPY DS → fakeDS before the +// entire Post chain (covers the copy step #10 which binds kMAIN_COPY DS). +// Inner layer (tonemap hook) handles kMAIN DS + kMAIN SRV for step #9. + +void DLSSperf::BeginPostIntercept() +{ + if (!hookActive || !fakeDSV) + return; + + ZoneScoped; + auto state = globals::state; + state->BeginPerfEvent("DLSSperf::BeginPostIntercept"); + + auto renderer = RE::BSGraphics::Renderer::GetSingleton(); + auto& dsData = renderer->GetDepthStencilData(); + auto& kmainCopyDS = dsData.depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN_COPY]; + + postInterceptActive = true; + + // Swap kMAIN_COPY DS views → fakeDS + for (int i = 0; i < 8; i++) { + savedKMainCopyViews[i] = kmainCopyDS.views[i]; + if (kmainCopyDS.views[i]) + kmainCopyDS.views[i] = fakeDSV.get(); + } + for (int i = 0; i < 8; i++) { + savedKMainCopyReadOnlyViews[i] = kmainCopyDS.readOnlyViews[i]; + if (kmainCopyDS.readOnlyViews[i]) + kmainCopyDS.readOnlyViews[i] = fakeDSV.get(); + } + + state->EndPerfEvent(); +} + +void DLSSperf::EndPostIntercept() +{ + if (!hookActive || !fakeDSV) + return; + + ZoneScoped; + auto state = globals::state; + state->BeginPerfEvent("DLSSperf::EndPostIntercept"); + + auto renderer = RE::BSGraphics::Renderer::GetSingleton(); + auto& dsData = renderer->GetDepthStencilData(); + auto& kmainCopyDS = dsData.depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN_COPY]; + + postInterceptActive = false; + postChainDone = true; + + // Restore kMAIN_COPY DS views + for (int i = 0; i < 8; i++) + kmainCopyDS.views[i] = savedKMainCopyViews[i]; + for (int i = 0; i < 8; i++) + kmainCopyDS.readOnlyViews[i] = savedKMainCopyReadOnlyViews[i]; + + state->EndPerfEvent(); +} + +// ============================================================================ +// DownscaleToKMain: Box 3×3 downscale testTexture (3k) → kMAIN (1k) +// ============================================================================ +// Called before the Post chain so the HDR pyramid builds from AA'd DLSS output +// instead of the raw 1k render, eliminating shimmer in bloom/exposure. +// Only kMAIN needs writing: +// - No refraction: kMAIN is the pyramid input directly. +// - With refraction: engine composites kMAIN → kMAIN_COPY, which enters pyramid. + +void DLSSperf::DownscaleToKMain() +{ + if (!hookActive || !testTextureSRV || !boxDownscalePS || !boxDownscaleVS || !linearSampler) + return; + + ZoneScoped; + auto state = globals::state; + auto renderer = globals::game::renderer; + auto context = globals::d3d::context; + auto& rtData = renderer->GetRuntimeData(); + + auto& kmain = rtData.renderTargets[RE::RENDER_TARGETS::kMAIN]; + + // Bail before opening the perf event so we don't leak a dangling + // Begin without End on the null-RTV early-return path. + if (!kmain.RTV) + return; + + state->BeginPerfEvent("DLSSperf::DownscaleToKMain"); + TracyD3D11Zone(state->tracyCtx, "DLSSperf::DownscaleToKMain"); + + { + FullscreenPassScope stateScope(context); + + // IA: fullscreen triangle (no vertex/index buffers) + context->IASetInputLayout(nullptr); + context->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); + context->IASetIndexBuffer(nullptr, DXGI_FORMAT_UNKNOWN, 0); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + // Shaders — clear GS/HS/DS to prevent pipeline interference + context->VSSetShader(boxDownscaleVS.get(), nullptr, 0); + context->PSSetShader(boxDownscalePS.get(), nullptr, 0); + context->GSSetShader(nullptr, nullptr, 0); + context->HSSetShader(nullptr, nullptr, 0); + context->DSSetShader(nullptr, nullptr, 0); + + ID3D11ShaderResourceView* srvs[] = { testTextureSRV.get() }; + context->PSSetShaderResources(0, 1, srvs); + ID3D11SamplerState* samplers[] = { linearSampler.get() }; + context->PSSetSamplers(0, 1, samplers); + + // Opaque overwrite: no blending, no depth test, default rasterizer + context->OMSetBlendState(nullptr, nullptr, 0xffffffff); + context->OMSetDepthStencilState(nullptr, 0); + context->RSSetState(nullptr); + + // Viewport at RenderRes SBS (1k) + D3D11_VIEWPORT vp = {}; + vp.Width = static_cast(renderEyeWidth * 2); + vp.Height = static_cast(renderEyeHeight); + vp.MaxDepth = 1.0f; + context->RSSetViewports(1, &vp); + + ID3D11RenderTargetView* rtv = kmain.RTV; + context->OMSetRenderTargets(1, &rtv, nullptr); + context->Draw(3, 0); + } + + state->EndPerfEvent(); +} + +// Bridge the DLSS-reconstructed menu BG into kTOTAL/kMENUBG. Driven from +// TonemapRender_Hook post-func in menu/loading state: the engine's tonemap +// shader's UV math assumes RT.size == kMAIN.size (true for DLAA, broken +// under DLSS), so we run our own DLSS evaluate against the engine's +// menu-state inputs (jitter via Main_UpdateJitter, depth via menu BG pre- +// pass, motion vectors as ISTemporalAA reads them) and blit testTexture → +// dest. One-shot per frame via blittedFrameId (Present doesn't fire here +// and PlayerView doesn't fire in main-menu, so the frame-id guard is the +// only reliable per-frame boundary). +void DLSSperf::MaybeBlitMenuBG(uint32_t boundRTIdx) +{ + const uint32_t currentFrame = globals::state ? globals::state->frameCount : 0; + if (!hookActive || blittedFrameId == currentFrame || !menuBlitPS || !boxDownscaleVS || !linearSampler) + return; + if (!testTexture || !testTextureSRV) + return; + if (!globals::state || !globals::state->IsMainOrLoadingMenuOpen()) + return; + if (boundRTIdx != RE::RENDER_TARGETS::kTOTAL && + boundRTIdx != RE::RENDER_TARGETS::kMENUBG) + return; + + auto renderer = globals::game::renderer; + auto& rtData = renderer->GetRuntimeData(); + auto& dest = rtData.renderTargets[boundRTIdx]; + if (!dest.RTV || !dest.texture) + return; + + ZoneScoped; + auto state = globals::state; + auto* context = globals::d3d::context; + state->BeginPerfEvent("DLSSperf::MenuBGBlit"); + TracyD3D11Zone(state->tracyCtx, "DLSSperf::MenuBGBlit"); + + globals::features::upscaling.Upscale(); + + { + FullscreenPassScope stateScope(context); + + // IA: fullscreen triangle, no VB/IB + context->IASetInputLayout(nullptr); + context->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); + context->IASetIndexBuffer(nullptr, DXGI_FORMAT_UNKNOWN, 0); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + context->VSSetShader(boxDownscaleVS.get(), nullptr, 0); + context->PSSetShader(menuBlitPS.get(), nullptr, 0); + context->GSSetShader(nullptr, nullptr, 0); + context->HSSetShader(nullptr, nullptr, 0); + context->DSSetShader(nullptr, nullptr, 0); + + ID3D11ShaderResourceView* srvs[] = { testTextureSRV.get() }; + context->PSSetShaderResources(0, 1, srvs); + ID3D11SamplerState* samplers[] = { linearSampler.get() }; + context->PSSetSamplers(0, 1, samplers); + + context->OMSetBlendState(nullptr, nullptr, 0xffffffff); + context->OMSetDepthStencilState(nullptr, 0); + context->RSSetState(nullptr); + + D3D11_TEXTURE2D_DESC destDesc{}; + static_cast(dest.texture)->GetDesc(&destDesc); + D3D11_VIEWPORT vp = {}; + vp.Width = static_cast(destDesc.Width); + vp.Height = static_cast(destDesc.Height); + vp.MaxDepth = 1.0f; + context->RSSetViewports(1, &vp); + + ID3D11RenderTargetView* rtv = dest.RTV; + context->OMSetRenderTargets(1, &rtv, nullptr); + context->Draw(3, 0); + } + + blittedFrameId = currentFrame; + state->EndPerfEvent(); +} + +void DLSSperf::HandlePostProcessing(const std::function& enginePost) +{ + ZoneScoped; + auto state = globals::state; + state->BeginPerfEvent("DLSSperf::HandlePostProcessing"); + + // Copy testTexture → refraTempTex before Post, so ISRefraction can read 3k scene + if (refraTempTex) { + globals::d3d::context->CopyResource(refraTempTex.get(), testTexture.get()); + } + + // Downscale testTexture (3k AA'd) → kMAIN (1k) so the HDR pyramid and + // bloom compute from anti-aliased content instead of raw 1k render. + DownscaleToKMain(); + + // Underwater mask analytical repair. Engine RTs (depth, mask) are at + // renderRes under DLSSperf, so the full-resolution path of UpscaleDepth + // would apply — but routing through UpscaleDepth here leaves pipeline + // state dirty and the trailing enginePost() loses kMAIN. Drive the + // mask-only draw directly inside our own FullscreenPassScope so the + // inbound engine state is restored on exit. + { + FullscreenPassScope scope(globals::d3d::context); + globals::features::upscaling.RunUnderwaterMaskRepair(); + } + + // Outer layer: swap kMAIN_COPY DS + SRV for refraction path coverage + BeginPostIntercept(); + + // Run full engine Post chain; IS shader hook handles tonemap step (#9) swap/restore + enginePost(); + + // Restore kMAIN_COPY DS + EndPostIntercept(); + + state->EndPerfEvent(); +} + +bool DLSSperf::MaybeSwapDSForEnlargedRT() +{ + if (!hookActive || postInterceptActive) + return false; + if (autoSwapDSIdx != UINT32_MAX) + return false; // re-entry guard + + auto* ss = globals::game::shadowState; + if (!ss) + return false; + auto& srd = ss->GetVRRuntimeData(); + + // Only the three RTs DLSSperf_MaybeEnlargeRT inflates to displayRes. + const uint32_t rtIdx = static_cast(srd.renderTargets[0]); + if (rtIdx != RE::RENDER_TARGETS::kTOTAL && + rtIdx != RE::RENDER_TARGETS::kMENUBG && + rtIdx != RE::RENDER_TARGETS::kIMAGESPACE_TEMP_COPY) + return false; + + const uint32_t dsIdx = static_cast(srd.depthStencil); + if (dsIdx != RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN && + dsIdx != RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN_COPY) + return false; + + auto renderer = globals::game::renderer; + auto& dsData = renderer->GetDepthStencilData(); + auto& bound = dsData.depthStencils[dsIdx]; + + // Unbind DS entirely rather than rebind to fakeDSV. fakeDSV is cleared + // once at init with stencil=0; ISHDRTonemapBlendCinematic (the menu's + // kMAIN→kTOTAL bridge at event 931 in the baseline capture) reads + // stencil to mask sky vs. world, so a wrong stencil value discards every + // pixel and the BG never reaches kTOTAL. Fullscreen IS shaders and the + // menu UI quad don't actually depth-test, so nullptr DS is safe and + // sidesteps the stencil-content mismatch. Swap pattern matches UIPass- + // Dispatch_Hook (all 8 view slots). + for (int i = 0; i < 8; ++i) { + autoSwapSavedViews[i] = bound.views[i]; + if (bound.views[i]) + bound.views[i] = nullptr; + } + for (int i = 0; i < 8; ++i) { + autoSwapSavedReadOnlyViews[i] = bound.readOnlyViews[i]; + if (bound.readOnlyViews[i]) + bound.readOnlyViews[i] = nullptr; + } + autoSwapDSIdx = dsIdx; + return true; +} + +void DLSSperf::RestoreSwappedDS() +{ + if (autoSwapDSIdx == UINT32_MAX) + return; + auto renderer = globals::game::renderer; + auto& dsData = renderer->GetDepthStencilData(); + auto& bound = dsData.depthStencils[autoSwapDSIdx]; + for (int i = 0; i < 8; ++i) + bound.views[i] = autoSwapSavedViews[i]; + for (int i = 0; i < 8; ++i) + bound.readOnlyViews[i] = autoSwapSavedReadOnlyViews[i]; + autoSwapDSIdx = UINT32_MAX; +} + +// ============================================================================ +// CreateRenderTarget enlarge — install + per-site thunks +// ============================================================================ +// Three specific call sites inside BSShaderRenderTargets::Create produce the +// displayRes-enlarged RTs (kMENUBG, kIMAGESPACE_TEMP_COPY, kTOTAL). Offsets +// identified in Ghidra (see CreateRT_k* labels inside the renamed +// BSShaderRenderTargets__Create function in SkyrimVR.exe). VR-only. + +// ============================================================================ +// ID3D11DeviceContext_Draw_Hook (vtable index 13) +// ============================================================================ +// Engine fade-overlay Draw(30) fires after the Post chain and before Submit. +// Under DLSSperf the draw's VP is computed at renderRes while the RT (kTOTAL) +// is displayRes — partial-screen "black stamp" without this swap. Gate on +// VertexCount==30 + isVR keeps the cost a single comparison on flat / non- +// fade draws. +void DLSSperf::ID3D11DeviceContext_Draw_Hook::thunk(ID3D11DeviceContext* This, UINT VertexCount, UINT StartVertexLocation) +{ + if (VertexCount == 30 && globals::game::isVR) { + auto& dlssPerf = globals::features::upscaling.dlssPerf; + if (dlssPerf.IsHookActive() && dlssPerf.IsPostChainDone()) { + UINT numVP = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; + D3D11_VIEWPORT savedVP[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE] = {}; + This->RSGetViewports(&numVP, savedVP); + + D3D11_VIEWPORT vp{}; + vp.Width = static_cast(dlssPerf.GetDisplayEyeWidth() * 2); + vp.Height = static_cast(dlssPerf.GetDisplayEyeHeight()); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + This->RSSetViewports(1, &vp); + + func(This, VertexCount, StartVertexLocation); + + if (numVP > 0) + This->RSSetViewports(numVP, savedVP); + return; + } + } + func(This, VertexCount, StartVertexLocation); +} + +void DLSSperf::InstallFadeOverlayHook(ID3D11DeviceContext* context) +{ + if (!globals::game::isVR || !context) + return; + stl::detour_vfunc<13, ID3D11DeviceContext_Draw_Hook>(context); +} + +void DLSSperf::InstallCreateRTThunks() +{ + if (!REL::Module::IsVR()) + return; + auto vrBase = REL::RelocationID(100458, 107175).address(); + stl::write_thunk_call(vrBase + 0x6cc); + stl::write_thunk_call(vrBase + 0x7a3); + stl::write_thunk_call(vrBase + 0x1547); +} + +void DLSSperf::BeginCreateRTEnlarge() +{ + if (!hookActive) + return; + enlargeWidth = displayEyeWidth * 2; + enlargeHeight = displayEyeHeight; + enlargeActive = true; +} + +void DLSSperf::EndCreateRTEnlarge() +{ + enlargeActive = false; +} + +namespace +{ + void EnlargeProps(RE::BSGraphics::RenderTargetProperties* a_props) + { + auto& dp = globals::features::upscaling.dlssPerf; + if (!dp.IsCreateRTEnlargeActive()) + return; + a_props->width = dp.GetEnlargeWidth(); + a_props->height = dp.GetEnlargeHeight(); + } +} + +void DLSSperf::CreateRT_MenuBG_Hook::thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) +{ + EnlargeProps(a_properties); + func(a_this, a_target, a_properties); +} + +void DLSSperf::CreateRT_ImagespaceTempCopy_Hook::thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) +{ + EnlargeProps(a_properties); + func(a_this, a_target, a_properties); +} + +void DLSSperf::CreateRT_Total_Hook::thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) +{ + EnlargeProps(a_properties); + func(a_this, a_target, a_properties); +} + +void DLSSperf::DrawSettings() +{ + // DLSSperf has no user-facing settings of its own — enablement is gated + // at install time by whether the BSShaderRenderTargets::Create hook ran + // successfully. A future PR may surface diagnostic info here. +} diff --git a/src/Features/Upscaling/DLSSperf.h b/src/Features/Upscaling/DLSSperf.h new file mode 100644 index 0000000000..22d3f74414 --- /dev/null +++ b/src/Features/Upscaling/DLSSperf.h @@ -0,0 +1,384 @@ +#pragma once + +// ============================================================================ +// DLSSperf — render-target size hook + post-processing interception +// ============================================================================ +// +// Opt-in VR upscaling feature. Hooks BSOpenVR::GetRenderTargetSize so all +// engine render targets are allocated at a small RenderRes while DLSS writes +// its output to a private DisplayRes testTexture. Ships standalone — the +// "DlssEnhancer" prerequisite from earlier drafts no longer applies. +// +// Benefits: +// - VRAM and bandwidth savings proportional to the quality-mode scale ratio. +// - UpscaleRT is no longer needed. +// - Game menus are no longer occluded by the upscaler output. +// +// Current limitations: +// - Post-processing still runs on renderRes kMAIN via a 3x3-box downscale +// of testTexture (see BoxDownscalePS.hlsl). Performance is good and +// visual loss is minimal. Once the post chain is rewritten to consume +// testTexture natively the downscale can be removed. +// - Main menu / pause backgrounds render through a path that doesn't pass +// through Main_PostProcessing. We bridge them via ISCopyRender_Hook + +// MaybeBlitMenuBG: ISCopy's destination viewport is stretched to the +// full dest dims so the source covers the panel, and MaybeBlitMenuBG +// drives a one-shot Upscaling::Upscale() + MenuBGBlitPS into the bound +// menu RT so the BG sees a DLSS-reconstructed image instead of a raw +// renderRes stretch. Both paths are one-shot per frame. +// +// ============================================================================ + +#include +#include +#include +#include + +struct DLSSperf +{ + void SetupResources(); + void DrawSettings(); + + // Phase 1: standalone test texture that receives Upscaling output instead of kMAIN. + // Returns nullptr when not ready. + ID3D11Texture2D* GetTestTexture() const { return testTexture.get(); } + ID3D11ShaderResourceView* GetTestTextureSRV() const { return testTextureSRV.get(); } + ID3D11UnorderedAccessView* GetTestTextureUAV() const { return testTextureUAV.get(); } + ID3D11Texture2D* GetRefraTempTex() const { return refraTempTex.get(); } + ID3D11ShaderResourceView* GetRefraTempSRV() const { return refraTempSRV.get(); } + + // Phase 2: resolution hook status + bool IsHookActive() const { return hookActive; } + bool IsPostInterceptActive() const { return postInterceptActive; } + bool IsPostChainDone() const { return postChainDone; } + void ClearPostChainDone() { postChainDone = false; } + uint32_t GetDisplayEyeWidth() const { return displayEyeWidth; } + uint32_t GetDisplayEyeHeight() const { return displayEyeHeight; } + uint32_t GetRenderEyeWidth() const { return renderEyeWidth; } + uint32_t GetRenderEyeHeight() const { return renderEyeHeight; } + + // Boot snapshots — engine RTs are sized once against these, so runtime + // upscaler reads must route through here instead of live `Upscaling:: + // settings` (mid-session UI changes would otherwise break the HMD). + bool HasBootSnapshot() const { return hookActive; } + uint32_t GetBootUpscaleMethod() const { return bootUpscaleMethod; } + uint32_t GetBootQualityMode() const { return bootQualityMode; } + + // Phase 3: real HMD display resolution in SBS format (e.g. 3072×1632) + // Used by Upscaling pipeline to override polluted screenSize (which equals RenderRes after hook) + float2 GetDisplayScreenSize() const + { + return { static_cast(displayEyeWidth * 2), static_cast(displayEyeHeight) }; + } + + // Phase 2: called from BSShaderRenderTargets_Create::thunk (before func()) + // where BSOpenVR is guaranteed to be available + void InstallRenderTargetSizeHook(); + + // Hybrid Post: tonemap interception via IS shader hooks + // Call BeginPostIntercept() before func(), EndPostIntercept() after. + void BeginPostIntercept(); + void EndPostIntercept(); + + // Downscale testTexture (3k AA'd DLSS output) → kMAIN (1k) + // so the HDR pyramid builds from anti-aliased content instead of raw 1k render. + // Only kMAIN: no-refra reads kMAIN directly; with-refra engine copies kMAIN→kMAIN_COPY. + void DownscaleToKMain(); + + // Post hybrid entry point: called from Upscaling's Main_PostProcessing::thunk. + // Wraps the engine Post chain with DLSSperf's two-layer struct swap. + // Keyed on postPipelineReady (set at the end of SetupResources) so a + // partial-init state can't slip past the gate into a null deref. The + // runtime upscaler-method gate is enforced separately by callers (the + // engine's BSOpenVR hook is install-time, so a mid-session DLSS→FSR swap + // would leave hookActive=true but testTexture stale — see Upscaling.cpp). + bool ShouldHandlePost() const { return postPipelineReady; } + void HandlePostProcessing(const std::function& enginePost); + + // Fake 3k DepthStencil for Post pass DS swap + ID3D11DepthStencilView* GetFakeDSV() const { return fakeDSV.get(); } + + // Bridge the DLSS-reconstructed menu BG (testTexture, displayRes) into + // the bound enlarged RT so OpenVR submit sees both BG + UI compositor + // output. One-shot per frame; gated via blittedFrameId. + void MaybeBlitMenuBG(uint32_t boundRTIdx); + + // Generic DS swap for draws binding an enlarged RT against kMAIN/kMAIN + // _COPY DS — without this the rasterizer clips to the smaller DS and + // fills only the renderRes corner of the enlarged RT. Skipped when + // postInterceptActive (HandlePostProcessing already redirects DS). + bool MaybeSwapDSForEnlargedRT(); + void RestoreSwappedDS(); + + // Install the 3 named per-site CreateRenderTarget thunks (kMENUBG, + // kIMAGESPACE_TEMP_COPY, kTOTAL — VR-only) at startup. Called from + // Hooks::Install in the BSShaderRenderTargets::Create install sequence. + // Thunks are no-ops outside the BeginEnlarge/EndEnlarge window so flat + // Skyrim is unaffected. + void InstallCreateRTThunks(); + + // Install the Draw vfunc detour (D3D11DeviceContext vtable index 13) + // that fixes the scene-fade overlay viewport. Called from Globals:: + // InstallD3DHooks. VR-only; thunk early-outs unless VertexCount==30 + // and the hook is live, so cost is one comparison per Draw call when + // DLSSperf isn't active. + void InstallFadeOverlayHook(ID3D11DeviceContext* context); + + // Enlarge window — set true around the engine's BSShaderRenderTargets:: + // Create call from Hooks.cpp's wrapper. The 3 installed thunks read + // enlargeActive/Width/Height directly. + void BeginCreateRTEnlarge(); + void EndCreateRTEnlarge(); + bool IsCreateRTEnlargeActive() const { return enlargeActive; } + uint32_t GetEnlargeWidth() const { return enlargeWidth; } + uint32_t GetEnlargeHeight() const { return enlargeHeight; } + +private: + // RAII snapshot of the D3D11 pipeline state our fullscreen passes + // (DownscaleToKMain, MaybeBlitMenuBG) overwrite: OM RT/DS, viewport array, + // blend, depth-stencil, VS/PS/GS/HS/DS/RS, PS sampler+SRV slot 0, IA + // layout/VB/IB/topology. Captured on ctor, restored + Released on dtor. + // We're sandwiched between engine passes that don't fully rebind, so any + // leaked binding shows up as corruption downstream. + struct FullscreenPassScope + { + explicit FullscreenPassScope(ID3D11DeviceContext* a_context); + ~FullscreenPassScope(); + FullscreenPassScope(const FullscreenPassScope&) = delete; + FullscreenPassScope& operator=(const FullscreenPassScope&) = delete; + + private: + ID3D11DeviceContext* ctx = nullptr; + ID3D11RenderTargetView* savedRTV = nullptr; + ID3D11DepthStencilView* savedDSV = nullptr; + UINT numVP = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; + D3D11_VIEWPORT savedVP[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE] = {}; + ID3D11BlendState* savedBlend = nullptr; + FLOAT savedBlendFactor[4] = {}; + UINT savedSampleMask = 0; + ID3D11DepthStencilState* savedDSState = nullptr; + UINT savedStencilRef = 0; + ID3D11VertexShader* savedVS = nullptr; + ID3D11PixelShader* savedPS = nullptr; + ID3D11GeometryShader* savedGS = nullptr; + ID3D11HullShader* savedHS = nullptr; + ID3D11DomainShader* savedDS = nullptr; + ID3D11RasterizerState* savedRS = nullptr; + ID3D11SamplerState* savedSampler0 = nullptr; + ID3D11ShaderResourceView* savedSRV0 = nullptr; + ID3D11InputLayout* savedIL = nullptr; + ID3D11Buffer* savedVB[D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT] = {}; + UINT savedVBStride[D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT] = {}; + UINT savedVBOffset[D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT] = {}; + ID3D11Buffer* savedIB = nullptr; + DXGI_FORMAT savedIBFormat = DXGI_FORMAT_UNKNOWN; + UINT savedIBOffset = 0; + D3D11_PRIMITIVE_TOPOLOGY savedTopology = D3D11_PRIMITIVE_TOPOLOGY_UNDEFINED; + }; + + // Phase 1 + winrt::com_ptr testTexture; + winrt::com_ptr testTextureSRV; + winrt::com_ptr testTextureUAV; + + // Phase 2: resolution hook state + bool hookActive = false; + + // Set at the end of SetupResources after every critical Post resource + // (textures, views, fake DS, downscale shaders, sampler) successfully + // initialized. ShouldHandlePost() returns this — a partial-init state + // (e.g., refraTempTex OOM after testTexture succeeds) flips this to false + // and the engine Post chain runs unwrapped on the small kMAIN, which is + // visually degraded but stable. + bool postPipelineReady = false; + + // Post intercept phase flag: when true, VP post-correction is skipped + // so enlarged kTEMP/kTOTAL get correct 3k VP from engine. + bool postInterceptActive = false; + + // Post-chain-done flag: set true after EndPostIntercept, cleared at + // PlayerView end by PlayerViewRender_Hook. When true, UpdateViewPort + // hook expands VP to displayRes so draws after the Post chain + // (UI composition, scene fade, submit prep) use the correct + // display-res VP. + bool postChainDone = false; + + uint32_t displayEyeWidth = 0; + uint32_t displayEyeHeight = 0; + uint32_t renderEyeWidth = 0; + uint32_t renderEyeHeight = 0; + + // Boot snapshot — see HasBootSnapshot() accessor above. + uint32_t bootUpscaleMethod = 0; + uint32_t bootQualityMode = 0; + + // Phase 2: vtable hook for BSOpenVR::GetRenderTargetSize (vfunc 0x12) + struct GetRenderTargetSize_Hook + { + static void thunk(RE::BSOpenVR* a_this, uint32_t* a_width, uint32_t* a_height); + static inline REL::Relocation func; + }; + + // IS shader hook: ISHDRTonemapBlendCinematic (Render vfunc 0x1 on vtable[3]) + // Chains after FrameAnnotations (if active). Swaps kMAIN SRV + kMAIN DS before + // tonemap, restores after. + struct TonemapRender_Hook + { + static void thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param); + static inline REL::Relocation func; + }; + bool tonemapHookInstalled = false; + + // IS shader hook: ISRefraction (Render vfunc 0x1 on vtable[3]) + // Replay DrawIndexed: func() runs 1k refraction normally, then replays 3k draw with sticky D3D state. + struct RefractionRender_Hook + { + static void thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param); + static inline REL::Relocation func; + }; + bool refractionHookInstalled = false; + + // IS shader hook: ISCopy (Render vfunc 0x1 on vtable[3]). + // The VR main menu / pause compositor uses a single ISCopy draw from kMAIN + // (RenderRes when DLSSperf is active) into kPROJECTEDMENU (fixed 2048²) or + // kMENUBG (DisplayRes via enlargement). With a 1:1 viewport the small + // source gets stamped into the top-left of the larger dest — that's the + // "main menu looks downscaled" bug. Strategy: let func() draw normally, + // then if dest > VP, replay the draw with the viewport stretched to the + // dest's full dims so the sampler-clamped source is rescaled across the + // whole panel. ISCopy's PS/CB/IA stay sticky on the context after func(), + // so the replay only needs a VP change + a DrawIndexed. + struct ISCopyRender_Hook + { + static void thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param); + static inline REL::Relocation func; + }; + bool isCopyHookInstalled = false; + + // UI pass hook: FinishAccumulatingDispatch (vfunc 0x2A on BSShaderAccumulator) + // When renderMode==24 (UI pass), swaps KMAIN DS → fakeDS so 3k kMENUBG gets 3k depth. + struct UIPassDispatch_Hook + { + static void thunk(RE::BSGraphics::BSShaderAccumulator* shaderAccumulator, uint32_t renderFlags); + static inline REL::Relocation func; + }; + bool uiPassHookInstalled = false; + + // PlayerView end hook: Main_RenderPlayerView (REL 35560/36559) + // Clears postChainDone after the entire VR pipeline (World→Post→UI→Submit) + // so Present-前 UI chain and next frame use normal VP compression. + struct PlayerViewRender_Hook + { + static void thunk(void* a1, bool a2, bool a3); + static inline REL::Relocation func; + }; + bool playerViewHookInstalled = false; + + // Chains via stl::detour_thunk on the same address Hooks.cpp + Terrain- + // Blending already detour. Wraps MaybeSwapDSForEnlargedRT around the + // engine's RT/DS flush; runs after the prior thunk so DLSSperf's swap + // is the innermost wrap. + struct BSGraphics_SetDirtyStates_Hook + { + static void thunk(bool isCompute); + static inline REL::Relocation func; + }; + bool setDirtyStatesHookInstalled = false; + + // D3D11 Draw vfunc detour. Engine's scene-fade overlay is a Draw(30) + // that fires after the Post chain and before Submit. Under DLSSperf + // the draw's VP/vertices are computed at renderRes while the RT + // (kTOTAL) is displayRes — produces a partial-screen "black stamp" + // without this swap. + struct ID3D11DeviceContext_Draw_Hook + { + static void thunk(ID3D11DeviceContext* This, UINT VertexCount, UINT StartVertexLocation); + static inline REL::Relocation func; + }; + + // Post-corrects the engine viewport whenever it differs from our + // enlarged RTs. Chains via stl::detour_thunk. + struct BSGraphics_Renderer_UpdateViewPort_Hook + { + static void thunk(RE::BSGraphics::Renderer* a_this, uint32_t a_width, uint32_t a_height, bool a_forceMatchRT); + static inline REL::Relocation func; + }; + bool updateViewPortHookInstalled = false; + + // Refraction: 3k temp texture (copy of testTexture) for ISRefraction input + winrt::com_ptr refraTempTex; + winrt::com_ptr refraTempSRV; + // Refraction: RTV for testTexture (ISRefraction 3k output target) + winrt::com_ptr testTextureRTV; + + // Two-layer swap: saved pointers for restore. + // Outer layer (BeginPostIntercept/EndPostIntercept): kMAIN_COPY DS views + // only — the engine writes the post chain's DS through kMAIN_COPY's + // depth slot, so we redirect it at the start/end of Post. + // Inner layer (TonemapRender_Hook): kMAIN + kMAIN_COPY SRVs and kMAIN DS + // views — the tonemap shader reads from kMAIN SRV (and the refraction + // path reads from kMAIN_COPY SRV); both need to point at testTextureSRV + // so the tonemap consumes the AA'd 3k DLSS output instead of the small + // kMAIN. savedKMainCopySRV is captured/restored by the inner layer, not + // the outer one + ID3D11DepthStencilView* savedKMainCopyViews[8] = {}; + ID3D11DepthStencilView* savedKMainCopyReadOnlyViews[8] = {}; + ID3D11ShaderResourceView* savedKMainCopySRV = nullptr; + ID3D11DepthStencilView* savedKMainViews[8] = {}; + ID3D11DepthStencilView* savedKMainReadOnlyViews[8] = {}; + ID3D11ShaderResourceView* savedKMainSRV = nullptr; + + // Fake 3k DepthStencil (DisplayRes, same format as engine kMAIN DS) + winrt::com_ptr fakeDS; + winrt::com_ptr fakeDSV; + + // autoSwapDSIdx == UINT32_MAX → no active swap; otherwise it's the + // kMAIN/kMAIN_COPY slot whose views[] were rewritten and must be + // restored on the matching RestoreSwappedDS(). + ID3D11DepthStencilView* autoSwapSavedViews[8] = {}; + ID3D11DepthStencilView* autoSwapSavedReadOnlyViews[8] = {}; + uint32_t autoSwapDSIdx = UINT32_MAX; + + // Downscale pass: Box 3×3 downscale testTexture (3k) → kMAIN (1k). + // (Named "boxDownscale" — earlier revisions called this "bilinearCopy" + // when the implementation was a true bilinear sample. It became a 9-tap + // box during development; the rename happened pre-release.) + winrt::com_ptr boxDownscalePS; + winrt::com_ptr boxDownscaleVS; + winrt::com_ptr linearSampler; + + // Menu BG blit — fullscreen sample of testTexture into kTOTAL/kMENUBG + // with format conversion (R16G16B16A16_FLOAT → R8G8B8A8_UNORM via the + // RTV view). Reuses boxDownscaleVS + linearSampler. blittedFrameId is + // the per-frame one-shot guard, compared against state->frameCount + // (PlayerView doesn't fire in main menu, so a flag-clear hook wouldn't + // reliably reset across all states). + winrt::com_ptr menuBlitPS; + uint32_t blittedFrameId = UINT32_MAX; + + // CreateRenderTarget enlarge window — see BeginCreateRTEnlarge. + bool enlargeActive = false; + uint32_t enlargeWidth = 0; + uint32_t enlargeHeight = 0; + + // Per-site CreateRenderTarget thunks. Each fires from a single + // installed call site within BSShaderRenderTargets::Create and overrides + // the RT's allocation dimensions when the enlarge window is active. + // Static `func` ptrs are per-struct so each chains the original + // CreateRenderTarget call independently. + struct CreateRT_MenuBG_Hook + { + static void thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties); + static inline REL::Relocation func; + }; + struct CreateRT_ImagespaceTempCopy_Hook + { + static void thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties); + static inline REL::Relocation func; + }; + struct CreateRT_Total_Hook + { + static void thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties); + static inline REL::Relocation func; + }; +}; diff --git a/src/Features/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index d7517c679f..30d29262f2 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -10,6 +10,7 @@ #include "../../State.h" #include "../../Util.h" #include "../Upscaling.h" +#include "DLSSperf.h" #include "DX12SwapChain.h" namespace @@ -420,8 +421,9 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) { sl::DLSSOptions dlssOptions{}; - // Map quality mode to DLSS mode - uint32_t qualityMode = globals::features::upscaling.settings.qualityMode; + // Boot qualityMode under DLSSperf — DLSS dispatch must match the + // renderRes the engine was sized for at install. + uint32_t qualityMode = globals::features::upscaling.dlssPerf.HasBootSnapshot() ? globals::features::upscaling.dlssPerf.GetBootQualityMode() : globals::features::upscaling.settings.qualityMode; switch (qualityMode) { case 1: dlssOptions.mode = sl::DLSSMode::eMaxQuality; @@ -442,8 +444,15 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) auto state = globals::state; + // DLSSperf bridge: state->screenSize.y is polluted to RenderRes by the + // BSOpenVR size hook; use dlssPerf's snapshot of the real DisplayRes when + // the hook is live so DLSS is created at the right scale. The width arg + // is already display-correct (caller computes from displaySize). + auto& dlssPerf = globals::features::upscaling.dlssPerf; + const bool dlssperfActive = dlssPerf.IsHookActive() && dlssPerf.GetTestTexture(); + dlssOptions.outputWidth = width; - dlssOptions.outputHeight = (uint)state->screenSize.y; + dlssOptions.outputHeight = dlssperfActive ? (uint)dlssPerf.GetDisplayScreenSize().y : (uint)state->screenSize.y; // Detect HDR from kMAIN format at runtime -- VR kMAIN may be 8-bit while SE is FP16 { @@ -597,11 +606,24 @@ void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r auto screenSize = state->screenSize; auto renderSize = Util::ConvertToDynamic(screenSize); - // When RCAS sharpening is active, direct DLSS output to sharpenerTexture so RCAS can - // sharpen directly into kMAIN.UAV without a CopyResource round-trip. + // DLSSperf bridge: when the BSOpenVR size hook is live, state->screenSize + // is polluted to RenderRes (the spoofed HMD recommended size). DLSS must + // be told the TRUE DisplayRes for its output extent, otherwise NGX rejects + // the evaluate as InvalidParameter (0xbad00005) because the configured + // quality-scale doesn't match the actual extent ratio. The upscale also + // has to write into dlssPerf's private DisplayRes testTexture instead of + // the now-RenderRes kMAIN. auto& upscaling = globals::features::upscaling; + auto& dlssPerf = globals::features::upscaling.dlssPerf; + const bool dlssperfActive = dlssPerf.IsHookActive() && dlssPerf.GetTestTexture(); + const auto displaySize = dlssperfActive ? dlssPerf.GetDisplayScreenSize() : screenSize; + + // When RCAS sharpening is active, direct DLSS output to sharpenerTexture so RCAS can + // sharpen directly into kMAIN.UAV without a CopyResource round-trip. DLSSperf + // bypasses the sharpener entirely (writes DLSS output straight into testTexture). ID3D11Resource* colorOut = - (upscaling.settings.sharpnessDLSS > 0.0f && upscaling.sharpenerTexture) ? upscaling.sharpenerTexture->resource.get() : a_upscalingTexture; + dlssperfActive ? static_cast(dlssPerf.GetTestTexture()) : + ((upscaling.settings.sharpnessDLSS > 0.0f && upscaling.sharpenerTexture) ? upscaling.sharpenerTexture->resource.get() : a_upscalingTexture); // VR stereo DLSS: NGX D3D11 only accepts zero-offset subrects. Non-zero offsets return // FAIL_InvalidParameter because Streamline's dlssEntry.cpp never sets @@ -617,8 +639,8 @@ void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r if (globals::game::isVR) { auto context = globals::d3d::context; - uint32_t eyeWidthOut = (uint32_t)(screenSize.x / 2); - uint32_t eyeHeightOut = (uint32_t)screenSize.y; + uint32_t eyeWidthOut = (uint32_t)(displaySize.x / 2); + uint32_t eyeHeightOut = (uint32_t)displaySize.y; uint32_t eyeWidthIn = (uint32_t)(renderSize.x / 2); uint32_t eyeHeightIn = (uint32_t)renderSize.y; @@ -676,12 +698,12 @@ void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r } else { // Non-VR: Simple full-texture upscale. sl::Extent extentIn{ 0, 0, (uint)renderSize.x, (uint)renderSize.y }; - sl::Extent extentOut{ 0, 0, (uint)screenSize.x, (uint)screenSize.y }; + sl::Extent extentOut{ 0, 0, (uint)displaySize.x, (uint)displaySize.y }; EvaluateDLSS(viewport, 0, a_upscalingTexture, colorOut, depthTexture.texture, a_motionVectors, a_reactiveMask, a_transparencyCompositionMask, - extentIn, extentOut, (uint)screenSize.x); + extentIn, extentOut, (uint)displaySize.x); } } diff --git a/src/Globals.cpp b/src/Globals.cpp index d216a05b74..8deee22e3d 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -404,5 +404,16 @@ namespace globals stl::detour_vfunc<36, ID3D11DeviceContext_OMSetDepthStencilState>(a_context); stl::detour_vfunc<53, ID3D11DeviceContext_ClearDepthStencilView>(a_context); } + + // Scene-fade overlay Draw(30) detour — only useful when DLSSperf will + // actually go active. The hook's thunk already early-outs on + // VertexCount != 30 || !hookActive, but skipping the vtable patch + // entirely when the user has DLSSperf off avoids a foreign-interop + // surface other context-vfunc hookers could trip on. Gated by the + // persisted intent, not IsHookActive(), because the hook is installed + // here at D3D init time while IsHookActive() only flips true later + // inside BSShaderRenderTargets::Create. + if (globals::game::isVR && globals::features::upscaling.settings.enableDLSSperf) + globals::features::upscaling.dlssPerf.InstallFadeOverlayHook(a_context); } } diff --git a/src/Hooks.cpp b/src/Hooks.cpp index afe2d4cda7..c71636c886 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -369,9 +369,38 @@ struct BSShaderRenderTargets_Create static void thunk() { Util::SetGameSettingValue("iNumFocusShadow:Display", iNumFocusShadow, 0); + + // DLSSperf: install the BSOpenVR render-target-size hook before the + // engine creates its render targets. This is the only place where + // BSOpenVR is guaranteed available AND we can still influence RT + // allocation. Gated on user opt-in via Upscaling::Settings AND on + // DLSS actually being the resolved upscale path — a stale config can + // leave enableDLSSperf=true while the active method is FSR/TAA or + // DLSS is unsupported on this GPU, and the rest of DLSSperf only + // makes sense for the DLSS output path. + const bool dlssperfShouldRun = + globals::game::isVR && + globals::features::upscaling.settings.enableDLSSperf && + globals::features::upscaling.GetUpscaleMethod() == Upscaling::UpscaleMethod::kDLSS; + + if (dlssperfShouldRun) { + globals::features::upscaling.dlssPerf.InstallRenderTargetSizeHook(); + } + + // Open DLSSperf's enlarge window across the engine's Create() so + // its 3 per-site thunks override props for the displayRes RTs. + auto& dlssPerf = globals::features::upscaling.dlssPerf; + dlssPerf.BeginCreateRTEnlarge(); func(); + dlssPerf.EndCreateRTEnlarge(); + globals::ReInit(); globals::state->Setup(); + + // DLSSperf is not in the Feature list (it's a worker driven by the + // upscaling toggle), so SetupResources runs here directly. + if (dlssPerf.IsHookActive()) + dlssPerf.SetupResources(); } static inline REL::Relocation func; }; @@ -867,6 +896,8 @@ namespace Hooks stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA25, 0xA25, 0xCD2)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA59, 0xA59, 0xD13)); + globals::features::upscaling.dlssPerf.InstallCreateRTThunks(); + #ifdef TRACY_ENABLE stl::write_thunk_call(REL::RelocationID(35551, 36544).address() + REL::Relocate(0x11F, 0x160)); #endif diff --git a/src/Utils/Subrect.cpp b/src/Utils/Subrect.cpp index 61512fdbe2..0284d42cb6 100644 --- a/src/Utils/Subrect.cpp +++ b/src/Utils/Subrect.cpp @@ -247,7 +247,6 @@ namespace Util::Subrect // explicit right eye and disable the auto-mirror fallback // once stereo is later enabled. Leave rightUV as nullopt in // mono — ApplyPreset will mirror left at apply time. - // (CodeRabbit Major @ scs#2356 for the stereo-side fix.) Preset newPreset{ .name = presetName, .uv = currentUV }; if (stereoEnabled) { newPreset.rightUV = currentRightUV; diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 404d7cfdb8..f105e48e1f 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1424,6 +1424,11 @@ namespace Util return globals::menu->GetTheme().StatusPalette.Disable; } + ImVec4 GetRestartNeeded() + { + return globals::menu->GetTheme().StatusPalette.RestartNeeded; + } + } namespace Text @@ -1469,6 +1474,8 @@ namespace Util UTIL_TEXT_WRAPPED(WrappedInfo, GetInfo) UTIL_TEXT(Disabled, GetDisabled) UTIL_TEXT_WRAPPED(WrappedDisabled, GetDisabled) + UTIL_TEXT(RestartNeeded, GetRestartNeeded) + UTIL_TEXT_WRAPPED(WrappedRestartNeeded, GetRestartNeeded) #undef UTIL_TEXT #undef UTIL_TEXT_WRAPPED diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 7a7aec368a..177a8970c3 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -953,6 +953,8 @@ namespace Util void WrappedInfo(const char* fmt, ...) IM_FMTARGS(1); void Disabled(const char* fmt, ...) IM_FMTARGS(1); void WrappedDisabled(const char* fmt, ...) IM_FMTARGS(1); + void RestartNeeded(const char* fmt, ...) IM_FMTARGS(1); + void WrappedRestartNeeded(const char* fmt, ...) IM_FMTARGS(1); } /** From 5c00a387a9344bb3f010a677e5680e39ccea839f Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 24 May 2026 17:37:14 -0700 Subject: [PATCH 16/24] build: drop /XO from robocopy auto-deploy (#37) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Drop `/XO` (eXclude Older) from the three robocopy invocations under `AUTO_PLUGIN_DEPLOYMENT` so changed source files actually reach the install - Update inline comments to document the trap (any prior touch to dest can give it a newer mtime than git-checkout source, which silently disables deploy) ## Why `/XO` skips files where source is older than dest. In a normal git workflow that's usually safe — but as soon as anything *else* touches the destination (a manual `cp` during debugging, a package install, even a previous build that ran later in wall time than the source's git checkout), dest gets a newer mtime and `/XO` then refuses to overwrite even when the source file's **size and content** have changed. ## Concrete failure that prompted this While debugging SLF I had RunGrass.hlsl fixed in source (commit `f11bfaa32`, source size 33287 bytes with the corrected ShadowSampling include order), but the deployed file stayed at the previous 31877 bytes across multiple full builds. Mtime on dest (from a manual `cp` during earlier diagnostics) was newer than the git-checkout mtime on source, so `/XO` skipped the copy. Grass and Particle pixel shaders failed to compile in-game: > `LightLimitFix/LightLimitFix.hlsli(85,3-28): error X3000: unrecognized identifier 'DirectionalShadowLightData'` Same shape of bug can hit any contributor who: tests a package install over their dev build, copies files manually for any reason, or just has clock skew across filesystem boundaries. ## What this changes | Before | After | |---|---| | \`/E /XD ... /COPY:DAT /XO /R:1 /W:1 ...\` | \`/E /XD ... /COPY:DAT /R:1 /W:1 ...\` | Without `/XO`, robocopy uses its default name + size + timestamp delta — copies whenever **any** of those differ. Catches both newer-source and size-mismatch cases. The git-HEAD-change block above the deploy commands already clears AIO and deploy stamps on branch switch, so the bulk-redeploy story is unchanged. ## Trade-off The property `/XO` nominally provided ("don't clobber files the user intentionally edited in-game between builds at the same HEAD") was fragile anyway — broken by any external touch to dest. Removing it costs ~100ms of extra IO per build on unchanged files (robocopy still skips matching name+size+timestamp). Correctness wins. ## Test plan - [ ] \`BuildRelease.bat ALL-WITH-AUTO-DEPLOYMENT\` produces a deployed install that matches \`build//aio\` byte-for-byte - [ ] Manually \`touch\` a file in the deployed install to give it a newer mtime, rebuild, confirm the source file overwrites it - [ ] Verify unchanged files still skip on subsequent builds (robocopy's default behavior) 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Chores** * Adjusted Windows plugin deployment configuration to improve synchronization between deployed and built files. Updated build system documentation comments. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/alandtse/open-shaders/pull/37?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- CMakeLists.txt | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 85d99e83d9..e7b606ade3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -765,11 +765,12 @@ if(AUTO_PLUGIN_DEPLOYMENT) # Detect git HEAD changes (branch switch or new commit) and invalidate stale # deploy state. BuildRelease.bat always reconfigures, so this runs on every # build invocation. When HEAD changes we: - # 1. Touch all AIO files so robocopy /XO sees them as newer than any - # previously-deployed files (including manual package installs). + # 1. Clear the AIO directory so PREPARE_AIO re-copies everything with + # fresh timestamps (catches content-identical files cmake's + # copy_if_different would otherwise skip). # 2. Delete deploy stamps so CMake re-runs the robocopy commands. - # Files the user intentionally edited in-game remain protected by /XO on - # subsequent builds (same HEAD = no touch, same stamp file exists). + # Per-build incremental relies on robocopy's default name+size+timestamp + # detection to copy any file whose size or mtime differs from dest. execute_process( COMMAND git rev-parse --short HEAD WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" @@ -792,10 +793,10 @@ if(AUTO_PLUGIN_DEPLOYMENT) # Delete the entire AIO directory so PREPARE_AIO re-copies everything # with fresh timestamps. Without this, copy_if_different skips # content-identical files (shaders, textures, configs, etc.) and - # leaves them with old timestamps that deployed files (e.g. from a - # manual package install) may beat under /XO. The DLL is always - # rebuilt fresh by the C++ compile step so it doesn't need special - # handling. + # leaves them with old timestamps that previously-deployed files + # (e.g. from a manual package install) can beat on timestamp delta. + # The DLL is always rebuilt fresh by the C++ compile step so it + # doesn't need special handling. file(REMOVE_RECURSE "${AIO_DIR}") # Also clear the PREPARE_AIO / COPY_SHADERS stamps so cmake --build # actually re-runs those targets. @@ -833,10 +834,18 @@ if(AUTO_PLUGIN_DEPLOYMENT) add_custom_command( OUTPUT ${DEPLOY_TARGET_HASH}_deploy.stamp COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_TARGET}" + # No /XO: a previous manual or external deploy can leave dest + # files with mtime > source. /XO then skips them even when the + # source file changed (different size and/or content), so build + # output silently diverges from what runs in-game. Without /XO, + # robocopy's default name+size+timestamp delta catches both + # newer-source and size-mismatch cases. The git-HEAD-change + # block above handles bulk-redeploy when checking out a new + # branch; this command handles the per-build incremental. COMMAND ${ROBOCOPY_WRAPPER} "${AIO_DIR}" "${DEPLOY_TARGET}" "/E" - "/XD" "${AIO_DIR}/Shaders" "/COPY:DAT" "/XO" "/R:1" "/W:1" - "/NFL" "/NDL" "/NJH" "/NJS" + "/XD" "${AIO_DIR}/Shaders" "/COPY:DAT" "/R:1" "/W:1" "/NFL" + "/NDL" "/NJH" "/NJS" COMMAND ${CMAKE_COMMAND} -E touch ${DEPLOY_TARGET_HASH}_deploy.stamp DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp @@ -876,10 +885,11 @@ if(AUTO_PLUGIN_DEPLOYMENT) COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_TARGET}/Shaders" + # See /XO rationale above on the main deploy block. COMMAND ${ROBOCOPY_WRAPPER} "${AIO_DIR}/Shaders" - "${DEPLOY_TARGET}/Shaders" "/E" "/COPY:DAT" "/XO" "/R:1" - "/W:1" "/NFL" "/NDL" "/NJH" "/NJS" + "${DEPLOY_TARGET}/Shaders" "/E" "/COPY:DAT" "/R:1" "/W:1" + "/NFL" "/NDL" "/NJH" "/NJS" COMMAND ${CMAKE_COMMAND} -E touch ${DEPLOY_TARGET_HASH}_shaders_only.stamp @@ -899,10 +909,11 @@ if(AUTO_PLUGIN_DEPLOYMENT) COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_TARGET}/Shaders" + # See /XO rationale above on the main deploy block. COMMAND ${ROBOCOPY_WRAPPER} "${AIO_DIR}/Shaders" - "${DEPLOY_TARGET}/Shaders" "/E" "/COPY:DAT" "/XO" "/R:1" - "/W:1" "/NFL" "/NDL" "/NJH" "/NJS" + "${DEPLOY_TARGET}/Shaders" "/E" "/COPY:DAT" "/R:1" "/W:1" + "/NFL" "/NDL" "/NJH" "/NJS" COMMAND ${CMAKE_COMMAND} -E touch ${DEPLOY_TARGET_HASH}_shaders_full.stamp From 045e72efa8ddc4a97f172cad0444fc6ad1a893b5 Mon Sep 17 00:00:00 2001 From: Codex <242516109+Codex@users.noreply.github.com> Date: Mon, 25 May 2026 01:23:58 -0700 Subject: [PATCH 17/24] refactor: unify restart-required infrastructure (#39) Co-authored-by: openai-code-agent[bot] <242516109+Codex@users.noreply.github.com> Co-authored-by: alandtse <7086117+alandtse@users.noreply.github.com> Co-authored-by: Alan Tse Co-authored-by: Claude Opus 4.7 --- .claude/CLAUDE.md | 1 + src/Feature.h | 37 +++++++ src/Features/DynamicCubemaps.cpp | 24 ++--- src/Features/DynamicCubemaps.h | 17 +++- src/Features/RemoteControl.cpp | 40 +++++++- src/Features/RenderDoc.cpp | 63 ++++++------ src/Features/RenderDoc.h | 26 ++++- src/Features/Upscaling.cpp | 45 ++++----- src/Features/Upscaling.h | 27 ++++++ src/Features/Upscaling/DLSSperf.cpp | 7 +- src/Features/Upscaling/DLSSperf.h | 11 --- src/Features/Upscaling/Streamline.cpp | 2 +- src/Features/Upscaling/Streamline.h | 1 - src/Features/VR.cpp | 2 + src/Features/VRStereoOptimizations.cpp | 7 +- src/Features/VRStereoOptimizations.h | 11 +++ src/Features/VolumetricLighting.cpp | 12 ++- src/Features/VolumetricLighting.h | 17 +++- src/Features/WeatherEditor.cpp | 3 +- src/Hooks.cpp | 5 + src/Menu/FeatureListRenderer.cpp | 6 +- src/Utils/BootSnapshot.h | 129 +++++++++++++++++++++++++ src/Utils/RestartSettings.h | 40 ++++++++ src/Utils/UI.h | 69 +++++++++++++ src/WeatherEditor/EditorWindow.cpp | 37 ++++--- tests/cpp/CMakeLists.txt | 1 + tests/cpp/test_bootsnapshot.cpp | 63 ++++++++++++ 27 files changed, 581 insertions(+), 122 deletions(-) create mode 100644 src/Utils/BootSnapshot.h create mode 100644 src/Utils/RestartSettings.h create mode 100644 tests/cpp/test_bootsnapshot.cpp diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 1a4f89c619..c4cb716895 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -373,6 +373,7 @@ Feature versions are automatically extracted from `.ini` files and compiled into - JSON-based settings with nlohmann_json - Hot-reload capability through ImGui interface - Versioned feature configurations for compatibility +- Restart-gated fields use `Util::Settings::BootSnapshot` + `kRestartFields` metadata to diff boot-latched vs selected values (drives `Util::Text::RestartNeeded` banners and MCP introspection; see Upscaling for a canary) ### Error Handling diff --git a/src/Feature.h b/src/Feature.h index 95ca1741a7..e8e75a0afe 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -3,6 +3,11 @@ #include "FeatureCategories.h" #include "FeatureConstraints.h" #include "FeatureVersions.h" +#include "Utils/RestartSettings.h" + +#include +#include +#include #ifdef TRACY_ENABLE # include # include @@ -21,6 +26,38 @@ struct Feature // Override in features to expose settings for search virtual std::vector GetSettingsSearchEntries() { return {}; } + // Restart-required settings introspection. Default: none. + // Features with restart-gated fields override these to expose them to UI + // helpers and MCP/RemoteControl without per-feature glue. + virtual std::span GetRestartRequiredFields() const { return {}; } + virtual const void* GetBootValue(std::string_view /*jsonKey*/) const { return nullptr; } + virtual const void* GetSettingsBlob() const { return nullptr; } + virtual size_t GetSettingsBlobSize() const { return 0; } + + // True if any restart-gated setting's live value differs from the + // boot-latched value. Drives the green "RestartNeeded" tint in the + // feature list and the `pending` flag in MCP's `list` response. + bool HasAnyPendingRestart() const + { + const auto fields = GetRestartRequiredFields(); + if (fields.empty()) + return false; + const auto* live = reinterpret_cast(GetSettingsBlob()); + const size_t liveSize = GetSettingsBlobSize(); + if (!live || liveSize == 0) + return false; + for (const auto& field : fields) { + if (!field.jsonKey || field.size == 0) + continue; + if (field.offset + field.size > liveSize) + continue; + const void* boot = GetBootValue(field.jsonKey); + if (boot && std::memcmp(boot, live + field.offset, field.size) != 0) + return true; + } + return false; + } + // Nexus Mods base URL for Skyrim Special Edition static constexpr std::string_view NEXUS_BASE_URL = "https://www.nexusmods.com/skyrimspecialedition/mods/"; bool loaded = false; diff --git a/src/Features/DynamicCubemaps.cpp b/src/Features/DynamicCubemaps.cpp index 560c902452..1d09edfae7 100644 --- a/src/Features/DynamicCubemaps.cpp +++ b/src/Features/DynamicCubemaps.cpp @@ -6,6 +6,7 @@ #include "ShaderCache.h" #include "State.h" #include "Utils/D3D.h" +#include "Utils/UI.h" constexpr auto MIPLEVELS = 8; @@ -30,14 +31,9 @@ void DynamicCubemaps::DrawSettings() recompileFlag |= ImGui::Checkbox("Enable Screen Space Reflections", reinterpret_cast(&settings.EnabledSSR)); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Enable Screen Space Reflections on Water"); - if (REL::Module::IsVR() && !enabledAtBoot) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); - ImGui::Text( - "A restart is required to enable in VR. " - "Save Settings after enabling and restart the game."); - ImGui::PopStyleColor(); - } } + if (globals::game::isVR) + Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::EnabledSSR); ImGui::TreePop(); } @@ -119,7 +115,7 @@ void DynamicCubemaps::DrawSettings() } ImGui::TreePop(); } - if (REL::Module::IsVR()) { + if (globals::game::isVR) { if (ImGui::TreeNodeEx("Advanced VR Settings", ImGuiTreeNodeFlags_DefaultOpen)) { Util::RenderImGuiSettingsTree(iniVRCubeMapSettings, "VR"); Util::RenderImGuiSettingsTree(hiddenVRCubeMapSettings, "hiddenVR"); @@ -131,7 +127,7 @@ void DynamicCubemaps::DrawSettings() void DynamicCubemaps::LoadSettings(json& o_json) { settings = o_json; - if (REL::Module::IsVR()) { + if (globals::game::isVR) { Util::LoadGameSettings(iniVRCubeMapSettings); } recompileFlag = true; @@ -140,7 +136,7 @@ void DynamicCubemaps::LoadSettings(json& o_json) void DynamicCubemaps::SaveSettings(json& o_json) { o_json = settings; - if (REL::Module::IsVR()) { + if (globals::game::isVR) { Util::SaveGameSettings(iniVRCubeMapSettings); } } @@ -148,7 +144,7 @@ void DynamicCubemaps::SaveSettings(json& o_json) void DynamicCubemaps::RestoreDefaultSettings() { settings = {}; - if (REL::Module::IsVR()) { + if (globals::game::isVR) { Util::ResetGameSettingsToDefaults(iniVRCubeMapSettings); Util::ResetGameSettingsToDefaults(hiddenVRCubeMapSettings); } @@ -157,7 +153,7 @@ void DynamicCubemaps::RestoreDefaultSettings() void DynamicCubemaps::DataLoaded() { - if (REL::Module::IsVR()) { + if (globals::game::isVR) { // enable cubemap settings in VR Util::EnableBooleanSettings(iniVRCubeMapSettings, GetName()); Util::EnableBooleanSettings(hiddenVRCubeMapSettings, GetName()); @@ -167,7 +163,8 @@ void DynamicCubemaps::DataLoaded() void DynamicCubemaps::PostPostLoad() { - if (REL::Module::IsVR() && settings.EnabledSSR) { + bootSnapshot.LatchIfNeeded(settings); + if (globals::game::isVR && settings.EnabledSSR) { std::map earlyhiddenVRCubeMapSettings{ { "bScreenSpaceReflectionEnabled:Display", 0x1ED5BC0 }, }; @@ -180,7 +177,6 @@ void DynamicCubemaps::PostPostLoad() *setting = true; } } - enabledAtBoot = true; } } diff --git a/src/Features/DynamicCubemaps.h b/src/Features/DynamicCubemaps.h index 7b23744d41..ac46d30629 100644 --- a/src/Features/DynamicCubemaps.h +++ b/src/Features/DynamicCubemaps.h @@ -1,6 +1,7 @@ #pragma once #include "Buffer.h" +#include "Utils/BootSnapshot.h" class MenuOpenCloseEventHandler : public RE::BSTEventSink { @@ -119,7 +120,21 @@ struct DynamicCubemaps : Feature }; Settings settings; - bool enabledAtBoot = false; + + inline static constexpr Util::Settings::RestartTable kRestartFields{ { + UTIL_RESTART_FIELD(Settings, EnabledSSR, "Screen Space Reflections"), + } }; + Util::Settings::BootSnapshot bootSnapshot{ kRestartFields }; + + std::span GetRestartRequiredFields() const override + { + // VR-only: enabling SSR needs game-setting initialization at startup. + return globals::game::isVR ? std::span{ kRestartFields.data(), kRestartFields.size() } : std::span{}; + } + const void* GetBootValue(std::string_view jsonKey) const override { return bootSnapshot.RawBoot(jsonKey); } + const void* GetSettingsBlob() const override { return &settings; } + size_t GetSettingsBlobSize() const override { return sizeof(settings); } + void UpdateCubemap(); void PostDeferred(); diff --git a/src/Features/RemoteControl.cpp b/src/Features/RemoteControl.cpp index bda02d65e2..186c926345 100644 --- a/src/Features/RemoteControl.cpp +++ b/src/Features/RemoteControl.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -451,7 +452,7 @@ static mcp::json EngineStateBlob() // Helper used by feature(action="list") to build one entry per feature. static mcp::json FeatureEntry(Feature* f) { - return mcp::json({ + mcp::json entry({ { "name", f->GetName() }, { "shortName", f->GetShortName() }, { "loaded", f->loaded }, @@ -461,6 +462,36 @@ static mcp::json FeatureEntry(Feature* f) { "supportsVR", f->SupportsVR() }, { "inMenu", f->IsInMenu() }, }); + + // Inline restart-gated metadata so `list` is the single tool that answers + // "what features exist", "which fields need a restart to apply", and + // "is anything currently pending". Each entry's `pending` is true when + // the live setting differs from the boot-latched value. + const auto fields = f->GetRestartRequiredFields(); + if (!fields.empty()) { + mcp::json restartFields = mcp::json::array(); + const auto* liveBase = reinterpret_cast(f->GetSettingsBlob()); + const size_t liveSize = f->GetSettingsBlobSize(); + for (const auto& field : fields) { + bool pending = false; + if (liveBase && field.jsonKey && field.size != 0 && + field.offset + field.size <= liveSize) { + const void* boot = f->GetBootValue(field.jsonKey); + if (boot && + std::memcmp(boot, liveBase + field.offset, field.size) != 0) { + pending = true; + } + } + restartFields.push_back(mcp::json({ + { "key", field.jsonKey ? field.jsonKey : "" }, + { "label", field.label ? field.label : "" }, + { "pending", pending }, + })); + } + entry["restartFields"] = restartFields; + } + + return entry; } void RemoteControl::RegisterInspectTool() @@ -517,7 +548,11 @@ void RemoteControl::RegisterFeatureTool() "Actions:\n" " list — no other params. Returns a JSON array; " "each entry has { name, shortName, loaded, version, " - "category, isCore, supportsVR, inMenu }.\n" + "category, isCore, supportsVR, inMenu }. Features " + "with restart-gated settings also include " + "`restartFields: [{ key, label, pending }]` — " + "`pending=true` means the user has staged a change " + "that won't take effect until the next launch.\n" " get — params: shortName. Returns the " "Feature::SaveSettings(json) blob. May return null " "if the feature has no SaveSettings/LoadSettings " @@ -588,6 +623,7 @@ void RemoteControl::RegisterFeatureTool() } const std::string shortName = params.value("shortName", std::string{}); + if (shortName.empty()) { return ErrorResult("missing required parameter 'shortName'", { { "action", action } }); diff --git a/src/Features/RenderDoc.cpp b/src/Features/RenderDoc.cpp index 88011ba6b2..9baa3c1568 100644 --- a/src/Features/RenderDoc.cpp +++ b/src/Features/RenderDoc.cpp @@ -31,8 +31,13 @@ RenderDoc* RenderDoc::GetSingleton() void RenderDoc::Load() { + // Latch the boot-time value of restart-gated fields so the menu can + // surface pending diffs even though the renderdoc.dll injection itself + // only runs once per launch. + bootSnapshot.LatchIfNeeded(settings); + // Only load RenderDoc if the user has enabled capture - if (!enableRenderDocCapture) { + if (!settings.enableCapture) { logger::debug("[RenderDoc] RenderDoc capture disabled, skipping initialization"); return; } @@ -132,16 +137,17 @@ void RenderDoc::DrawSettings() bool isSectionVisible = false; // Include enable toggle and annotation forcing logic here - bool prevRenderDocCapture = enableRenderDocCapture; - if (ImGui::Checkbox("Enable RenderDoc Capture", &enableRenderDocCapture)) { - if (enableRenderDocCapture && !prevRenderDocCapture) { + bool prevRenderDocCapture = settings.enableCapture; + if (ImGui::Checkbox("Enable RenderDoc Capture", &settings.enableCapture)) { + if (settings.enableCapture && !prevRenderDocCapture) { globals::state->useFrameAnnotations = globals::state->frameAnnotations; globals::state->frameAnnotations = true; } - if (!enableRenderDocCapture && prevRenderDocCapture) { + if (!settings.enableCapture && prevRenderDocCapture) { globals::state->frameAnnotations = globals::state->useFrameAnnotations; } } + Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::enableCapture); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Enable RenderDoc frame capture for providing debug captures to the Open Shaders team (or upstream Community Shaders for upstream-relevant issues)."); @@ -149,18 +155,17 @@ void RenderDoc::DrawSettings() } // The rest of the UI renders only when capture is active - bool renderDocCaptureEnabled = enableRenderDocCapture; + bool renderDocCaptureEnabled = settings.enableCapture; bool renderDocActive = IsAvailable(); const auto& themeSettings = Menu::GetSingleton()->GetTheme(); if (renderDocCaptureEnabled && !renderDocActive) { - ImGui::TextColored(themeSettings.StatusPalette.RestartNeeded, "Requires restart to enable RenderDoc capture."); return; } if (!renderDocCaptureEnabled && renderDocActive) { - ImGui::TextColored(themeSettings.StatusPalette.Warning, "Requires restart to disable RenderDoc capture, performance will be severely impacted."); + ImGui::TextColored(themeSettings.StatusPalette.Warning, "Performance will be severely impacted until the game is restarted."); return; } @@ -539,36 +544,34 @@ void RenderDoc::SetupResources() void RenderDoc::SaveSettings(json& o_json) { - o_json["Enable RenderDoc Capture"] = enableRenderDocCapture; + o_json["Enable RenderDoc Capture"] = settings.enableCapture; o_json["Capture Frame Count"] = GetCaptureFrameCount(); } void RenderDoc::LoadSettings(json& o_json) { if (o_json.contains("Enable RenderDoc Capture") && o_json["Enable RenderDoc Capture"].is_boolean()) { - enableRenderDocCapture = o_json["Enable RenderDoc Capture"]; - } - if (!o_json.contains("Capture Frame Count")) { - return; - } - - const auto& frameCountJson = o_json["Capture Frame Count"]; - if (frameCountJson.is_number_unsigned()) { - const auto frameCount = std::min(frameCountJson.get(), static_cast(kMaxCaptureFrameCount)); - SetCaptureFrameCount(static_cast(frameCount)); - } else if (frameCountJson.is_number_integer()) { - const auto frameCount = std::clamp( - frameCountJson.get(), - static_cast(kMinCaptureFrameCount), - static_cast(kMaxCaptureFrameCount)); - SetCaptureFrameCount(static_cast(frameCount)); + settings.enableCapture = o_json["Enable RenderDoc Capture"]; + } + if (o_json.contains("Capture Frame Count")) { + const auto& frameCountJson = o_json["Capture Frame Count"]; + if (frameCountJson.is_number_unsigned()) { + const auto frameCount = std::min(frameCountJson.get(), static_cast(kMaxCaptureFrameCount)); + SetCaptureFrameCount(static_cast(frameCount)); + } else if (frameCountJson.is_number_integer()) { + const auto frameCount = std::clamp( + frameCountJson.get(), + static_cast(kMinCaptureFrameCount), + static_cast(kMaxCaptureFrameCount)); + SetCaptureFrameCount(static_cast(frameCount)); + } } + bootSnapshot.LatchIfNeeded(settings); } void RenderDoc::RestoreDefaultSettings() { - enableRenderDocCapture = false; - SetCaptureFrameCount(1); + settings = {}; } void RenderDoc::ClearShaderCache() @@ -726,12 +729,12 @@ bool RenderDoc::HandleCaptureHotkey(uint32_t a_vkKey) uint32_t RenderDoc::GetCaptureFrameCount() const { - return std::clamp(captureFrameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount); + return std::clamp(settings.captureFrameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount); } void RenderDoc::SetCaptureFrameCount(uint32_t a_frameCount) { - captureFrameCount = std::clamp(a_frameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount); + settings.captureFrameCount = std::clamp(a_frameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount); } uint64_t RenderDoc::GetRequiredCaptureSpaceBytes() const @@ -780,7 +783,7 @@ bool RenderDoc::IsCapturing() const return false; // RenderDoc API doesn't have a direct IsCapturing method, but we can check if captures are enabled - return enableRenderDocCapture && renderDocApi != nullptr; + return settings.enableCapture && renderDocApi != nullptr; } std::string RenderDoc::GetCapturePath(uint32_t a_index) diff --git a/src/Features/RenderDoc.h b/src/Features/RenderDoc.h index ed3c133392..e5f8da05fb 100644 --- a/src/Features/RenderDoc.h +++ b/src/Features/RenderDoc.h @@ -1,6 +1,7 @@ #pragma once #include "Feature.h" +#include "Utils/BootSnapshot.h" #include #include #include @@ -118,9 +119,28 @@ class RenderDoc : public Feature std::string pendingCaptureComments; mutable std::mutex pendingCommentsMutex; - // RenderDoc capture enable setting - bool enableRenderDocCapture = false; - uint32_t captureFrameCount = 1; + struct Settings + { + bool enableCapture = false; + uint32_t captureFrameCount = 1; + }; + Settings settings; + + // `enableCapture` is restart-gated: the renderdoc.dll only gets injected + // at Load(); toggling the checkbox mid-session stages the change for next + // launch but doesn't install/uninstall the API. + inline static constexpr Util::Settings::RestartTable kRestartFields{ { + UTIL_RESTART_FIELD(Settings, enableCapture, "RenderDoc Capture"), + } }; + Util::Settings::BootSnapshot bootSnapshot{ kRestartFields }; + + std::span GetRestartRequiredFields() const override + { + return { kRestartFields.data(), kRestartFields.size() }; + } + const void* GetBootValue(std::string_view jsonKey) const override { return bootSnapshot.RawBoot(jsonKey); } + const void* GetSettingsBlob() const override { return &settings; } + size_t GetSettingsBlobSize() const override { return sizeof(settings); } // Track the last capture count we've processed for automatic comments uint32_t lastCaptureCount = 0; diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index bcb02a3b06..5569154abc 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -228,9 +228,9 @@ void Upscaling::DrawSettings() // path mode slot (upscaleMethod, not upscaleMethodNoDLSS), since // that's the one the boot snapshot locked. if (currentUpscaleMode == &settings.upscaleMethod && - settings.upscaleMethod != dlssPerf.GetBootUpscaleMethod()) { + bootSnapshot.HasPendingChange(settings, &Settings::upscaleMethod)) { const uint live = std::clamp(settings.upscaleMethod, 0u, availableModes); - const uint boot = std::clamp(dlssPerf.GetBootUpscaleMethod(), 0u, availableModes); + const uint boot = std::clamp(bootSnapshot.Boot(&Settings::upscaleMethod), 0u, availableModes); Util::Text::RestartNeeded( "Pending restart: currently active method = %s (selected = %s).", upscaleModes[boot].c_str(), upscaleModes[live].c_str()); @@ -280,9 +280,9 @@ void Upscaling::DrawSettings() // Pending-diff vs the boot snapshot the runtime upscaler is // actually using. Without this the slider change looks like a // no-op. - if (dlssPerf.HasBootSnapshot() && - settings.qualityMode != dlssPerf.GetBootQualityMode()) { - const uint bm = std::clamp(dlssPerf.GetBootQualityMode(), 0u, 4u); + if (dlssPerf.IsHookActive() && + bootSnapshot.HasPendingChange(settings, &Settings::qualityMode)) { + const uint bm = std::clamp(bootSnapshot.Boot(&Settings::qualityMode), 0u, 4u); const char* bootLabel = (upscaleMethod == UpscaleMethod::kDLSS) ? upscalePresetsDLSS[std::clamp(4 - (int)bm, 0, 4)] : upscalePresets[std::clamp(4 - (int)bm, 0, 4)]; Util::Text::RestartNeeded( "Pending restart: currently active = %s ( %.2fx ). Change applies after game restart.", @@ -301,7 +301,6 @@ void Upscaling::DrawSettings() ImGui::Text("Choose which DLSS AI model preset to use."); ImGui::Text("Each model offers different visual quality, performance, and motion stability."); ImGui::Text("Set to 'Default' for automatic selection based on your Upscale Preset and hardware."); - ImGui::Text("Changing this setting requires a restart to take effect."); } } @@ -336,9 +335,8 @@ void Upscaling::DrawSettings() } if (!dlssAvailable && settings.enableDLSSperf) Util::Text::Disabled("DLSSperf requires DLSS — switch upscaler Method to DLSS to activate."); - if (dlssAvailable && settings.enableDLSSperf != globals::features::upscaling.dlssPerf.IsHookActive()) - Util::Text::RestartNeeded("Pending restart: DLSSperf will %s on next launch.", - settings.enableDLSSperf ? "enable" : "disable"); + if (dlssAvailable) + Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::enableDLSSperf); } } @@ -351,37 +349,23 @@ void Upscaling::DrawSettings() if (HasFrameGenModule()) ImGui::Text("AMD FSR Frame Generation is available."); ImGui::Text("Requires a D3D11 to D3D12 proxy which can create compatibility issues"); - ImGui::Text("Toggling this setting requires a restart to work correctly"); - - bool onlyRequiresRestart = true; if (!isWindowed) { Util::Text::Warning("Warning: Requires windowed mode"); - - onlyRequiresRestart = false; } if (lowRefreshRate && !settings.frameGenerationForceEnable) { Util::Text::Warning("Warning: Requires a high refresh rate monitor or Force Enable Frame Generation"); - - onlyRequiresRestart = false; } if (fidelityFXMissing) { Util::Text::Warning("Warning: FidelityFX DLLs are not loaded"); - - onlyRequiresRestart = false; } - if (onlyRequiresRestart && settings.frameGenerationMode && !frameGenerationDx12PathActive) - Util::Text::Warning("Warning: Requires restart"); - - if (!settings.frameGenerationMode && frameGenerationDx12PathActive) - Util::Text::Warning("Warning: Requires restart"); - bool fgEnabled = settings.frameGenerationMode != 0; if (ImGui::Checkbox("Frame Generation", &fgEnabled)) settings.frameGenerationMode = fgEnabled ? 1 : 0; + Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::frameGenerationMode); if (!frameGenerationDx12PathActive) ImGui::BeginDisabled(); @@ -397,6 +381,7 @@ void Upscaling::DrawSettings() bool fgForce = settings.frameGenerationForceEnable != 0; if (ImGui::Checkbox("Force Enable Frame Generation", &fgForce)) settings.frameGenerationForceEnable = fgForce ? 1 : 0; + Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::frameGenerationForceEnable); ImGui::Checkbox("Frame Generation in Menus", &settings.frameGenerationAllowInMenus); if (auto _tt = Util::HoverTooltipWrapper()) { @@ -492,7 +477,7 @@ void Upscaling::DrawSettings() if (ImGui::Combo("Streamline Logging", &logLevelIdx, logLevels, IM_ARRAYSIZE(logLevels))) { settings.streamlineLogLevel = static_cast(logLevelIdx); } - ImGui::TextUnformatted("Changing this requires a restart to take effect."); + Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::streamlineLogLevel); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G."); } @@ -592,6 +577,10 @@ void Upscaling::LoadSettings(json& o_json) logger::warn("[Upscaling] Loaded upscaleMethodNoDLSS {} out of range, clamping to {}", settings.upscaleMethodNoDLSS, enumCount ? enumCount - 1 : 0); settings.upscaleMethodNoDLSS = enumCount ? enumCount - 1 : 0; } + if (settings.qualityMode > 4) { + logger::warn("[Upscaling] Loaded qualityMode {} out of range, clamping to 4", settings.qualityMode); + settings.qualityMode = 4; + } if (settings.presetDLSS > 4) { logger::warn("[Upscaling] Loaded presetDLSS {} out of range, resetting to 0 (Default)", settings.presetDLSS); settings.presetDLSS = 0; @@ -715,8 +704,8 @@ Upscaling::UpscaleMethod Upscaling::GetUpscaleMethod() const // Lock runtime to the boot upscaler under DLSSperf — engine RTs are // sized for it, and routing a different method through testTexture/ // renderRes paths breaks the HMD. - if (globals::features::upscaling.dlssPerf.HasBootSnapshot()) - return (UpscaleMethod)globals::features::upscaling.dlssPerf.GetBootUpscaleMethod(); + if (globals::features::upscaling.dlssPerf.IsHookActive()) + return static_cast(bootSnapshot.Boot(&Settings::upscaleMethod)); if (streamline.featureDLSS) return (UpscaleMethod)settings.upscaleMethod; return (UpscaleMethod)settings.upscaleMethodNoDLSS; @@ -1337,7 +1326,7 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) } else { // Boot qualityMode under DLSSperf so projection stays coherent // with the engine RTs sized at install. - const uint32_t qm = globals::features::upscaling.dlssPerf.HasBootSnapshot() ? globals::features::upscaling.dlssPerf.GetBootQualityMode() : settings.qualityMode; + const uint32_t qm = globals::features::upscaling.dlssPerf.IsHookActive() ? bootSnapshot.Boot(&Settings::qualityMode) : settings.qualityMode; float resolutionScaleBase = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)qm); auto renderWidth = static_cast(screenWidth * resolutionScaleBase); diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index cc9abab8d7..481d491b83 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -6,6 +6,7 @@ #include "Upscaling/FidelityFX.h" #include "Upscaling/RCAS/RCAS.h" #include "Upscaling/Streamline.h" +#include "Utils/BootSnapshot.h" #include #include #include @@ -80,6 +81,24 @@ struct Upscaling : Feature Settings settings; + // Single source of truth for restart-gated fields. Order is not load-bearing + // — the call-site `DrawSettingDiff` invocations in DrawSettings() handle any + // per-field conditional gating (e.g., qualityMode/upscaleMethod banners only + // render while DLSSperf's render-target hook is active). MCP discovery + // reports the full set; clients can check feature state themselves. + // presetDLSS is deliberately NOT here: Streamline::SetDLSSOptions reads + // settings.presetDLSS per-frame and applies it via slDLSSSetOptions, so + // it's already runtime-effective. + inline static constexpr Util::Settings::RestartTable kRestartFields{ { + UTIL_RESTART_FIELD(Settings, frameGenerationMode, "Frame Generation"), + UTIL_RESTART_FIELD(Settings, frameGenerationForceEnable, "Force Enable Frame Generation"), + UTIL_RESTART_FIELD(Settings, enableDLSSperf, "DLSSperf"), + UTIL_RESTART_FIELD(Settings, streamlineLogLevel, "Streamline Logging"), + UTIL_RESTART_FIELD(Settings, upscaleMethod, "Upscaling Method"), + UTIL_RESTART_FIELD(Settings, qualityMode, "Upscale Preset"), + } }; + Util::Settings::BootSnapshot bootSnapshot{ kRestartFields }; + struct JitterCB { float2 jitter; @@ -116,6 +135,14 @@ struct Upscaling : Feature bool IsUpscalingActive() const; // Feature interface overrides + std::span GetRestartRequiredFields() const override + { + return { kRestartFields.data(), kRestartFields.size() }; + } + const void* GetBootValue(std::string_view jsonKey) const override { return bootSnapshot.RawBoot(jsonKey); } + const void* GetSettingsBlob() const override { return &settings; } + size_t GetSettingsBlobSize() const override { return sizeof(settings); } + virtual void DrawSettings() override; virtual void SaveSettings(json& o_json) override; virtual void LoadSettings(json& o_json) override; diff --git a/src/Features/Upscaling/DLSSperf.cpp b/src/Features/Upscaling/DLSSperf.cpp index 3983da4175..713104ef79 100644 --- a/src/Features/Upscaling/DLSSperf.cpp +++ b/src/Features/Upscaling/DLSSperf.cpp @@ -139,10 +139,9 @@ void DLSSperf::InstallRenderTargetSizeHook() renderEyeWidth = std::max(1, (uint32_t)(w / scale)); renderEyeHeight = std::max(1, (uint32_t)(h / scale)); - // Boot snapshot — runtime upscaler paths read these; UI keeps editing - // live `settings` for JSON persistence. - bootUpscaleMethod = globals::features::upscaling.settings.upscaleMethod; - bootQualityMode = qualityMode; + // Restart-required settings snapshot is latched by the render-target + // creation hook, but keep this robust to call-order changes. + globals::features::upscaling.bootSnapshot.LatchIfNeeded(globals::features::upscaling.settings); stl::write_vfunc<0x12, GetRenderTargetSize_Hook>(RE::VTABLE_BSOpenVR[0]); diff --git a/src/Features/Upscaling/DLSSperf.h b/src/Features/Upscaling/DLSSperf.h index 22d3f74414..a5a331b2f1 100644 --- a/src/Features/Upscaling/DLSSperf.h +++ b/src/Features/Upscaling/DLSSperf.h @@ -57,13 +57,6 @@ struct DLSSperf uint32_t GetRenderEyeWidth() const { return renderEyeWidth; } uint32_t GetRenderEyeHeight() const { return renderEyeHeight; } - // Boot snapshots — engine RTs are sized once against these, so runtime - // upscaler reads must route through here instead of live `Upscaling:: - // settings` (mid-session UI changes would otherwise break the HMD). - bool HasBootSnapshot() const { return hookActive; } - uint32_t GetBootUpscaleMethod() const { return bootUpscaleMethod; } - uint32_t GetBootQualityMode() const { return bootQualityMode; } - // Phase 3: real HMD display resolution in SBS format (e.g. 3072×1632) // Used by Upscaling pipeline to override polluted screenSize (which equals RenderRes after hook) float2 GetDisplayScreenSize() const @@ -208,10 +201,6 @@ struct DLSSperf uint32_t renderEyeWidth = 0; uint32_t renderEyeHeight = 0; - // Boot snapshot — see HasBootSnapshot() accessor above. - uint32_t bootUpscaleMethod = 0; - uint32_t bootQualityMode = 0; - // Phase 2: vtable hook for BSOpenVR::GetRenderTargetSize (vfunc 0x12) struct GetRenderTargetSize_Hook { diff --git a/src/Features/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index 30d29262f2..c5c2a812a2 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -423,7 +423,7 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) // Boot qualityMode under DLSSperf — DLSS dispatch must match the // renderRes the engine was sized for at install. - uint32_t qualityMode = globals::features::upscaling.dlssPerf.HasBootSnapshot() ? globals::features::upscaling.dlssPerf.GetBootQualityMode() : globals::features::upscaling.settings.qualityMode; + uint32_t qualityMode = globals::features::upscaling.dlssPerf.IsHookActive() ? globals::features::upscaling.bootSnapshot.Boot(&Upscaling::Settings::qualityMode) : globals::features::upscaling.settings.qualityMode; switch (qualityMode) { case 1: dlssOptions.mode = sl::DLSSMode::eMaxQuality; diff --git a/src/Features/Upscaling/Streamline.h b/src/Features/Upscaling/Streamline.h index f173dd1bde..2f1f11e866 100644 --- a/src/Features/Upscaling/Streamline.h +++ b/src/Features/Upscaling/Streamline.h @@ -28,7 +28,6 @@ class Streamline inline std::string GetShortName() { return "Streamline"; } - bool enabledAtBoot = false; bool initialized = false; bool triedInitialization = false; diff --git a/src/Features/VR.cpp b/src/Features/VR.cpp index 5834a0ace6..1f011fc01b 100644 --- a/src/Features/VR.cpp +++ b/src/Features/VR.cpp @@ -136,6 +136,8 @@ void VR::SetupResources() void VR::PostPostLoad() { + stereoOpt.LatchBootSnapshot(); + gDepthBufferCulling = reinterpret_cast(REL::Offset(0x1EC6B88).address()); if (!gDepthBufferCulling) { static bool s_defaultDepthBufferCulling = false; diff --git a/src/Features/VRStereoOptimizations.cpp b/src/Features/VRStereoOptimizations.cpp index 63d5da8942..8f230c593b 100644 --- a/src/Features/VRStereoOptimizations.cpp +++ b/src/Features/VRStereoOptimizations.cpp @@ -259,11 +259,8 @@ void VRStereoOptimizations::DrawSettings() settings.stereoMode = static_cast(currentMode); Util::AddTooltip("Reprojects Eye 0 (left) pixels into Eye 1 (right) using depth and motion data,\nskipping redundant full shading where the views overlap.\nReduces GPU cost in VR by shading each pixel fewer times per frame."); - if (globals::game::isVR && settings.stereoMode == StereoMode::Enable && !loaded) { - const auto& themeSettings = Menu::GetSingleton()->GetTheme(); - ImGui::TextColored(themeSettings.StatusPalette.RestartNeeded, - "Restart is required to enable VR stereo reprojection."); - } + if (globals::game::isVR) + Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::stereoMode); if (settings.stereoMode == StereoMode::Off) return; diff --git a/src/Features/VRStereoOptimizations.h b/src/Features/VRStereoOptimizations.h index 4f324395ce..d3b564eef3 100644 --- a/src/Features/VRStereoOptimizations.h +++ b/src/Features/VRStereoOptimizations.h @@ -3,6 +3,7 @@ #include using json = nlohmann::json; +#include "Utils/BootSnapshot.h" #include #include #include @@ -95,6 +96,16 @@ struct VRStereoOptimizations } settings; + // stereoMode is restart-gated: the stencil/CS resources are only set up + // when `loaded` is true at boot, and toggling mid-session can't install + // them. Latched from VR::PostPostLoad. + inline static constexpr Util::Settings::RestartTable kRestartFields{ { + UTIL_RESTART_FIELD(Settings, stereoMode, "VR Stereo Reprojection"), + } }; + Util::Settings::BootSnapshot bootSnapshot{ kRestartFields }; + + void LatchBootSnapshot() { bootSnapshot.LatchIfNeeded(settings); } + //============================================================================= // GPU CONSTANT BUFFER (must match HLSL cbuffer layout exactly) //============================================================================= diff --git a/src/Features/VolumetricLighting.cpp b/src/Features/VolumetricLighting.cpp index d52d725ee3..c7f9739194 100644 --- a/src/Features/VolumetricLighting.cpp +++ b/src/Features/VolumetricLighting.cpp @@ -3,6 +3,7 @@ #include "InteriorSun.h" #include "ShaderCache.h" #include "State.h" +#include "Utils/UI.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( VolumetricLighting::TextureSize, @@ -23,12 +24,16 @@ void VolumetricLighting::DrawSettings() { if (ImGui::Checkbox("Enable Volumetric Lighting in Exteriors", &settings.ExteriorEnabled)) SetupVL(); + if (globals::game::isVR) + Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::ExteriorEnabled); if (settings.ExteriorEnabled) DrawVolumetricLightingSettings(settings.ExteriorQuality, settings.ExteriorCustomSize, false, !inInterior); if (ImGui::Checkbox("Enable Volumetric Lighting in Interiors", &settings.InteriorEnabled)) SetupVL(); + if (globals::game::isVR) + Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::InteriorEnabled); if (settings.InteriorEnabled) DrawVolumetricLightingSettings(settings.InteriorQuality, settings.InteriorCustomSize, true, inInterior); @@ -147,7 +152,7 @@ void VolumetricLighting::DataLoaded() const static auto address = REL::Offset{ 0x1ec6b88 }.address(); bool& bDepthBufferCulling = *reinterpret_cast(address); - if (REL::Module::IsVR() && bDepthBufferCulling && shaderCache->IsDiskCache()) { + if (globals::game::isVR && bDepthBufferCulling && shaderCache->IsDiskCache()) { // clear cache to fix bug caused by bDepthBufferCulling logger::info("Force clearing cache due to bDepthBufferCulling"); shaderCache->Clear(); @@ -156,7 +161,8 @@ void VolumetricLighting::DataLoaded() void VolumetricLighting::PostPostLoad() { - if (REL::Module::IsVR()) { + bootSnapshot.LatchIfNeeded(settings); + if (globals::game::isVR) { if (settings.ExteriorEnabled || settings.InteriorEnabled) EnableBooleanSettings(hiddenVRSettings, GetName()); auto address = REL::RelocationID(100475, 0).address() + 0x45b; // AE not needed, VR only hook @@ -337,4 +343,4 @@ void VolumetricLighting::CopyResource::thunk(ID3D11DeviceContext* a_this, ID3D11 if (!(Util::IsDynamicResolution() && singleton.bEnableVolumetricLighting)) { a_this->CopyResource(a_renderTarget, a_renderTargetSource); } -} \ No newline at end of file +} diff --git a/src/Features/VolumetricLighting.h b/src/Features/VolumetricLighting.h index e5251c52ef..4c9c7bbc15 100644 --- a/src/Features/VolumetricLighting.h +++ b/src/Features/VolumetricLighting.h @@ -1,5 +1,7 @@ #pragma once +#include "Utils/BootSnapshot.h" + struct VolumetricLighting : Feature { public: @@ -22,7 +24,20 @@ struct VolumetricLighting : Feature Settings settings; - bool enabledAtBoot = false; + inline static constexpr Util::Settings::RestartTable kRestartFields{ { + UTIL_RESTART_FIELD(Settings, ExteriorEnabled, "Volumetric Lighting (Exterior)"), + UTIL_RESTART_FIELD(Settings, InteriorEnabled, "Volumetric Lighting (Interior)"), + } }; + Util::Settings::BootSnapshot bootSnapshot{ kRestartFields }; + + std::span GetRestartRequiredFields() const override + { + // VR-only: enabling VL relies on startup-only game setting initialization. + return globals::game::isVR ? std::span{ kRestartFields.data(), kRestartFields.size() } : std::span{}; + } + const void* GetBootValue(std::string_view jsonKey) const override { return bootSnapshot.RawBoot(jsonKey); } + const void* GetSettingsBlob() const override { return &settings; } + size_t GetSettingsBlobSize() const override { return sizeof(settings); } virtual inline std::string GetName() override { return "Volumetric Lighting"; } virtual inline std::string GetShortName() override { return "VolumetricLighting"; } diff --git a/src/Features/WeatherEditor.cpp b/src/Features/WeatherEditor.cpp index 8f6ff39e43..2272b870a6 100644 --- a/src/Features/WeatherEditor.cpp +++ b/src/Features/WeatherEditor.cpp @@ -559,7 +559,6 @@ void WeatherEditor::DisplayWindInfo(RE::TESWeather* weather) auto sky = globals::game::sky; if (!weather || (weather->data.windSpeed <= 0 && (!sky || sky->windSpeed <= 0.0f))) return; - const auto& theme = Menu::GetSingleton()->GetTheme(); float windSpeedDisplay = weather->data.windSpeed / 255.0f; ImGui::BulletText("Weather Wind Speed: %.2f (raw %d)", windSpeedDisplay, weather->data.windSpeed); if (auto _tt = Util::HoverTooltipWrapper()) { @@ -603,7 +602,7 @@ void WeatherEditor::DisplayWindInfo(RE::TESWeather* weather) windRelation = "Left crosswind"; } ImGui::SameLine(); - ImGui::TextColored(theme.StatusPalette.RestartNeeded, "(%s)", windRelation); + Util::Text::RestartNeeded("(%s)", windRelation); if (auto _tt = Util::HoverTooltipWrapper()) { Util::DrawMultiLineTooltip({ "Wind relative to player direction:", diff --git a/src/Hooks.cpp b/src/Hooks.cpp index c71636c886..5c9d4abb17 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -370,6 +370,11 @@ struct BSShaderRenderTargets_Create { Util::SetGameSettingValue("iNumFocusShadow:Display", iNumFocusShadow, 0); + // Restart-required settings snapshot. Latch once as soon as engine + // rendering state begins initializing (pre-RT allocation) so UI/MCP + // can diff "active at boot" vs "selected". + globals::features::upscaling.bootSnapshot.LatchIfNeeded(globals::features::upscaling.settings); + // DLSSperf: install the BSOpenVR render-target-size hook before the // engine creates its render targets. This is the only place where // BSOpenVR is guaranteed available AND we can still influence RT diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 978984eb7d..c8f66ffef0 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -509,7 +509,11 @@ void FeatureListRenderer::ListMenuVisitor::operator()(Feature* feat) if (isDisabled) { textColor = themeSettings.StatusPalette.Disable; } else if (isLoaded) { - textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); + // Loaded feature with staged but-not-yet-applied restart-gated + // settings tints the same green as a feature pending re-enable. + // Same semantic from the user's POV: "this feature has unmade + // changes that take effect on restart." + textColor = feat->HasAnyPendingRestart() ? themeSettings.StatusPalette.RestartNeeded : ImGui::GetStyleColorVec4(ImGuiCol_Text); } else if (hasFailedMessage) { textColor = feat->version.empty() ? themeSettings.StatusPalette.Disable : themeSettings.StatusPalette.Error; } else { diff --git a/src/Utils/BootSnapshot.h b/src/Utils/BootSnapshot.h new file mode 100644 index 0000000000..7efad8d870 --- /dev/null +++ b/src/Utils/BootSnapshot.h @@ -0,0 +1,129 @@ +#pragma once + +#include "Utils/RestartSettings.h" + +#include +#include +#include +#include +#include + +namespace Util::Settings +{ + namespace detail + { + template + size_t MemberOffset(T SettingsT::* member) noexcept + { + static_assert(std::is_default_constructible_v); + SettingsT tmp{}; + const auto* base = reinterpret_cast(&tmp); + const auto* field = reinterpret_cast(&(tmp.*member)); + return static_cast(field - base); + } + } + + template + class BootSnapshot + { + public: + template + explicit constexpr BootSnapshot(const RestartTable& table) noexcept : + table_(table.data()), tableSize_(N) + { + static_assert(std::is_standard_layout_v, "BootSnapshot requires standard-layout Settings for offsetof-based tables."); + static_assert(std::is_trivially_copyable_v, "BootSnapshot requires trivially-copyable Settings."); + } + + void Latch(const SettingsT& live) noexcept + { + // Byte-wise copy so padding bytes are reproduced verbatim. Assignment + // of a trivially-copyable struct copies the object representation + // (which the C++ standard guarantees for trivially-copyable types), + // but memcpy makes that intent explicit and removes any compiler + // latitude that might leave padding indeterminate — `HasPendingChange` + // uses memcmp on field slices, so any padding-byte drift would + // surface as a false-positive diff. + std::memcpy(&bootCopy_, &live, sizeof(SettingsT)); + latched_ = true; + } + + void LatchIfNeeded(const SettingsT& live) noexcept + { + if (!latched_) { + Latch(live); + } + } + + bool IsLatched() const noexcept { return latched_; } + + std::span Fields() const noexcept + { + return { table_, tableSize_ }; + } + + const void* RawBoot(std::string_view jsonKey) const noexcept + { + if (!latched_) { + return nullptr; + } + const auto* field = FindRestartField(Fields(), jsonKey); + if (!field) { + return nullptr; + } + return reinterpret_cast(&bootCopy_) + field->offset; + } + + template + const T& Boot(T SettingsT::* member) const noexcept + { + static const T kZero{}; + if (!latched_) { + return kZero; + } + const size_t offset = detail::MemberOffset(member); + return *reinterpret_cast(reinterpret_cast(&bootCopy_) + offset); + } + + template + bool HasPendingChange(const SettingsT& live, T SettingsT::* member) const noexcept + { + if (!latched_) { + return false; + } + const size_t offset = detail::MemberOffset(member); + return std::memcmp(reinterpret_cast(&bootCopy_) + offset, + reinterpret_cast(&live) + offset, + sizeof(T)) != 0; + } + + bool HasPendingChange(const SettingsT& live, const RestartFieldInfo& field) const noexcept + { + if (!latched_ || !field.jsonKey) { + return false; + } + return std::memcmp(reinterpret_cast(&bootCopy_) + field.offset, + reinterpret_cast(&live) + field.offset, + field.size) != 0; + } + + template + const RestartFieldInfo* FindField(T SettingsT::* member) const noexcept + { + const size_t offset = detail::MemberOffset(member); + const size_t size = sizeof(T); + for (const auto& field : Fields()) { + if (field.offset == offset && field.size == size) { + return &field; + } + } + return nullptr; + } + + private: + SettingsT bootCopy_{}; + const RestartFieldInfo* table_ = nullptr; + size_t tableSize_ = 0; + bool latched_ = false; + }; +} diff --git a/src/Utils/RestartSettings.h b/src/Utils/RestartSettings.h new file mode 100644 index 0000000000..1a5f217b82 --- /dev/null +++ b/src/Utils/RestartSettings.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +namespace Util::Settings +{ + // Type-erased field descriptor for restart-gated settings. + // + // `jsonKey` must match the NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE field name so + // MCP/RemoteControl can refer to it without per-feature glue. + struct RestartFieldInfo + { + const char* jsonKey = nullptr; + const char* label = nullptr; + size_t offset = 0; + size_t size = 0; + }; + + template + using RestartTable = std::array; + + inline constexpr const RestartFieldInfo* FindRestartField(std::span fields, std::string_view jsonKey) noexcept + { + for (const auto& field : fields) { + if (field.jsonKey && jsonKey == field.jsonKey) { + return &field; + } + } + return nullptr; + } +} + +// Convenience macro for building a RestartFieldInfo entry without duplicating +// the member name string. Requires SettingsT to be standard-layout. +#define UTIL_RESTART_FIELD(SettingsT, member, userLabel) \ + Util::Settings::RestartFieldInfo{ #member, userLabel, offsetof(SettingsT, member), sizeof(decltype(SettingsT::member)) } + diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 177a8970c3..ccd1e95004 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -5,11 +5,14 @@ #include #include #include +#include +#include #include #include // For WPARAM and virtual key constants #include "../FeatureConstraints.h" #include "../Menu/Fonts.h" +#include "Utils/BootSnapshot.h" #include "Utils/Input.h" // Forward declarations @@ -957,6 +960,72 @@ namespace Util void WrappedRestartNeeded(const char* fmt, ...) IM_FMTARGS(1); } + // Restart-required settings UI helpers. + namespace UI + { + template + inline void DrawSettingDiff(const Util::Settings::BootSnapshot& snapshot, const SettingsT& live, T SettingsT::*field) + { + if (!snapshot.IsLatched()) { + return; + } + const auto* info = snapshot.FindField(field); + if (!info) { + return; + } + if (!snapshot.HasPendingChange(live, field)) { + return; + } + + if constexpr (std::is_same_v) { + const bool boot = snapshot.Boot(field); + Util::Text::RestartNeeded( + "Pending restart: %s changed (active = %s, selected = %s).", + info->label, + boot ? "on" : "off", + (live.*field) ? "on" : "off"); + return; + } + if constexpr (std::is_integral_v) { + const auto boot = static_cast(snapshot.Boot(field)); + const auto selected = static_cast(live.*field); + Util::Text::RestartNeeded( + "Pending restart: %s changed (active = %lld, selected = %lld).", + info->label, + boot, + selected); + return; + } + if constexpr (std::is_floating_point_v) { + const double boot = static_cast(snapshot.Boot(field)); + const double selected = static_cast(live.*field); + Util::Text::RestartNeeded( + "Pending restart: %s changed (active = %.3f, selected = %.3f).", + info->label, + boot, + selected); + return; + } + + Util::Text::RestartNeeded("Pending restart: %s changed.", info->label); + } + + template + inline void DrawPendingBanners(const Util::Settings::BootSnapshot& snapshot, + const SettingsT& live, + std::span fields) + { + if (!snapshot.IsLatched()) { + return; + } + for (const auto& field : fields) { + if (snapshot.HasPendingChange(live, field)) { + Util::Text::RestartNeeded("Pending restart: %s changed.", field.label); + } + } + } + } + /** * @brief Input handling utilities for ImGui integration * diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 7f84372e48..9eb12a0fc5 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -260,7 +260,8 @@ void EditorWindow::ShowObjectsWindow() }; // Build active records for the current category tab - struct ActiveRecord { + struct ActiveRecord + { std::string label; std::string suffix; RE::FormID formId; @@ -285,15 +286,17 @@ void EditorWindow::ShowObjectsWindow() }; auto addSingle = [&](RE::TESForm* form, const WidgetVec& widgets, std::string suffix = "") { - if (!form) return; + if (!form) + return; auto id = form->GetFormID(); activeRecords.push_back({ ResolveEditorId(form, widgets), std::move(suffix), id, openByFormId(id, &widgets) }); }; - auto addTOD = [&](auto* (&fields)[RE::TESWeather::ColorTimes::kTotal], const WidgetVec& widgets) { + auto addTOD = [&](auto*(&fields)[RE::TESWeather::ColorTimes::kTotal], const WidgetVec& widgets) { for (int tod = 0; tod < RE::TESWeather::ColorTimes::kTotal; ++tod) { auto* form = fields[tod]; - if (!form) continue; + if (!form) + continue; auto id = form->GetFormID(); bool already = std::any_of(activeRecords.begin(), activeRecords.end(), [&](const ActiveRecord& r) { return r.formId == id; }); @@ -303,7 +306,8 @@ void EditorWindow::ShowObjectsWindow() }; auto addWeather = [&](RE::TESWeather* weatherRecord, std::string suffix = "") { - if (!weatherRecord) return; + if (!weatherRecord) + return; auto id = weatherRecord->GetFormID(); activeRecords.push_back({ ResolveEditorId(weatherRecord, weatherWidgets), std::move(suffix), id, openByFormId(id, &weatherWidgets) }); }; @@ -313,7 +317,8 @@ void EditorWindow::ShowObjectsWindow() if (sky && sky->lastWeather != weather) addWeather(sky->lastWeather, "transitioning"); } else if (m_selectedCategory == "ImageSpace") { - if (weather) addTOD(weather->imageSpaces, imageSpaceWidgets); + if (weather) + addTOD(weather->imageSpaces, imageSpaceWidgets); } else if (m_selectedCategory == "Lighting Template") { auto* player = RE::PlayerCharacter::GetSingleton(); if (player && player->parentCell) @@ -339,13 +344,17 @@ void EditorWindow::ShowObjectsWindow() } }); } } else if (m_selectedCategory == "Volumetric Lighting") { - if (weather) addTOD(weather->volumetricLighting, volumetricLightingWidgets); + if (weather) + addTOD(weather->volumetricLighting, volumetricLightingWidgets); } else if (m_selectedCategory == "Shader Particle Geometry") { - if (weather) addSingle(weather->precipitationData, precipitationWidgets); + if (weather) + addSingle(weather->precipitationData, precipitationWidgets); } else if (m_selectedCategory == "Lens Flare") { - if (weather) addSingle(weather->sunGlareLensFlare, lensFlareWidgets); + if (weather) + addSingle(weather->sunGlareLensFlare, lensFlareWidgets); } else if (m_selectedCategory == "Visual Effect") { - if (weather) addSingle(weather->referenceEffect, referenceEffectWidgets); + if (weather) + addSingle(weather->referenceEffect, referenceEffectWidgets); } // Fall back to current weather when the active category has no active record @@ -359,9 +368,7 @@ void EditorWindow::ShowObjectsWindow() if (!activeRecords.empty()) { const auto& theme = Menu::GetSingleton()->GetTheme(); - ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.RestartNeeded); - ImGui::Text("Active:"); - ImGui::PopStyleColor(); + Util::Text::RestartNeeded("Active:"); ImGui::SameLine(); const float recordX = ImGui::GetCursorPosX(); @@ -1859,8 +1866,8 @@ void EditorWindow::DrawTimeControls() const float framePadX = ImGui::GetStyle().FramePadding.x * 2.0f; const float buttonWidth = std::max({ ImGui::CalcTextSize("Resume Time").x, - ImGui::CalcTextSize("Pause Time").x, - ImGui::CalcTextSize("Reset Speed").x }) + + ImGui::CalcTextSize("Pause Time").x, + ImGui::CalcTextSize("Reset Speed").x }) + framePadX; if (ImGui::Button(timePaused ? "Resume Time" : "Pause Time", ImVec2(buttonWidth, 0))) TogglePause(); diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index 9ec4f08996..769dcc84d0 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -38,6 +38,7 @@ find_package(imgui CONFIG REQUIRED) add_executable(cpp_tests test_main.cpp + test_bootsnapshot.cpp test_subrect.cpp # Compile the unit-under-test directly into the test binary so we don't # depend on the plugin DLL build (which pulls in FFX/Streamline/etc.). diff --git a/tests/cpp/test_bootsnapshot.cpp b/tests/cpp/test_bootsnapshot.cpp new file mode 100644 index 0000000000..7c33ea13af --- /dev/null +++ b/tests/cpp/test_bootsnapshot.cpp @@ -0,0 +1,63 @@ +// Unit tests for Util::Settings::BootSnapshot (restart-required settings diff). + +#include "Utils/BootSnapshot.h" + +#include + +#include + +namespace +{ + struct TestSettings + { + uint32_t mode = 0; + bool enabled = false; + float value = 0.0f; + }; + + inline constexpr Util::Settings::RestartTable kFields{ { + UTIL_RESTART_FIELD(TestSettings, mode, "Mode"), + UTIL_RESTART_FIELD(TestSettings, enabled, "Enabled"), + } }; +} + +TEST_CASE("BootSnapshot starts unlatched and ignores diffs", "[bootsnapshot]") +{ + Util::Settings::BootSnapshot snap{ kFields }; + TestSettings live{}; + live.mode = 3; + live.enabled = true; + + REQUIRE_FALSE(snap.IsLatched()); + REQUIRE(snap.RawBoot("mode") == nullptr); + REQUIRE_FALSE(snap.HasPendingChange(live, &TestSettings::mode)); +} + +TEST_CASE("BootSnapshot detects member changes after latch", "[bootsnapshot]") +{ + Util::Settings::BootSnapshot snap{ kFields }; + TestSettings boot{}; + boot.mode = 1; + boot.enabled = false; + + snap.Latch(boot); + REQUIRE(snap.IsLatched()); + REQUIRE(snap.Boot(&TestSettings::mode) == 1); + REQUIRE(snap.Boot(&TestSettings::enabled) == false); + + TestSettings live = boot; + REQUIRE_FALSE(snap.HasPendingChange(live, &TestSettings::mode)); + + live.mode = 2; + REQUIRE(snap.HasPendingChange(live, &TestSettings::mode)); + REQUIRE_FALSE(snap.HasPendingChange(live, &TestSettings::enabled)); +} + +TEST_CASE("BootSnapshot exposes field metadata by member", "[bootsnapshot]") +{ + Util::Settings::BootSnapshot snap{ kFields }; + const auto* info = snap.FindField(&TestSettings::enabled); + REQUIRE(info != nullptr); + REQUIRE(std::string(info->jsonKey) == "enabled"); + REQUIRE(std::string(info->label) == "Enabled"); +} From 0f307d253079479fb0b57745595e4c0e6b293943 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 25 May 2026 10:45:17 -0700 Subject: [PATCH 18/24] refactor(BootSnapshot): allow non-trivially-copyable Settings (#40) Co-authored-by: Claude Opus 4.7 (1M context) --- src/Utils/BootSnapshot.h | 32 +++++++++++------ src/Utils/UI.h | 57 +++++++++++++++++++++++++++-- tests/cpp/test_bootsnapshot.cpp | 64 +++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 13 deletions(-) diff --git a/src/Utils/BootSnapshot.h b/src/Utils/BootSnapshot.h index 7efad8d870..86ae038c63 100644 --- a/src/Utils/BootSnapshot.h +++ b/src/Utils/BootSnapshot.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace Util::Settings { @@ -32,23 +33,32 @@ namespace Util::Settings table_(table.data()), tableSize_(N) { static_assert(std::is_standard_layout_v, "BootSnapshot requires standard-layout Settings for offsetof-based tables."); - static_assert(std::is_trivially_copyable_v, "BootSnapshot requires trivially-copyable Settings."); + static_assert(std::is_copy_assignable_v, "BootSnapshot requires copy-assignable Settings."); + static_assert(std::is_default_constructible_v, + "BootSnapshot requires default-constructible Settings (bootCopy_ default-inits and detail::MemberOffset constructs a temporary)."); } - void Latch(const SettingsT& live) noexcept + void Latch(const SettingsT& live) noexcept(std::is_nothrow_copy_assignable_v) { - // Byte-wise copy so padding bytes are reproduced verbatim. Assignment - // of a trivially-copyable struct copies the object representation - // (which the C++ standard guarantees for trivially-copyable types), - // but memcpy makes that intent explicit and removes any compiler - // latitude that might leave padding indeterminate — `HasPendingChange` - // uses memcmp on field slices, so any padding-byte drift would - // surface as a false-positive diff. - std::memcpy(&bootCopy_, &live, sizeof(SettingsT)); + if constexpr (std::is_trivially_copyable_v) { + // Trivially-copyable fast path: memcpy preserves padding bytes + // verbatim. Matters when a registered restart field's *type* + // contains padding -- HasPendingChange's memcmp would otherwise + // see false-positive diffs from uninitialized padding bytes. + std::memcpy(&bootCopy_, &live, sizeof(SettingsT)); + } else { + // Copy-assign for Settings with non-trivial members (e.g. the + // std::string formula fields in ShadowCasterManager::Settings). + // Registered restart fields must still be trivially comparable + // for HasPendingChange's per-field memcmp to be meaningful; + // std::string in the outer struct is fine as long as it isn't + // registered (it isn't -- formulas are runtime-tunable). + bootCopy_ = live; + } latched_ = true; } - void LatchIfNeeded(const SettingsT& live) noexcept + void LatchIfNeeded(const SettingsT& live) noexcept(noexcept(std::declval().Latch(live))) { if (!latched_) { Latch(live); diff --git a/src/Utils/UI.h b/src/Utils/UI.h index ccd1e95004..c48adeefba 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -1,11 +1,12 @@ #pragma once #include #include // For FLT_MAX +#include #include #include #include -#include #include +#include #include #include #include // For WPARAM and virtual key constants @@ -964,7 +965,7 @@ namespace Util namespace UI { template - inline void DrawSettingDiff(const Util::Settings::BootSnapshot& snapshot, const SettingsT& live, T SettingsT::*field) + inline void DrawSettingDiff(const Util::Settings::BootSnapshot& snapshot, const SettingsT& live, T SettingsT::* field) { if (!snapshot.IsLatched()) { return; @@ -1024,6 +1025,58 @@ namespace Util } } } + + // One-call wrapper for a restart-gated ImGui control. Call IMMEDIATELY + // AFTER the control (Checkbox / SliderInt / Combo / etc.) so the + // HoverTooltipWrapper attaches to that control. Does two things: + // 1. If hovered, renders a tooltip via HoverTooltipWrapper (the + // codebase's dominant tooltip pattern, used 296+ times -- gives + // a consistent Subtext font role and viewport-clamped placement) + // with the standard "Requires a game restart to change." suffix + // appended after the caller-supplied body. + // 2. Calls DrawSettingDiff outside the hover scope to render the + // "Pending restart" banner when the live value diverges from + // the boot snapshot. + // + // Two overloads: + // - `const char* body` for the simple single-string case. + // - Callable `body` for multi-line tooltips that already use + // HoverTooltipWrapper-style content (multiple ImGui::Text / + // TextWrapped calls). The callable runs inside the + // HoverTooltipWrapper RAII scope so callers can use any ImGui + // text primitive. + // + // Pass nullptr / empty body to render just the suffix. + template + inline void RestartGatedAnnotate(const Util::Settings::BootSnapshot& snapshot, + const SettingsT& live, + T SettingsT::* field, + const char* tooltipBody = nullptr) + { + if (auto _tt = Util::HoverTooltipWrapper()) { + if (tooltipBody && tooltipBody[0]) { + ImGui::TextUnformatted(tooltipBody); + ImGui::Spacing(); + } + ImGui::TextUnformatted("Requires a game restart to change."); + } + DrawSettingDiff(snapshot, live, field); + } + + template + requires std::invocable + inline void RestartGatedAnnotate(const Util::Settings::BootSnapshot& snapshot, + const SettingsT& live, + T SettingsT::* field, + Body&& body) + { + if (auto _tt = Util::HoverTooltipWrapper()) { + body(); + ImGui::Spacing(); + ImGui::TextUnformatted("Requires a game restart to change."); + } + DrawSettingDiff(snapshot, live, field); + } } /** diff --git a/tests/cpp/test_bootsnapshot.cpp b/tests/cpp/test_bootsnapshot.cpp index 7c33ea13af..ea5f05228b 100644 --- a/tests/cpp/test_bootsnapshot.cpp +++ b/tests/cpp/test_bootsnapshot.cpp @@ -5,6 +5,7 @@ #include #include +#include namespace { @@ -61,3 +62,66 @@ TEST_CASE("BootSnapshot exposes field metadata by member", "[bootsnapshot]") REQUIRE(std::string(info->jsonKey) == "enabled"); REQUIRE(std::string(info->label) == "Enabled"); } + +namespace +{ + // Settings with a non-trivial member (std::string) -- not trivially- + // copyable but still copy-assignable. Mirrors real cases like + // ShadowCasterManager::Settings which carries exprtk formula strings + // alongside the POD restart-gated fields. + struct SettingsWithString + { + int32_t shadowLightCount = 0; + bool enabled = false; + std::string formula = "default"; // not registered as restart-gated + }; + + inline constexpr Util::Settings::RestartTable kStringFields{ { + UTIL_RESTART_FIELD(SettingsWithString, shadowLightCount, "Shadow Light Count"), + UTIL_RESTART_FIELD(SettingsWithString, enabled, "Enabled"), + } }; +} + +TEST_CASE("BootSnapshot deep-copies non-trivial members on Latch", "[bootsnapshot]") +{ + // Regression: the original BootSnapshot static_asserted trivially-copyable + // and Latch() used memcpy, which would shallow-copy std::string internals + // (corrupting the boot snapshot's string when the live string later + // reallocated). After the relaxation, Latch uses copy-assign so the + // non-trivial member is deep-copied. The POD restart-gated fields still + // drive HasPendingChange via memcmp. + Util::Settings::BootSnapshot snap{ kStringFields }; + SettingsWithString boot{}; + boot.shadowLightCount = 16; + boot.enabled = true; + const std::string originalFormula = "lightradius * lightintensity"; + boot.formula = originalFormula; + + snap.Latch(boot); + REQUIRE(snap.IsLatched()); + REQUIRE(snap.Boot(&SettingsWithString::shadowLightCount) == 16); + REQUIRE(snap.Boot(&SettingsWithString::enabled) == true); + // Read the snapshot's std::string directly. Catches shallow-copy + // regressions: if Latch were still doing a memcpy of SettingsWithString, + // the boot copy would hold a stale pointer into live.formula's heap + // buffer, and reading it (especially after live.formula reallocates) + // would crash or return garbage. The POD-only assertions don't cover + // this on their own. + REQUIRE(snap.Boot(&SettingsWithString::formula) == originalFormula); + + // Mutating the live struct (including reallocating its string) must NOT + // disturb the boot copy or produce false-positive diffs for unregistered + // fields. Force a string reallocation by growing it well past SSO size, + // then verify the boot copy's string is unchanged. + SettingsWithString live = boot; + live.formula = std::string(256, 'x'); + REQUIRE_FALSE(snap.HasPendingChange(live, &SettingsWithString::shadowLightCount)); + REQUIRE_FALSE(snap.HasPendingChange(live, &SettingsWithString::enabled)); + REQUIRE(snap.Boot(&SettingsWithString::formula) == originalFormula); + + // Now flip a registered POD field; the diff fires. + live.shadowLightCount = 32; + REQUIRE(snap.HasPendingChange(live, &SettingsWithString::shadowLightCount)); + REQUIRE_FALSE(snap.HasPendingChange(live, &SettingsWithString::enabled)); + REQUIRE(snap.Boot(&SettingsWithString::formula) == originalFormula); +} From c97626cd424a6827a4b79153deff472f5f4fef00 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 25 May 2026 19:55:02 -0700 Subject: [PATCH 19/24] feat(upscaling): generalize VR perf-mode rendering (#42) Co-authored-by: Claude --- .../BoxDownscalePS.hlsl | 2 +- .../{DLSSperf => PerfMode}/MenuBGBlitPS.hlsl | 2 +- src/Features/Upscaling.cpp | 138 ++++++---- src/Features/Upscaling.h | 16 +- src/Features/Upscaling/FidelityFX.cpp | 26 +- src/Features/Upscaling/FidelityFX.h | 6 +- .../Upscaling/{DLSSperf.cpp => PerfMode.cpp} | 240 +++++++++--------- .../Upscaling/{DLSSperf.h => PerfMode.h} | 14 +- src/Features/Upscaling/Streamline.cpp | 30 +-- src/Globals.cpp | 8 +- src/Hooks.cpp | 37 +-- 11 files changed, 284 insertions(+), 235 deletions(-) rename features/Upscaling/Shaders/Upscaling/{DLSSperf => PerfMode}/BoxDownscalePS.hlsl (95%) rename features/Upscaling/Shaders/Upscaling/{DLSSperf => PerfMode}/MenuBGBlitPS.hlsl (93%) rename src/Features/Upscaling/{DLSSperf.cpp => PerfMode.cpp} (86%) rename src/Features/Upscaling/{DLSSperf.h => PerfMode.h} (97%) diff --git a/features/Upscaling/Shaders/Upscaling/DLSSperf/BoxDownscalePS.hlsl b/features/Upscaling/Shaders/Upscaling/PerfMode/BoxDownscalePS.hlsl similarity index 95% rename from features/Upscaling/Shaders/Upscaling/DLSSperf/BoxDownscalePS.hlsl rename to features/Upscaling/Shaders/Upscaling/PerfMode/BoxDownscalePS.hlsl index 755a5778f6..a1370e417b 100644 --- a/features/Upscaling/Shaders/Upscaling/DLSSperf/BoxDownscalePS.hlsl +++ b/features/Upscaling/Shaders/Upscaling/PerfMode/BoxDownscalePS.hlsl @@ -1,4 +1,4 @@ -// BoxDownscalePS.hlsl — DLSSperf downscale pass +// BoxDownscalePS.hlsl — PerfMode downscale pass // Box 3×3 filter: testTexture (3k) → kMAIN (1k). // For 3:1 downscale, each output pixel averages the 3×3 source region, // ensuring all DLSS output pixels contribute (vs bilinear's 2×2 coverage). diff --git a/features/Upscaling/Shaders/Upscaling/DLSSperf/MenuBGBlitPS.hlsl b/features/Upscaling/Shaders/Upscaling/PerfMode/MenuBGBlitPS.hlsl similarity index 93% rename from features/Upscaling/Shaders/Upscaling/DLSSperf/MenuBGBlitPS.hlsl rename to features/Upscaling/Shaders/Upscaling/PerfMode/MenuBGBlitPS.hlsl index 2e946df518..9b20ae742b 100644 --- a/features/Upscaling/Shaders/Upscaling/DLSSperf/MenuBGBlitPS.hlsl +++ b/features/Upscaling/Shaders/Upscaling/PerfMode/MenuBGBlitPS.hlsl @@ -1,4 +1,4 @@ -// MenuBGBlitPS.hlsl — DLSSperf main-menu / loading-screen BG blit. +// MenuBGBlitPS.hlsl — PerfMode main-menu / loading-screen BG blit. // Fullscreen 1:1 sample of the source texture into kTOTAL/kMENUBG. The // caller (MaybeBlitMenuBG) feeds DLSS-reconstructed testTexture (R16G16 // B16A16_FLOAT, displayRes) and the destination kTOTAL is R8G8B8A8_UNORM diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 5569154abc..32c0cd170c 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -4,7 +4,7 @@ #include "HDRDisplay.h" #include "Hooks.h" #include "State.h" -#include "Upscaling/DLSSperf.h" +#include "Upscaling/PerfMode.h" #include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" #include "Upscaling/Streamline.h" @@ -34,7 +34,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( reflexUseMarkersToOptimize, reflexUseFPSLimit, reflexFPSLimit, - enableDLSSperf); + renderAtUpscaleRes); decltype(&D3D11CreateDeviceAndSwapChain) ptrD3D11CreateDeviceAndSwapChainUpscaling; @@ -214,14 +214,14 @@ void Upscaling::DrawSettings() // Check the current upscale method auto upscaleMethod = GetUpscaleMethod(); - // DLSSperf: BSOpenVR size hook + RT::Create run once at world load, so + // PerfMode: BSOpenVR size hook + RT::Create run once at world load, so // runtime reads of method/qualityMode route through the boot snapshot. // The always-present explanation is plain text — only the staged-change // diff uses the RestartNeeded color so users learn the cue means "you // changed something that won't apply yet." - if (dlssPerf.IsHookActive()) { + if (perfMode.IsHookActive()) { ImGui::TextWrapped( - "DLSSperf is active: Method and Upscale Preset changes only take effect after a game restart. " + "Render-at-upscaled-resolution is active: Method and Upscale Preset changes only take effect after a game restart. " "Sharpness / model preset / Reflex remain live."); // Method pending-diff. Only fires when the user is editing the DLSS- @@ -270,7 +270,7 @@ void Upscaling::DrawSettings() if (baseLabel) { // Derive scale from live `settings.qualityMode` — `resolution- - // Scale` is locked to the DLSSperf boot snapshot, so reusing it + // Scale` is locked to the PerfMode boot snapshot, so reusing it // here would mismatch the slider position the user sees. const float displayScale = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)std::clamp(settings.qualityMode, 0u, 4u)); std::string labelWithScale = std::format("{} ( {:.2f}x )", baseLabel, displayScale); @@ -280,7 +280,7 @@ void Upscaling::DrawSettings() // Pending-diff vs the boot snapshot the runtime upscaler is // actually using. Without this the slider change looks like a // no-op. - if (dlssPerf.IsHookActive() && + if (perfMode.IsHookActive() && bootSnapshot.HasPendingChange(settings, &Settings::qualityMode)) { const uint bm = std::clamp(bootSnapshot.Boot(&Settings::qualityMode), 0u, 4u); const char* bootLabel = (upscaleMethod == UpscaleMethod::kDLSS) ? upscalePresetsDLSS[std::clamp(4 - (int)bm, 0, 4)] : upscalePresets[std::clamp(4 - (int)bm, 0, 4)]; @@ -304,39 +304,41 @@ void Upscaling::DrawSettings() } } - // VR DLSSperf: opt-in performance feature. Lives in the main + // VR PerfMode: opt-in performance feature. Lives in the main // upscaler section (not Backend Diagnostics) so users discover it // alongside the rest of the upscaler controls. Restart-gated — // the BSOpenVR size hook reads this at world load and sizes every // engine RT off the boot value. // // The setting persists across method switches (we don't auto-flip - // it when the user picks FSR/TAA), but the checkbox itself is - // disabled outside the DLSS context since the install path triple- - // gates on DLSS being the resolved method. Keep visible-but-greyed - // so users see the option exists and understand why it isn't live. + // it when the user picks TAA/NONE), but the checkbox itself is + // disabled outside upscalers that can target a separate displayRes + // output (DLSS, FSR). Keep visible-but-greyed so users see the + // option exists and understand why it isn't live. if (globals::game::isVR) { - const bool dlssAvailable = upscaleMethod == UpscaleMethod::kDLSS; - if (!dlssAvailable) + const bool methodSupportsPerf = + upscaleMethod == UpscaleMethod::kDLSS || + upscaleMethod == UpscaleMethod::kFSR; + if (!methodSupportsPerf) ImGui::BeginDisabled(); - ImGui::Checkbox("Render engine at upscaled resolution (DLSSperf)", &settings.enableDLSSperf); - if (!dlssAvailable) + ImGui::Checkbox("Render engine at upscaled resolution", &settings.renderAtUpscaleRes); + if (!methodSupportsPerf) ImGui::EndDisabled(); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "When enabled, the engine pipeline allocates render targets at the upscaled-render\n" - "resolution instead of the HMD display resolution. DLSS writes its output to a private\n" - "DisplayRes texture. Substantial VRAM and bandwidth savings, especially at high HMD\n" - "resolutions.\n" + "resolution instead of the HMD display resolution. The upscaler (DLSS or FSR) writes\n" + "its output to a private DisplayRes texture. Substantial VRAM and bandwidth savings,\n" + "especially at high HMD resolutions.\n" "\n" - "Requires the DLSS upscaler. Restart required to enable/disable. Method and Upscale\n" + "Requires DLSS or FSR. Restart required to enable/disable. Method and Upscale\n" "Preset changes also require a restart while this is active; sharpness / model preset\n" "/ Reflex remain live."); } - if (!dlssAvailable && settings.enableDLSSperf) - Util::Text::Disabled("DLSSperf requires DLSS — switch upscaler Method to DLSS to activate."); - if (dlssAvailable) - Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::enableDLSSperf); + if (!methodSupportsPerf && settings.renderAtUpscaleRes) + Util::Text::Disabled("Render-at-upscaled-resolution requires DLSS or FSR — switch upscaler Method to activate."); + if (methodSupportsPerf) + Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::renderAtUpscaleRes); } } @@ -701,10 +703,10 @@ void Upscaling::PostPostLoad() Upscaling::UpscaleMethod Upscaling::GetUpscaleMethod() const { - // Lock runtime to the boot upscaler under DLSSperf — engine RTs are + // Lock runtime to the boot upscaler under PerfMode — engine RTs are // sized for it, and routing a different method through testTexture/ // renderRes paths breaks the HMD. - if (globals::features::upscaling.dlssPerf.IsHookActive()) + if (globals::features::upscaling.perfMode.IsHookActive()) return static_cast(bootSnapshot.Boot(&Settings::upscaleMethod)); if (streamline.featureDLSS) return (UpscaleMethod)settings.upscaleMethod; @@ -1085,12 +1087,12 @@ void Upscaling::EnsureVRIntermediateTextures() auto screenSize = globals::state->screenSize; auto renderSize = Util::ConvertToDynamic(screenSize); - // DLSSperf: state->screenSize is polluted to RenderRes (the BSOpenVR size + // PerfMode: state->screenSize is polluted to RenderRes (the BSOpenVR size // hook spoofs HMD-recommended size). DLSS output needs to land at real - // DisplayRes, so size the OUTPUT intermediates from dlssPerf's snapshot + // DisplayRes, so size the OUTPUT intermediates from perfMode's snapshot // of the true HMD resolution. Input intermediates stay at renderSize. - const bool dlssperfActive = dlssPerf.IsHookActive() && dlssPerf.GetTestTexture(); - const float2 outputSize = dlssperfActive ? dlssPerf.GetDisplayScreenSize() : screenSize; + const bool dlssperfActive = perfMode.IsHookActive() && perfMode.GetTestTexture(); + const float2 outputSize = dlssperfActive ? perfMode.GetDisplayScreenSize() : screenSize; uint32_t eyeWidthOut = (uint32_t)(outputSize.x / 2); uint32_t eyeHeightOut = (uint32_t)outputSize.y; @@ -1164,10 +1166,18 @@ void Upscaling::FinalizePerEyeOutputs(ID3D11Resource* colorDst) state->BeginPerfEvent("VR Upscaling Finalize"); auto context = globals::d3d::context; - auto screenSize = state->screenSize; - uint32_t eyeWidthOut = (uint32_t)(screenSize.x / 2); - uint32_t eyeHeightOut = (uint32_t)screenSize.y; + // Drive output dims from the per-eye intermediate desc, not state->screenSize. + // Under PerfMode the state value is polluted to renderRes while the intermediates + // were allocated at displayRes via EnsureVRIntermediateTextures' size bridge. + if (!vrIntermediateColorOut[0]) { + if (state->frameAnnotations) + state->EndPerfEvent(); + return; + } + + uint32_t eyeWidthOut = vrIntermediateColorOut[0]->desc.Width; + uint32_t eyeHeightOut = vrIntermediateColorOut[0]->desc.Height; // Write upscaled outputs back for (uint32_t i = 0; i < 2; ++i) { @@ -1295,24 +1305,29 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) auto screenHeight = static_cast(screenSize.y); if (upscaleMethod != UpscaleMethod::kNONE && upscaleMethod != UpscaleMethod::kTAA) { - // DLSSperf: when the BSOpenVR size hook is live, every engine RT was + // PerfMode: when the BSOpenVR size hook is live, every engine RT was // already allocated at RenderRes — so the DRS-style scale is identity. - // Jitter is still computed at the real DisplayRes phase ratio so DLSS - // has enough sub-pixel diversity for the upscale. + // Jitter is still computed at the real DisplayRes phase ratio so the + // upscaler has enough sub-pixel diversity for reconstruction. // // The upscaleMethod here comes from GetUpscaleMethod(), which under - // DLSSperf+hookActive is locked to the boot snapshot — so this gate + // PerfMode+hookActive is locked to the boot snapshot — so this gate // reads the value the user had selected at game start, not what they // later moved the slider to. Engine RTs were sized off that boot // choice (irreversible — the size hook can't un-allocate them); the - // boot-snapshot lock keeps the runtime DLSS evaluate consistent with - // those allocations. UI staged-change banners explain the restart - // requirement for method/quality edits. - if (dlssPerf.IsHookActive() && upscaleMethod == UpscaleMethod::kDLSS) { + // boot-snapshot lock keeps the runtime evaluate consistent with those + // allocations. UI staged-change banners explain the restart + // requirement for method/quality edits. Branch fires for both DLSS + // and FSR since both consume the renderRes engine RTs and write to + // perfMode.testTexture. + const bool dlssperfRenderResPath = + perfMode.IsHookActive() && + (upscaleMethod == UpscaleMethod::kDLSS || upscaleMethod == UpscaleMethod::kFSR); + if (dlssperfRenderResPath) { resolutionScale = { 1.0f, 1.0f }; - auto renderWidth = static_cast(dlssPerf.GetRenderEyeWidth()); - auto displayWidth = static_cast(dlssPerf.GetDisplayEyeWidth()); + auto renderWidth = static_cast(perfMode.GetRenderEyeWidth()); + auto displayWidth = static_cast(perfMode.GetDisplayEyeWidth()); auto phaseCount = GetJitterPhaseCount(renderWidth, displayWidth); GetJitterOffset(&jitter.x, &jitter.y, state->frameCount, phaseCount); @@ -1322,11 +1337,11 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) else a_viewport->projectionPosScaleX = -2.0f * jitter.x / renderWidth; - a_viewport->projectionPosScaleY = 2.0f * jitter.y / static_cast(dlssPerf.GetRenderEyeHeight()); + a_viewport->projectionPosScaleY = 2.0f * jitter.y / static_cast(perfMode.GetRenderEyeHeight()); } else { - // Boot qualityMode under DLSSperf so projection stays coherent + // Boot qualityMode under PerfMode so projection stays coherent // with the engine RTs sized at install. - const uint32_t qm = globals::features::upscaling.dlssPerf.IsHookActive() ? bootSnapshot.Boot(&Settings::qualityMode) : settings.qualityMode; + const uint32_t qm = globals::features::upscaling.perfMode.IsHookActive() ? bootSnapshot.Boot(&Settings::qualityMode) : settings.qualityMode; float resolutionScaleBase = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)qm); auto renderWidth = static_cast(screenWidth * resolutionScaleBase); @@ -1896,7 +1911,14 @@ void Upscaling::Upscale() } streamline.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVectorCopyTexture->resource.get()); } else if (upscaleMethod == UpscaleMethod::kFSR) { - fidelityFX.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVector.texture, settings.sharpnessFSR); + // PerfMode bridge: when the engine RTs are shrunk to renderRes, FSR's displayRes + // output must land in perfMode.testTexture (the private displayRes target used for + // OpenVR submit), not back in the now-small kMAIN. Mirrors Streamline's colorOut + // routing for DLSS. + ID3D11Resource* fsrColorOut = (perfMode.IsHookActive() && perfMode.GetTestTexture()) ? + static_cast(perfMode.GetTestTexture()) : + nullptr; + fidelityFX.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVector.texture, settings.sharpnessFSR, fsrColorOut); } state->EndPerfEvent(); @@ -2305,19 +2327,23 @@ void Upscaling::Main_PostProcessing::thunk(RE::ImageSpaceManager* a_this, uint32 if (hdrLoaded) globals::features::hdrDisplay.RedirectFramebuffer(); - // DLSSperf: hybrid Post — HandlePostProcessing performs a two-layer + // PerfMode: hybrid Post — HandlePostProcessing performs a two-layer // struct swap around the engine's func() so tonemap + refraction read // the DisplayRes testTexture instead of the small kMAIN. The supplied // lambda is the engine call we'd normally make directly. // - // Upscaler gate: testTexture is only populated by DLSS's evaluate path - // (Streamline routes its colorOut there when DLSSperf is active). Under - // DLSSperf+hookActive, GetUpscaleMethod() returns the boot snapshot so - // this kDLSS check evaluates against the install-time choice — staged - // UI method changes don't reach here until restart. ShouldHandlePost() - // covers the partial-init case (post resources missing). - if (upscaleMethod == UpscaleMethod::kDLSS && globals::features::upscaling.dlssPerf.ShouldHandlePost()) { - globals::features::upscaling.dlssPerf.HandlePostProcessing([&]() { + // Upscaler gate: testTexture is populated by whichever upscaler ran + // (Streamline routes DLSS colorOut there, FidelityFX routes FSR + // colorOut there). Under PerfMode+hookActive, GetUpscaleMethod() returns + // the boot snapshot so this check evaluates against the install-time + // choice — staged UI method changes don't reach here until restart. + // ShouldHandlePost() covers the partial-init case (post resources + // missing). + const bool upscalerWritesTestTexture = + upscaleMethod == UpscaleMethod::kDLSS || + upscaleMethod == UpscaleMethod::kFSR; + if (upscalerWritesTestTexture && globals::features::upscaling.perfMode.ShouldHandlePost()) { + globals::features::upscaling.perfMode.HandlePostProcessing([&]() { func(a_this, a3, a_target, a_4, a_5); }); } else { diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index 481d491b83..9fbdcf47a4 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -1,7 +1,7 @@ #pragma once #include "Feature.h" -#include "Upscaling/DLSSperf.h" +#include "Upscaling/PerfMode.h" #include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" #include "Upscaling/RCAS/RCAS.h" @@ -71,12 +71,12 @@ struct Upscaling : Feature bool reflexUseFPSLimit = false; float reflexFPSLimit = 60.0f; - // VR DLSSperf: opt-in. When set, BSShaderRenderTargets::Create installs + // VR PerfMode: opt-in. When set, BSShaderRenderTargets::Create installs // the BSOpenVR render-target-size hook at engine init so the entire // engine pipeline allocates render targets at upscaled-render resolution // instead of display resolution. Saves VRAM/bandwidth proportional to // the quality-mode scale ratio. Requires a game restart to take effect. - bool enableDLSSperf = false; + bool renderAtUpscaleRes = false; }; Settings settings; @@ -84,7 +84,7 @@ struct Upscaling : Feature // Single source of truth for restart-gated fields. Order is not load-bearing // — the call-site `DrawSettingDiff` invocations in DrawSettings() handle any // per-field conditional gating (e.g., qualityMode/upscaleMethod banners only - // render while DLSSperf's render-target hook is active). MCP discovery + // render while PerfMode's render-target hook is active). MCP discovery // reports the full set; clients can check feature state themselves. // presetDLSS is deliberately NOT here: Streamline::SetDLSSOptions reads // settings.presetDLSS per-frame and applies it via slDLSSSetOptions, so @@ -92,7 +92,7 @@ struct Upscaling : Feature inline static constexpr Util::Settings::RestartTable kRestartFields{ { UTIL_RESTART_FIELD(Settings, frameGenerationMode, "Frame Generation"), UTIL_RESTART_FIELD(Settings, frameGenerationForceEnable, "Force Enable Frame Generation"), - UTIL_RESTART_FIELD(Settings, enableDLSSperf, "DLSSperf"), + UTIL_RESTART_FIELD(Settings, renderAtUpscaleRes, "Render at Upscaled Resolution"), UTIL_RESTART_FIELD(Settings, streamlineLogLevel, "Streamline Logging"), UTIL_RESTART_FIELD(Settings, upscaleMethod, "Upscaling Method"), UTIL_RESTART_FIELD(Settings, qualityMode, "Upscale Preset"), @@ -235,7 +235,7 @@ struct Upscaling : Feature static inline FidelityFX fidelityFX; ///< Only for frame generation static inline DX12SwapChain dx12SwapChain; static inline RCAS rcas; ///< Standalone RCAS sharpening for DLSS - static inline DLSSperf dlssPerf; ///< VR-only: render engine at upscaled-render res + static inline PerfMode perfMode; ///< VR-only: render engine at upscaled-render res winrt::com_ptr copyDepthToSharedBufferPS; @@ -265,11 +265,11 @@ struct Upscaling : Feature * * Same draw as UpscaleDepth's mask branch on the full-resolution path, * extracted so callers that bypass the standard upscale flow (notably - * DLSSperf::HandlePostProcessing, where engine RTs are pre-shrunk to + * PerfMode::HandlePostProcessing, where engine RTs are pre-shrunk to * renderRes and DLSS targets a private displayRes texture) can drive * the repair without going through UpscaleDepth's wider envelope. * Sets and leaves D3D11 pipeline state dirty on exit — wrap in your - * own save/restore (DLSSperf uses its FullscreenPassScope). + * own save/restore (PerfMode uses its FullscreenPassScope). */ void RunUnderwaterMaskRepair(); diff --git a/src/Features/Upscaling/FidelityFX.cpp b/src/Features/Upscaling/FidelityFX.cpp index b9976a99c7..e22ae987b9 100644 --- a/src/Features/Upscaling/FidelityFX.cpp +++ b/src/Features/Upscaling/FidelityFX.cpp @@ -273,8 +273,17 @@ void FidelityFX::CreateFSRResources() auto screenSize = state->screenSize; auto renderSize = Util::ConvertToDynamic(screenSize); - uint32_t displayWidth = (uint32_t)(globals::game::isVR ? screenSize.x / 2 : screenSize.x); - uint32_t displayHeight = (uint32_t)screenSize.y; + // PerfMode bridge: when the BSOpenVR size hook is live, state->screenSize is polluted + // to renderRes (engine RTs were allocated small). FSR3 still needs to upscale to the + // real HMD display resolution, so use perfMode's snapshot for displaySize/maxUpscaleSize. + // maxRenderSize stays at screenSize (which IS renderRes under the hook — that's FSR's + // expected input extent). + auto& perfMode = globals::features::upscaling.perfMode; + const bool dlssperfActive = perfMode.IsHookActive(); + const auto displaySize = dlssperfActive ? perfMode.GetDisplayScreenSize() : screenSize; + + uint32_t displayWidth = (uint32_t)(globals::game::isVR ? displaySize.x / 2 : displaySize.x); + uint32_t displayHeight = (uint32_t)displaySize.y; uint32_t renderWidth = (uint32_t)(globals::game::isVR ? renderSize.x / 2 : renderSize.x); uint32_t renderHeight = (uint32_t)renderSize.y; @@ -344,7 +353,7 @@ FfxResource ffxGetResource(ID3D11Resource* dx11Resource, return resource; } -void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors, float a_sharpness) +void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors, float a_sharpness, ID3D11Resource* a_colorOut) { auto renderer = globals::game::renderer; auto context = globals::d3d::context; @@ -357,6 +366,10 @@ void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r auto& upscaling = globals::features::upscaling; auto jitter = upscaling.jitter; + // Default to in-place output when caller didn't supply a separate destination. + if (!a_colorOut) + a_colorOut = a_upscalingTexture; + auto DispatchFSR = [&](uint32_t contextIndex, ID3D11Resource* r_color, ID3D11Resource* r_depth, ID3D11Resource* r_mvec, ID3D11Resource* r_reactive, ID3D11Resource* r_trans, ID3D11Resource* r_output, uint32_t r_width, float mv_scale_x) { @@ -431,8 +444,9 @@ void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r renderSize.x / 2.0f); } - // Merge outputs back to kMAIN - upscaling.FinalizePerEyeOutputs(a_upscalingTexture); + // Merge outputs into the supplied displayRes destination (kMAIN by default; + // perfMode.testTexture when PerfMode has shrunk the engine RTs). + upscaling.FinalizePerEyeOutputs(a_colorOut); } else { DispatchFSR(0, a_upscalingTexture, @@ -440,7 +454,7 @@ void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r a_motionVectors, a_reactiveMask, a_transparencyCompositionMask, - a_upscalingTexture, // Output to same texture + a_colorOut, (uint)renderSize.x, renderSize.x); } diff --git a/src/Features/Upscaling/FidelityFX.h b/src/Features/Upscaling/FidelityFX.h index 5db922ebec..f324bb580c 100644 --- a/src/Features/Upscaling/FidelityFX.h +++ b/src/Features/Upscaling/FidelityFX.h @@ -55,7 +55,11 @@ class FidelityFX void DestroyFSRResources(); - void Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors, float a_sharpness); + // a_colorOut is the destination for the upscaled result. When nullptr, output is written + // back into a_upscalingTexture (legacy in-place behavior). Callers route a separate output + // when the engine's kMAIN is renderRes (e.g., PerfMode VR mode) and the upscaled result + // must land in a displayRes target. + void Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors, float a_sharpness, ID3D11Resource* a_colorOut = nullptr); private: // FSR scratch buffer - needs to be freed in DestroyFSRResources diff --git a/src/Features/Upscaling/DLSSperf.cpp b/src/Features/Upscaling/PerfMode.cpp similarity index 86% rename from src/Features/Upscaling/DLSSperf.cpp rename to src/Features/Upscaling/PerfMode.cpp index 713104ef79..611017c498 100644 --- a/src/Features/Upscaling/DLSSperf.cpp +++ b/src/Features/Upscaling/PerfMode.cpp @@ -1,4 +1,4 @@ -#include "DLSSperf.h" +#include "PerfMode.h" #include #include @@ -9,10 +9,10 @@ // Quality mode → render-scale resolution is supplied by the FFX SDK helper // (same one Upscaling.cpp uses at ConfigureUpscaling), avoiding a duplicate // scale table here. Decoupled from the original PR's DlssEnhancer::Bridge so -// DLSSperf can ship without the larger enhancer framework. +// PerfMode can ship without the larger enhancer framework. #include -DLSSperf::FullscreenPassScope::FullscreenPassScope(ID3D11DeviceContext* a_context) : +PerfMode::FullscreenPassScope::FullscreenPassScope(ID3D11DeviceContext* a_context) : ctx(a_context) { ctx->OMGetRenderTargets(1, &savedRTV, &savedDSV); @@ -33,7 +33,7 @@ DLSSperf::FullscreenPassScope::FullscreenPassScope(ID3D11DeviceContext* a_contex ctx->IAGetPrimitiveTopology(&savedTopology); } -DLSSperf::FullscreenPassScope::~FullscreenPassScope() +PerfMode::FullscreenPassScope::~FullscreenPassScope() { // Null the SRV slot before restoring to break any potential SRV-vs-RTV // hazard from the pass we just ran (matches the explicit null-pass the @@ -93,7 +93,7 @@ DLSSperf::FullscreenPassScope::~FullscreenPassScope() savedIB->Release(); } -void DLSSperf::InstallRenderTargetSizeHook() +void PerfMode::InstallRenderTargetSizeHook() { if (!globals::game::isVR) return; @@ -104,14 +104,14 @@ void DLSSperf::InstallRenderTargetSizeHook() // Eager capture — get real HMD resolution BEFORE installing hook auto* openvr = RE::BSOpenVR::GetSingleton(); if (!openvr || !openvr->vrSystem) { - logger::error("[DLSSperf] BSOpenVR or vrSystem not available — hook NOT installed"); + logger::error("[PerfMode] BSOpenVR or vrSystem not available — hook NOT installed"); return; } uint32_t w = 0, h = 0; openvr->vrSystem->GetRecommendedRenderTargetSize(&w, &h); if (w == 0 || h == 0) { - logger::error("[DLSSperf] GetRecommendedRenderTargetSize returned {}x{} — hook NOT installed", w, h); + logger::error("[PerfMode] GetRecommendedRenderTargetSize returned {}x{} — hook NOT installed", w, h); return; } @@ -127,13 +127,13 @@ void DLSSperf::InstallRenderTargetSizeHook() // Validate before division: a bad/corrupt JSON could put qualityMode // outside FFX's range, returning 0/inf/NaN; that would propagate to bogus // renderEye dimensions and silently mis-size every engine RT. Fail closed - // — leave hookActive=false so the rest of DLSSperf is dormant and DLSS + // — leave hookActive=false so the rest of PerfMode is dormant and DLSS // runs on dev's standard path. const uint32_t qualityModeRaw = globals::features::upscaling.settings.qualityMode; const uint32_t qualityMode = std::clamp(qualityModeRaw, 0, 4); // FfxFsr3QualityMode range const float scale = ffxFsr3GetUpscaleRatioFromQualityMode(static_cast(qualityMode)); if (!std::isfinite(scale) || scale <= 0.0f) { - logger::error("[DLSSperf] FFX returned invalid upscale ratio {} for qualityMode {} (raw {}); hook NOT installed", scale, qualityMode, qualityModeRaw); + logger::error("[PerfMode] FFX returned invalid upscale ratio {} for qualityMode {} (raw {}); hook NOT installed", scale, qualityMode, qualityModeRaw); return; } renderEyeWidth = std::max(1, (uint32_t)(w / scale)); @@ -160,18 +160,18 @@ void DLSSperf::InstallRenderTargetSizeHook() hookActive = true; } -void DLSSperf::GetRenderTargetSize_Hook::thunk(RE::BSOpenVR* a_this, uint32_t* a_width, uint32_t* a_height) +void PerfMode::GetRenderTargetSize_Hook::thunk(RE::BSOpenVR* a_this, uint32_t* a_width, uint32_t* a_height) { // Call original to get real HMD resolution func(a_this, a_width, a_height); - auto& dlssPerf = globals::features::upscaling.dlssPerf; + auto& perfMode = globals::features::upscaling.perfMode; - *a_width = dlssPerf.renderEyeWidth; - *a_height = dlssPerf.renderEyeHeight; + *a_width = perfMode.renderEyeWidth; + *a_height = perfMode.renderEyeHeight; } -void DLSSperf::SetupResources() +void PerfMode::SetupResources() { if (!globals::game::isVR) return; @@ -179,7 +179,7 @@ void DLSSperf::SetupResources() auto renderer = globals::game::renderer; auto& mainRT = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; if (!mainRT.texture) { - logger::error("[DLSSperf] kMAIN texture not available in SetupResources"); + logger::error("[PerfMode] kMAIN texture not available in SetupResources"); return; } @@ -204,7 +204,7 @@ void DLSSperf::SetupResources() auto device = globals::d3d::device; HRESULT hr = device->CreateTexture2D(&desc, nullptr, testTexture.put()); if (FAILED(hr)) { - logger::error("[DLSSperf] Failed to create test texture: {:#x}", (uint32_t)hr); + logger::error("[PerfMode] Failed to create test texture: {:#x}", (uint32_t)hr); return; } @@ -215,7 +215,7 @@ void DLSSperf::SetupResources() srvDesc.Texture2D.MostDetailedMip = 0; hr = device->CreateShaderResourceView(testTexture.get(), &srvDesc, testTextureSRV.put()); if (FAILED(hr)) { - logger::error("[DLSSperf] Failed to create test texture SRV: {:#x}", (uint32_t)hr); + logger::error("[PerfMode] Failed to create test texture SRV: {:#x}", (uint32_t)hr); testTexture = nullptr; testTextureUAV = nullptr; return; @@ -229,7 +229,7 @@ void DLSSperf::SetupResources() uavDesc.Texture2D.MipSlice = 0; hr = device->CreateUnorderedAccessView(testTexture.get(), &uavDesc, testTextureUAV.put()); if (FAILED(hr)) { - logger::error("[DLSSperf] Failed to create testTexture UAV: {:#x}", (uint32_t)hr); + logger::error("[PerfMode] Failed to create testTexture UAV: {:#x}", (uint32_t)hr); } } @@ -241,7 +241,7 @@ void DLSSperf::SetupResources() rtvDesc.Texture2D.MipSlice = 0; hr = device->CreateRenderTargetView(testTexture.get(), &rtvDesc, testTextureRTV.put()); if (FAILED(hr)) { - logger::error("[DLSSperf] Failed to create testTexture RTV: {:#x}", (uint32_t)hr); + logger::error("[PerfMode] Failed to create testTexture RTV: {:#x}", (uint32_t)hr); } } @@ -252,7 +252,7 @@ void DLSSperf::SetupResources() hr = device->CreateTexture2D(&refraDesc, nullptr, refraTempTex.put()); if (FAILED(hr)) { - logger::error("[DLSSperf] Failed to create refraTempTex: {:#x}", (uint32_t)hr); + logger::error("[PerfMode] Failed to create refraTempTex: {:#x}", (uint32_t)hr); } else { D3D11_SHADER_RESOURCE_VIEW_DESC refraSrvDesc{}; refraSrvDesc.Format = refraDesc.Format; @@ -261,7 +261,7 @@ void DLSSperf::SetupResources() refraSrvDesc.Texture2D.MostDetailedMip = 0; hr = device->CreateShaderResourceView(refraTempTex.get(), &refraSrvDesc, refraTempSRV.put()); if (FAILED(hr)) { - logger::error("[DLSSperf] Failed to create refraTempSRV: {:#x}", (uint32_t)hr); + logger::error("[PerfMode] Failed to create refraTempSRV: {:#x}", (uint32_t)hr); refraTempTex = nullptr; } } @@ -281,7 +281,7 @@ void DLSSperf::SetupResources() HRESULT hr2 = device->CreateTexture2D(&fakeDesc, nullptr, fakeDS.put()); if (FAILED(hr2)) { - logger::error("[DLSSperf] Failed to create fake DS texture: {:#x}", (uint32_t)hr2); + logger::error("[PerfMode] Failed to create fake DS texture: {:#x}", (uint32_t)hr2); } else { // Create DSV — format depends on typeless base format D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc{}; @@ -302,12 +302,12 @@ void DLSSperf::SetupResources() hr2 = device->CreateDepthStencilView(fakeDS.get(), &dsvDesc, fakeDSV.put()); if (FAILED(hr2)) { - logger::error("[DLSSperf] Failed to create fake DSV: {:#x}", (uint32_t)hr2); + logger::error("[PerfMode] Failed to create fake DSV: {:#x}", (uint32_t)hr2); fakeDS = nullptr; } } } else { - logger::warn("[DLSSperf] kMAIN DS texture not available, skipping fake DS creation"); + logger::warn("[PerfMode] kMAIN DS texture not available, skipping fake DS creation"); } } @@ -352,21 +352,21 @@ void DLSSperf::SetupResources() // Downscale + blit shaders if (hookActive && !boxDownscalePS) { boxDownscalePS.attach(static_cast( - Util::CompileShader(L"Data/Shaders/Upscaling/DLSSperf/BoxDownscalePS.hlsl", { { "PSHADER", "" } }, "ps_5_0"))); + Util::CompileShader(L"Data/Shaders/Upscaling/PerfMode/BoxDownscalePS.hlsl", { { "PSHADER", "" } }, "ps_5_0"))); if (!boxDownscalePS) - logger::error("[DLSSperf] Failed to compile BoxDownscalePS"); + logger::error("[PerfMode] Failed to compile BoxDownscalePS"); } if (hookActive && !boxDownscaleVS) { boxDownscaleVS.attach(static_cast( Util::CompileShader(L"Data/Shaders/Upscaling/UpscaleVS.hlsl", { { "VSHADER", "" } }, "vs_5_0"))); if (!boxDownscaleVS) - logger::error("[DLSSperf] Failed to compile BoxDownscale VS"); + logger::error("[PerfMode] Failed to compile BoxDownscale VS"); } if (hookActive && !menuBlitPS) { menuBlitPS.attach(static_cast( - Util::CompileShader(L"Data/Shaders/Upscaling/DLSSperf/MenuBGBlitPS.hlsl", { { "PSHADER", "" } }, "ps_5_0"))); + Util::CompileShader(L"Data/Shaders/Upscaling/PerfMode/MenuBGBlitPS.hlsl", { { "PSHADER", "" } }, "ps_5_0"))); if (!menuBlitPS) - logger::error("[DLSSperf] Failed to compile MenuBGBlitPS"); + logger::error("[PerfMode] Failed to compile MenuBGBlitPS"); } if (hookActive && !linearSampler) { D3D11_SAMPLER_DESC sd{}; @@ -377,7 +377,7 @@ void DLSSperf::SetupResources() sd.MaxAnisotropy = 1; sd.MaxLOD = D3D11_FLOAT32_MAX; if (FAILED(device->CreateSamplerState(&sd, linearSampler.put()))) - logger::error("[DLSSperf] Failed to create linear sampler"); + logger::error("[PerfMode] Failed to create linear sampler"); } // Fail-closed pipeline-ready gate @@ -402,7 +402,7 @@ void DLSSperf::SetupResources() if (hookActive && !postPipelineReady) { logger::error( - "[DLSSperf] Post pipeline failed to initialize fully — Post wrap " + "[PerfMode] Post pipeline failed to initialize fully — Post wrap " "disabled, engine RTs remain at RenderRes. Check upstream resource " "creation errors above."); } @@ -415,11 +415,11 @@ void DLSSperf::SetupResources() // Inner layer of two-layer swap: swaps kMAIN SRV → testTextureSRV and // kMAIN DS → fakeDS before tonemap Render(), restores after. -void DLSSperf::TonemapRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) +void PerfMode::TonemapRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) { - auto& dlssPerf = globals::features::upscaling.dlssPerf; + auto& perfMode = globals::features::upscaling.perfMode; - if (!dlssPerf.hookActive || !dlssPerf.testTextureSRV || !dlssPerf.fakeDSV) { + if (!perfMode.hookActive || !perfMode.testTextureSRV || !perfMode.fakeDSV) { func(imageSpaceShader, shape, param); return; } @@ -434,12 +434,12 @@ void DLSSperf::TonemapRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* // runs at most once per frame regardless of how many menu redraws fire. if (globals::state && globals::state->IsMainOrLoadingMenuOpen()) { func(imageSpaceShader, shape, param); - dlssPerf.MaybeBlitMenuBG(RE::RENDER_TARGETS::kTOTAL); + perfMode.MaybeBlitMenuBG(RE::RENDER_TARGETS::kTOTAL); return; } ZoneScoped; - TracyD3D11Zone(globals::state->tracyCtx, "DLSSperf::TonemapRender"); + TracyD3D11Zone(globals::state->tracyCtx, "PerfMode::TonemapRender"); auto renderer = RE::BSGraphics::Renderer::GetSingleton(); auto& rtData = renderer->GetRuntimeData(); @@ -447,43 +447,43 @@ void DLSSperf::TonemapRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* // --- Swap kMAIN SRV → testTextureSRV (so tonemap reads 3k upscaled color) --- auto& kmainRT = rtData.renderTargets[RE::RENDER_TARGETS::kMAIN]; - dlssPerf.savedKMainSRV = kmainRT.SRV; - kmainRT.SRV = dlssPerf.testTextureSRV.get(); + perfMode.savedKMainSRV = kmainRT.SRV; + kmainRT.SRV = perfMode.testTextureSRV.get(); // --- Also swap kMAIN_COPY SRV (refraction path reads this instead of kMAIN) --- auto& kmainCopyRT = rtData.renderTargets[RE::RENDER_TARGETS::kMAIN_COPY]; - dlssPerf.savedKMainCopySRV = kmainCopyRT.SRV; - kmainCopyRT.SRV = dlssPerf.testTextureSRV.get(); + perfMode.savedKMainCopySRV = kmainCopyRT.SRV; + kmainCopyRT.SRV = perfMode.testTextureSRV.get(); // --- Swap kMAIN DS views → fakeDS (so 3k RT doesn't mismatch 1k DS) --- auto& kmainDS = dsData.depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; for (int i = 0; i < 8; i++) { - dlssPerf.savedKMainViews[i] = kmainDS.views[i]; + perfMode.savedKMainViews[i] = kmainDS.views[i]; if (kmainDS.views[i]) - kmainDS.views[i] = dlssPerf.fakeDSV.get(); + kmainDS.views[i] = perfMode.fakeDSV.get(); } for (int i = 0; i < 8; i++) { - dlssPerf.savedKMainReadOnlyViews[i] = kmainDS.readOnlyViews[i]; + perfMode.savedKMainReadOnlyViews[i] = kmainDS.readOnlyViews[i]; if (kmainDS.readOnlyViews[i]) - kmainDS.readOnlyViews[i] = dlssPerf.fakeDSV.get(); + kmainDS.readOnlyViews[i] = perfMode.fakeDSV.get(); } // --- Call original (or FrameAnnotations chain) --- func(imageSpaceShader, shape, param); // --- Restore kMAIN SRV --- - kmainRT.SRV = dlssPerf.savedKMainSRV; - dlssPerf.savedKMainSRV = nullptr; + kmainRT.SRV = perfMode.savedKMainSRV; + perfMode.savedKMainSRV = nullptr; // --- Restore kMAIN_COPY SRV --- - kmainCopyRT.SRV = dlssPerf.savedKMainCopySRV; - dlssPerf.savedKMainCopySRV = nullptr; + kmainCopyRT.SRV = perfMode.savedKMainCopySRV; + perfMode.savedKMainCopySRV = nullptr; // --- Restore kMAIN DS views --- for (int i = 0; i < 8; i++) - kmainDS.views[i] = dlssPerf.savedKMainViews[i]; + kmainDS.views[i] = perfMode.savedKMainViews[i]; for (int i = 0; i < 8; i++) - kmainDS.readOnlyViews[i] = dlssPerf.savedKMainReadOnlyViews[i]; + kmainDS.readOnlyViews[i] = perfMode.savedKMainReadOnlyViews[i]; } // ============================================================================ @@ -493,17 +493,17 @@ void DLSSperf::TonemapRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* // After func() returns, D3D11 state is sticky (PS/CB/sampler/IA all still bound). // We replay the draw with our own RT (testTexture 3k), VP (3k), and SRV (refraTempTex 3k). -void DLSSperf::RefractionRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) +void PerfMode::RefractionRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) { - auto& dlssPerf = globals::features::upscaling.dlssPerf; + auto& perfMode = globals::features::upscaling.perfMode; - if (!dlssPerf.hookActive || !dlssPerf.testTextureRTV || !dlssPerf.refraTempSRV) { + if (!perfMode.hookActive || !perfMode.testTextureRTV || !perfMode.refraTempSRV) { func(imageSpaceShader, shape, param); return; } ZoneScoped; - TracyD3D11Zone(globals::state->tracyCtx, "DLSSperf::RefractionRender"); + TracyD3D11Zone(globals::state->tracyCtx, "PerfMode::RefractionRender"); // --- Pass 1: engine's normal 1k refraction (untouched) --- func(imageSpaceShader, shape, param); @@ -532,21 +532,21 @@ void DLSSperf::RefractionRender_Hook::thunk(void* imageSpaceShader, RE::BSTriSha context->PSGetShaderResources(0, 1, &savedSRV0); // Set 3k output: testTexture RTV, no DS needed for fullscreen IS shader - ID3D11RenderTargetView* rtv3k = dlssPerf.testTextureRTV.get(); + ID3D11RenderTargetView* rtv3k = perfMode.testTextureRTV.get(); context->OMSetRenderTargets(1, &rtv3k, nullptr); // Set 3k VP D3D11_VIEWPORT vp3k = {}; vp3k.TopLeftX = 0.0f; vp3k.TopLeftY = 0.0f; - vp3k.Width = static_cast(dlssPerf.displayEyeWidth * 2); - vp3k.Height = static_cast(dlssPerf.displayEyeHeight); + vp3k.Width = static_cast(perfMode.displayEyeWidth * 2); + vp3k.Height = static_cast(perfMode.displayEyeHeight); vp3k.MinDepth = 0.0f; vp3k.MaxDepth = 1.0f; context->RSSetViewports(1, &vp3k); // Set 3k input: refraTempTex as t0 (scene color for refraction sampling) - ID3D11ShaderResourceView* srv3k = dlssPerf.refraTempSRV.get(); + ID3D11ShaderResourceView* srv3k = perfMode.refraTempSRV.get(); context->PSSetShaderResources(0, 1, &srv3k); // Draw with the same geometry (BSTriShape fullscreen quad, IA still bound) @@ -574,8 +574,8 @@ void DLSSperf::RefractionRender_Hook::thunk(void* imageSpaceShader, RE::BSTriSha // ISCopyRender_Hook: stretch ISCopy when source < dest (menu compositor fix) // ============================================================================ // The VR menu compositor uses a single ISCopy draw to blit the rendered scene -// (kMAIN — RenderRes under DLSSperf) into a fixed-size projection surface -// (kPROJECTEDMENU 2048², or kMENUBG which DLSSperf enlarges to DisplayRes). +// (kMAIN — RenderRes under PerfMode) into a fixed-size projection surface +// (kPROJECTEDMENU 2048², or kMENUBG which PerfMode enlarges to DisplayRes). // Engine ISCopy uses a 1:1 viewport sized to the source, so the small source // is stamped into the top-left of the larger dest. Symptom: "main menu image // looks downscaled." @@ -586,16 +586,16 @@ void DLSSperf::RefractionRender_Hook::thunk(void* imageSpaceShader, RE::BSTriSha // relies on), so the replay needs only a viewport change + DrawIndexed and // the engine's clamp-sampler stretches the source naturally. // -// In-game ISCopy (where source.w == dest.w under DLSSperf — kMAIN renderRes +// In-game ISCopy (where source.w == dest.w under PerfMode — kMAIN renderRes // → kMAIN_COPY renderRes) takes the early-out branch and the engine's draw // is the final pixel. -void DLSSperf::ISCopyRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) +void PerfMode::ISCopyRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) { - auto& dlssPerf = globals::features::upscaling.dlssPerf; + auto& perfMode = globals::features::upscaling.perfMode; // Inactive / non-VR: passthrough. - if (!dlssPerf.hookActive) { + if (!perfMode.hookActive) { func(imageSpaceShader, shape, param); return; } @@ -647,7 +647,7 @@ void DLSSperf::ISCopyRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* if (needsStretch) { ZoneScoped; - TracyD3D11Zone(globals::state->tracyCtx, "DLSSperf::ISCopyStretch"); + TracyD3D11Zone(globals::state->tracyCtx, "PerfMode::ISCopyStretch"); // Replay viewport: full dest extent, preserve depth range from the // original so anything sampling depth (unlikely for ISCopy but safe) @@ -680,13 +680,13 @@ void DLSSperf::ISCopyRender_Hook::thunk(void* imageSpaceShader, RE::BSTriShape* // UI pass draws VR HUD to kMENUBG (now 3k). Engine binds KMAIN(DS) as DS, // which is still 1k → size mismatch. Swap to fakeDS (3k) before, restore after. -void DLSSperf::UIPassDispatch_Hook::thunk(RE::BSGraphics::BSShaderAccumulator* shaderAccumulator, uint32_t renderFlags) +void PerfMode::UIPassDispatch_Hook::thunk(RE::BSGraphics::BSShaderAccumulator* shaderAccumulator, uint32_t renderFlags) { - auto& dlssPerf = globals::features::upscaling.dlssPerf; + auto& perfMode = globals::features::upscaling.perfMode; // Only intercept renderMode==24 (UI pass) when hook is active auto& rtData = shaderAccumulator->GetRuntimeData(); - if (!dlssPerf.hookActive || !dlssPerf.fakeDSV || rtData.renderMode != 24) { + if (!perfMode.hookActive || !perfMode.fakeDSV || rtData.renderMode != 24) { func(shaderAccumulator, renderFlags); return; } @@ -701,12 +701,12 @@ void DLSSperf::UIPassDispatch_Hook::thunk(RE::BSGraphics::BSShaderAccumulator* s for (int i = 0; i < 8; i++) { savedViews[i] = kmainDS.views[i]; if (kmainDS.views[i]) - kmainDS.views[i] = dlssPerf.fakeDSV.get(); + kmainDS.views[i] = perfMode.fakeDSV.get(); } for (int i = 0; i < 8; i++) { savedReadOnlyViews[i] = kmainDS.readOnlyViews[i]; if (kmainDS.readOnlyViews[i]) - kmainDS.readOnlyViews[i] = dlssPerf.fakeDSV.get(); + kmainDS.readOnlyViews[i] = perfMode.fakeDSV.get(); } // Force engine to re-bind DS from struct @@ -721,19 +721,19 @@ void DLSSperf::UIPassDispatch_Hook::thunk(RE::BSGraphics::BSShaderAccumulator* s savedVP = vp; vp.TopLeftX = 0.0f; vp.TopLeftY = 0.0f; - vp.Width = static_cast(dlssPerf.displayEyeWidth * 2); - vp.Height = static_cast(dlssPerf.displayEyeHeight); + vp.Width = static_cast(perfMode.displayEyeWidth * 2); + vp.Height = static_cast(perfMode.displayEyeHeight); vp.MinDepth = 0.0f; vp.MaxDepth = 1.0f; globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_VIEWPORT); } // Skip VP compression in UpdateViewPort hook during UI pass - dlssPerf.postInterceptActive = true; + perfMode.postInterceptActive = true; func(shaderAccumulator, renderFlags); - dlssPerf.postInterceptActive = false; + perfMode.postInterceptActive = false; // Restore viewport if (ss) { @@ -758,11 +758,11 @@ void DLSSperf::UIPassDispatch_Hook::thunk(RE::BSGraphics::BSShaderAccumulator* s // After func() returns, clear postChainDone so the Present-前 UI chain // and the next frame use normal VP compression. -void DLSSperf::PlayerViewRender_Hook::thunk(void* a1, bool a2, bool a3) +void PerfMode::PlayerViewRender_Hook::thunk(void* a1, bool a2, bool a3) { func(a1, a2, a3); - globals::features::upscaling.dlssPerf.ClearPostChainDone(); + globals::features::upscaling.perfMode.ClearPostChainDone(); } // ============================================================================ @@ -770,14 +770,14 @@ void DLSSperf::PlayerViewRender_Hook::thunk(void* a1, bool a2, bool a3) // ============================================================================ // Wraps DS swap around the engine's RT/DS flush so enlarged-RT draws don't // rasterizer-clip to the smaller kMAIN DS bounds. -void DLSSperf::BSGraphics_SetDirtyStates_Hook::thunk(bool isCompute) +void PerfMode::BSGraphics_SetDirtyStates_Hook::thunk(bool isCompute) { bool swapped = false; if (!isCompute) - swapped = globals::features::upscaling.dlssPerf.MaybeSwapDSForEnlargedRT(); + swapped = globals::features::upscaling.perfMode.MaybeSwapDSForEnlargedRT(); func(isCompute); if (swapped) - globals::features::upscaling.dlssPerf.RestoreSwappedDS(); + globals::features::upscaling.perfMode.RestoreSwappedDS(); } // ============================================================================ @@ -785,33 +785,33 @@ void DLSSperf::BSGraphics_SetDirtyStates_Hook::thunk(bool isCompute) // ============================================================================ // Post-corrects the engine viewport when the bound RT and the requested VP // don't agree about render-vs-display extent. Was originally in Hooks.cpp. -void DLSSperf::BSGraphics_Renderer_UpdateViewPort_Hook::thunk(RE::BSGraphics::Renderer* a_this, uint32_t a_width, uint32_t a_height, bool a_forceMatchRT) +void PerfMode::BSGraphics_Renderer_UpdateViewPort_Hook::thunk(RE::BSGraphics::Renderer* a_this, uint32_t a_width, uint32_t a_height, bool a_forceMatchRT) { func(a_this, a_width, a_height, a_forceMatchRT); - auto& dlssPerf = globals::features::upscaling.dlssPerf; - if (!dlssPerf.IsHookActive()) + auto& perfMode = globals::features::upscaling.perfMode; + if (!perfMode.IsHookActive()) return; // During Post intercept enlarged kTEMP/kTOTAL already get the right VP // from func() because of their inflated RT dims — don't second-guess it. - if (dlssPerf.IsPostInterceptActive()) + if (perfMode.IsPostInterceptActive()) return; auto* ss = globals::game::shadowState; if (!ss) return; auto& vp = ss->GetVRRuntimeData().viewPort; - const uint32_t displayW = dlssPerf.GetDisplayEyeWidth() * 2; - const uint32_t displayH = dlssPerf.GetDisplayEyeHeight(); - const uint32_t renderW = dlssPerf.GetRenderEyeWidth() * 2; - const uint32_t renderH = dlssPerf.GetRenderEyeHeight(); + const uint32_t displayW = perfMode.GetDisplayEyeWidth() * 2; + const uint32_t displayH = perfMode.GetDisplayEyeHeight(); + const uint32_t renderW = perfMode.GetRenderEyeWidth() * 2; + const uint32_t renderH = perfMode.GetRenderEyeHeight(); // After the Post chain, UI / submit-prep draws target enlarged kTOTAL // at displayRes — expand any renderRes VP the engine sets back up. // The fade Draw(30) bypasses this path entirely (direct D3D RSSet- // Viewports) and is handled by the Draw vfunc hook in Globals.cpp. - if (dlssPerf.IsPostChainDone()) { + if (perfMode.IsPostChainDone()) { if (static_cast(vp.Width) == renderW && static_cast(vp.Height) == renderH) { vp.Width = static_cast(displayW); @@ -852,14 +852,14 @@ void DLSSperf::BSGraphics_Renderer_UpdateViewPort_Hook::thunk(RE::BSGraphics::Re // entire Post chain (covers the copy step #10 which binds kMAIN_COPY DS). // Inner layer (tonemap hook) handles kMAIN DS + kMAIN SRV for step #9. -void DLSSperf::BeginPostIntercept() +void PerfMode::BeginPostIntercept() { if (!hookActive || !fakeDSV) return; ZoneScoped; auto state = globals::state; - state->BeginPerfEvent("DLSSperf::BeginPostIntercept"); + state->BeginPerfEvent("PerfMode::BeginPostIntercept"); auto renderer = RE::BSGraphics::Renderer::GetSingleton(); auto& dsData = renderer->GetDepthStencilData(); @@ -882,14 +882,14 @@ void DLSSperf::BeginPostIntercept() state->EndPerfEvent(); } -void DLSSperf::EndPostIntercept() +void PerfMode::EndPostIntercept() { if (!hookActive || !fakeDSV) return; ZoneScoped; auto state = globals::state; - state->BeginPerfEvent("DLSSperf::EndPostIntercept"); + state->BeginPerfEvent("PerfMode::EndPostIntercept"); auto renderer = RE::BSGraphics::Renderer::GetSingleton(); auto& dsData = renderer->GetDepthStencilData(); @@ -916,7 +916,7 @@ void DLSSperf::EndPostIntercept() // - No refraction: kMAIN is the pyramid input directly. // - With refraction: engine composites kMAIN → kMAIN_COPY, which enters pyramid. -void DLSSperf::DownscaleToKMain() +void PerfMode::DownscaleToKMain() { if (!hookActive || !testTextureSRV || !boxDownscalePS || !boxDownscaleVS || !linearSampler) return; @@ -934,8 +934,8 @@ void DLSSperf::DownscaleToKMain() if (!kmain.RTV) return; - state->BeginPerfEvent("DLSSperf::DownscaleToKMain"); - TracyD3D11Zone(state->tracyCtx, "DLSSperf::DownscaleToKMain"); + state->BeginPerfEvent("PerfMode::DownscaleToKMain"); + TracyD3D11Zone(state->tracyCtx, "PerfMode::DownscaleToKMain"); { FullscreenPassScope stateScope(context); @@ -987,7 +987,7 @@ void DLSSperf::DownscaleToKMain() // dest. One-shot per frame via blittedFrameId (Present doesn't fire here // and PlayerView doesn't fire in main-menu, so the frame-id guard is the // only reliable per-frame boundary). -void DLSSperf::MaybeBlitMenuBG(uint32_t boundRTIdx) +void PerfMode::MaybeBlitMenuBG(uint32_t boundRTIdx) { const uint32_t currentFrame = globals::state ? globals::state->frameCount : 0; if (!hookActive || blittedFrameId == currentFrame || !menuBlitPS || !boxDownscaleVS || !linearSampler) @@ -1009,8 +1009,8 @@ void DLSSperf::MaybeBlitMenuBG(uint32_t boundRTIdx) ZoneScoped; auto state = globals::state; auto* context = globals::d3d::context; - state->BeginPerfEvent("DLSSperf::MenuBGBlit"); - TracyD3D11Zone(state->tracyCtx, "DLSSperf::MenuBGBlit"); + state->BeginPerfEvent("PerfMode::MenuBGBlit"); + TracyD3D11Zone(state->tracyCtx, "PerfMode::MenuBGBlit"); globals::features::upscaling.Upscale(); @@ -1055,11 +1055,11 @@ void DLSSperf::MaybeBlitMenuBG(uint32_t boundRTIdx) state->EndPerfEvent(); } -void DLSSperf::HandlePostProcessing(const std::function& enginePost) +void PerfMode::HandlePostProcessing(const std::function& enginePost) { ZoneScoped; auto state = globals::state; - state->BeginPerfEvent("DLSSperf::HandlePostProcessing"); + state->BeginPerfEvent("PerfMode::HandlePostProcessing"); // Copy testTexture → refraTempTex before Post, so ISRefraction can read 3k scene if (refraTempTex) { @@ -1071,7 +1071,7 @@ void DLSSperf::HandlePostProcessing(const std::function& enginePost) DownscaleToKMain(); // Underwater mask analytical repair. Engine RTs (depth, mask) are at - // renderRes under DLSSperf, so the full-resolution path of UpscaleDepth + // renderRes under PerfMode, so the full-resolution path of UpscaleDepth // would apply — but routing through UpscaleDepth here leaves pipeline // state dirty and the trailing enginePost() loses kMAIN. Drive the // mask-only draw directly inside our own FullscreenPassScope so the @@ -1093,7 +1093,7 @@ void DLSSperf::HandlePostProcessing(const std::function& enginePost) state->EndPerfEvent(); } -bool DLSSperf::MaybeSwapDSForEnlargedRT() +bool PerfMode::MaybeSwapDSForEnlargedRT() { if (!hookActive || postInterceptActive) return false; @@ -1105,7 +1105,7 @@ bool DLSSperf::MaybeSwapDSForEnlargedRT() return false; auto& srd = ss->GetVRRuntimeData(); - // Only the three RTs DLSSperf_MaybeEnlargeRT inflates to displayRes. + // Only the three RTs PerfMode_MaybeEnlargeRT inflates to displayRes. const uint32_t rtIdx = static_cast(srd.renderTargets[0]); if (rtIdx != RE::RENDER_TARGETS::kTOTAL && rtIdx != RE::RENDER_TARGETS::kMENUBG && @@ -1143,7 +1143,7 @@ bool DLSSperf::MaybeSwapDSForEnlargedRT() return true; } -void DLSSperf::RestoreSwappedDS() +void PerfMode::RestoreSwappedDS() { if (autoSwapDSIdx == UINT32_MAX) return; @@ -1169,22 +1169,22 @@ void DLSSperf::RestoreSwappedDS() // ID3D11DeviceContext_Draw_Hook (vtable index 13) // ============================================================================ // Engine fade-overlay Draw(30) fires after the Post chain and before Submit. -// Under DLSSperf the draw's VP is computed at renderRes while the RT (kTOTAL) +// Under PerfMode the draw's VP is computed at renderRes while the RT (kTOTAL) // is displayRes — partial-screen "black stamp" without this swap. Gate on // VertexCount==30 + isVR keeps the cost a single comparison on flat / non- // fade draws. -void DLSSperf::ID3D11DeviceContext_Draw_Hook::thunk(ID3D11DeviceContext* This, UINT VertexCount, UINT StartVertexLocation) +void PerfMode::ID3D11DeviceContext_Draw_Hook::thunk(ID3D11DeviceContext* This, UINT VertexCount, UINT StartVertexLocation) { if (VertexCount == 30 && globals::game::isVR) { - auto& dlssPerf = globals::features::upscaling.dlssPerf; - if (dlssPerf.IsHookActive() && dlssPerf.IsPostChainDone()) { + auto& perfMode = globals::features::upscaling.perfMode; + if (perfMode.IsHookActive() && perfMode.IsPostChainDone()) { UINT numVP = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; D3D11_VIEWPORT savedVP[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE] = {}; This->RSGetViewports(&numVP, savedVP); D3D11_VIEWPORT vp{}; - vp.Width = static_cast(dlssPerf.GetDisplayEyeWidth() * 2); - vp.Height = static_cast(dlssPerf.GetDisplayEyeHeight()); + vp.Width = static_cast(perfMode.GetDisplayEyeWidth() * 2); + vp.Height = static_cast(perfMode.GetDisplayEyeHeight()); vp.MinDepth = 0.0f; vp.MaxDepth = 1.0f; This->RSSetViewports(1, &vp); @@ -1199,14 +1199,14 @@ void DLSSperf::ID3D11DeviceContext_Draw_Hook::thunk(ID3D11DeviceContext* This, U func(This, VertexCount, StartVertexLocation); } -void DLSSperf::InstallFadeOverlayHook(ID3D11DeviceContext* context) +void PerfMode::InstallFadeOverlayHook(ID3D11DeviceContext* context) { if (!globals::game::isVR || !context) return; stl::detour_vfunc<13, ID3D11DeviceContext_Draw_Hook>(context); } -void DLSSperf::InstallCreateRTThunks() +void PerfMode::InstallCreateRTThunks() { if (!REL::Module::IsVR()) return; @@ -1216,7 +1216,7 @@ void DLSSperf::InstallCreateRTThunks() stl::write_thunk_call(vrBase + 0x1547); } -void DLSSperf::BeginCreateRTEnlarge() +void PerfMode::BeginCreateRTEnlarge() { if (!hookActive) return; @@ -1225,7 +1225,7 @@ void DLSSperf::BeginCreateRTEnlarge() enlargeActive = true; } -void DLSSperf::EndCreateRTEnlarge() +void PerfMode::EndCreateRTEnlarge() { enlargeActive = false; } @@ -1234,7 +1234,7 @@ namespace { void EnlargeProps(RE::BSGraphics::RenderTargetProperties* a_props) { - auto& dp = globals::features::upscaling.dlssPerf; + auto& dp = globals::features::upscaling.perfMode; if (!dp.IsCreateRTEnlargeActive()) return; a_props->width = dp.GetEnlargeWidth(); @@ -1242,27 +1242,27 @@ namespace } } -void DLSSperf::CreateRT_MenuBG_Hook::thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) +void PerfMode::CreateRT_MenuBG_Hook::thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) { EnlargeProps(a_properties); func(a_this, a_target, a_properties); } -void DLSSperf::CreateRT_ImagespaceTempCopy_Hook::thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) +void PerfMode::CreateRT_ImagespaceTempCopy_Hook::thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) { EnlargeProps(a_properties); func(a_this, a_target, a_properties); } -void DLSSperf::CreateRT_Total_Hook::thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) +void PerfMode::CreateRT_Total_Hook::thunk(RE::BSGraphics::Renderer* a_this, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) { EnlargeProps(a_properties); func(a_this, a_target, a_properties); } -void DLSSperf::DrawSettings() +void PerfMode::DrawSettings() { - // DLSSperf has no user-facing settings of its own — enablement is gated + // PerfMode has no user-facing settings of its own — enablement is gated // at install time by whether the BSShaderRenderTargets::Create hook ran // successfully. A future PR may surface diagnostic info here. } diff --git a/src/Features/Upscaling/DLSSperf.h b/src/Features/Upscaling/PerfMode.h similarity index 97% rename from src/Features/Upscaling/DLSSperf.h rename to src/Features/Upscaling/PerfMode.h index a5a331b2f1..dee76bc168 100644 --- a/src/Features/Upscaling/DLSSperf.h +++ b/src/Features/Upscaling/PerfMode.h @@ -1,7 +1,7 @@ #pragma once // ============================================================================ -// DLSSperf — render-target size hook + post-processing interception +// PerfMode — render-target size hook + post-processing interception // ============================================================================ // // Opt-in VR upscaling feature. Hooks BSOpenVR::GetRenderTargetSize so all @@ -34,7 +34,7 @@ #include #include -struct DLSSperf +struct PerfMode { void SetupResources(); void DrawSettings(); @@ -79,7 +79,7 @@ struct DLSSperf void DownscaleToKMain(); // Post hybrid entry point: called from Upscaling's Main_PostProcessing::thunk. - // Wraps the engine Post chain with DLSSperf's two-layer struct swap. + // Wraps the engine Post chain with PerfMode's two-layer struct swap. // Keyed on postPipelineReady (set at the end of SetupResources) so a // partial-init state can't slip past the gate into a null deref. The // runtime upscaler-method gate is enforced separately by callers (the @@ -114,7 +114,7 @@ struct DLSSperf // that fixes the scene-fade overlay viewport. Called from Globals:: // InstallD3DHooks. VR-only; thunk early-outs unless VertexCount==30 // and the hook is live, so cost is one comparison per Draw call when - // DLSSperf isn't active. + // PerfMode isn't active. void InstallFadeOverlayHook(ID3D11DeviceContext* context); // Enlarge window — set true around the engine's BSShaderRenderTargets:: @@ -229,7 +229,7 @@ struct DLSSperf // IS shader hook: ISCopy (Render vfunc 0x1 on vtable[3]). // The VR main menu / pause compositor uses a single ISCopy draw from kMAIN - // (RenderRes when DLSSperf is active) into kPROJECTEDMENU (fixed 2048²) or + // (RenderRes when PerfMode is active) into kPROJECTEDMENU (fixed 2048²) or // kMENUBG (DisplayRes via enlargement). With a 1:1 viewport the small // source gets stamped into the top-left of the larger dest — that's the // "main menu looks downscaled" bug. Strategy: let func() draw normally, @@ -265,7 +265,7 @@ struct DLSSperf // Chains via stl::detour_thunk on the same address Hooks.cpp + Terrain- // Blending already detour. Wraps MaybeSwapDSForEnlargedRT around the - // engine's RT/DS flush; runs after the prior thunk so DLSSperf's swap + // engine's RT/DS flush; runs after the prior thunk so PerfMode's swap // is the innermost wrap. struct BSGraphics_SetDirtyStates_Hook { @@ -275,7 +275,7 @@ struct DLSSperf bool setDirtyStatesHookInstalled = false; // D3D11 Draw vfunc detour. Engine's scene-fade overlay is a Draw(30) - // that fires after the Post chain and before Submit. Under DLSSperf + // that fires after the Post chain and before Submit. Under PerfMode // the draw's VP/vertices are computed at renderRes while the RT // (kTOTAL) is displayRes — produces a partial-screen "black stamp" // without this swap. diff --git a/src/Features/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index c5c2a812a2..29e4aebe74 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -10,7 +10,7 @@ #include "../../State.h" #include "../../Util.h" #include "../Upscaling.h" -#include "DLSSperf.h" +#include "PerfMode.h" #include "DX12SwapChain.h" namespace @@ -421,9 +421,9 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) { sl::DLSSOptions dlssOptions{}; - // Boot qualityMode under DLSSperf — DLSS dispatch must match the + // Boot qualityMode under PerfMode — DLSS dispatch must match the // renderRes the engine was sized for at install. - uint32_t qualityMode = globals::features::upscaling.dlssPerf.IsHookActive() ? globals::features::upscaling.bootSnapshot.Boot(&Upscaling::Settings::qualityMode) : globals::features::upscaling.settings.qualityMode; + uint32_t qualityMode = globals::features::upscaling.perfMode.IsHookActive() ? globals::features::upscaling.bootSnapshot.Boot(&Upscaling::Settings::qualityMode) : globals::features::upscaling.settings.qualityMode; switch (qualityMode) { case 1: dlssOptions.mode = sl::DLSSMode::eMaxQuality; @@ -444,15 +444,15 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) auto state = globals::state; - // DLSSperf bridge: state->screenSize.y is polluted to RenderRes by the - // BSOpenVR size hook; use dlssPerf's snapshot of the real DisplayRes when + // PerfMode bridge: state->screenSize.y is polluted to RenderRes by the + // BSOpenVR size hook; use perfMode's snapshot of the real DisplayRes when // the hook is live so DLSS is created at the right scale. The width arg // is already display-correct (caller computes from displaySize). - auto& dlssPerf = globals::features::upscaling.dlssPerf; - const bool dlssperfActive = dlssPerf.IsHookActive() && dlssPerf.GetTestTexture(); + auto& perfMode = globals::features::upscaling.perfMode; + const bool dlssperfActive = perfMode.IsHookActive() && perfMode.GetTestTexture(); dlssOptions.outputWidth = width; - dlssOptions.outputHeight = dlssperfActive ? (uint)dlssPerf.GetDisplayScreenSize().y : (uint)state->screenSize.y; + dlssOptions.outputHeight = dlssperfActive ? (uint)perfMode.GetDisplayScreenSize().y : (uint)state->screenSize.y; // Detect HDR from kMAIN format at runtime -- VR kMAIN may be 8-bit while SE is FP16 { @@ -606,23 +606,23 @@ void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r auto screenSize = state->screenSize; auto renderSize = Util::ConvertToDynamic(screenSize); - // DLSSperf bridge: when the BSOpenVR size hook is live, state->screenSize + // PerfMode bridge: when the BSOpenVR size hook is live, state->screenSize // is polluted to RenderRes (the spoofed HMD recommended size). DLSS must // be told the TRUE DisplayRes for its output extent, otherwise NGX rejects // the evaluate as InvalidParameter (0xbad00005) because the configured // quality-scale doesn't match the actual extent ratio. The upscale also - // has to write into dlssPerf's private DisplayRes testTexture instead of + // has to write into perfMode's private DisplayRes testTexture instead of // the now-RenderRes kMAIN. auto& upscaling = globals::features::upscaling; - auto& dlssPerf = globals::features::upscaling.dlssPerf; - const bool dlssperfActive = dlssPerf.IsHookActive() && dlssPerf.GetTestTexture(); - const auto displaySize = dlssperfActive ? dlssPerf.GetDisplayScreenSize() : screenSize; + auto& perfMode = globals::features::upscaling.perfMode; + const bool dlssperfActive = perfMode.IsHookActive() && perfMode.GetTestTexture(); + const auto displaySize = dlssperfActive ? perfMode.GetDisplayScreenSize() : screenSize; // When RCAS sharpening is active, direct DLSS output to sharpenerTexture so RCAS can - // sharpen directly into kMAIN.UAV without a CopyResource round-trip. DLSSperf + // sharpen directly into kMAIN.UAV without a CopyResource round-trip. PerfMode // bypasses the sharpener entirely (writes DLSS output straight into testTexture). ID3D11Resource* colorOut = - dlssperfActive ? static_cast(dlssPerf.GetTestTexture()) : + dlssperfActive ? static_cast(perfMode.GetTestTexture()) : ((upscaling.settings.sharpnessDLSS > 0.0f && upscaling.sharpenerTexture) ? upscaling.sharpenerTexture->resource.get() : a_upscalingTexture); // VR stereo DLSS: NGX D3D11 only accepts zero-offset subrects. Non-zero offsets return diff --git a/src/Globals.cpp b/src/Globals.cpp index 8deee22e3d..68aea71e0c 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -405,15 +405,15 @@ namespace globals stl::detour_vfunc<53, ID3D11DeviceContext_ClearDepthStencilView>(a_context); } - // Scene-fade overlay Draw(30) detour — only useful when DLSSperf will + // Scene-fade overlay Draw(30) detour — only useful when PerfMode will // actually go active. The hook's thunk already early-outs on // VertexCount != 30 || !hookActive, but skipping the vtable patch - // entirely when the user has DLSSperf off avoids a foreign-interop + // entirely when the user has PerfMode off avoids a foreign-interop // surface other context-vfunc hookers could trip on. Gated by the // persisted intent, not IsHookActive(), because the hook is installed // here at D3D init time while IsHookActive() only flips true later // inside BSShaderRenderTargets::Create. - if (globals::game::isVR && globals::features::upscaling.settings.enableDLSSperf) - globals::features::upscaling.dlssPerf.InstallFadeOverlayHook(a_context); + if (globals::game::isVR && globals::features::upscaling.settings.renderAtUpscaleRes) + globals::features::upscaling.perfMode.InstallFadeOverlayHook(a_context); } } diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 5c9d4abb17..497945ae16 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -375,37 +375,42 @@ struct BSShaderRenderTargets_Create // can diff "active at boot" vs "selected". globals::features::upscaling.bootSnapshot.LatchIfNeeded(globals::features::upscaling.settings); - // DLSSperf: install the BSOpenVR render-target-size hook before the + // PerfMode: install the BSOpenVR render-target-size hook before the // engine creates its render targets. This is the only place where // BSOpenVR is guaranteed available AND we can still influence RT // allocation. Gated on user opt-in via Upscaling::Settings AND on - // DLSS actually being the resolved upscale path — a stale config can - // leave enableDLSSperf=true while the active method is FSR/TAA or - // DLSS is unsupported on this GPU, and the rest of DLSSperf only - // makes sense for the DLSS output path. + // the resolved upscale path being one that can write its output to + // a separate displayRes target (DLSS via Streamline, FSR via + // FidelityFX). TAA/NONE have no upscale output to redirect, and a + // stale config can leave renderAtUpscaleRes=true after the user + // switched methods or after DLSS becomes unsupported on this GPU. + const auto resolvedUpscaleMethod = globals::features::upscaling.GetUpscaleMethod(); + const bool methodSupportsPerfMode = + resolvedUpscaleMethod == Upscaling::UpscaleMethod::kDLSS || + resolvedUpscaleMethod == Upscaling::UpscaleMethod::kFSR; const bool dlssperfShouldRun = globals::game::isVR && - globals::features::upscaling.settings.enableDLSSperf && - globals::features::upscaling.GetUpscaleMethod() == Upscaling::UpscaleMethod::kDLSS; + globals::features::upscaling.settings.renderAtUpscaleRes && + methodSupportsPerfMode; if (dlssperfShouldRun) { - globals::features::upscaling.dlssPerf.InstallRenderTargetSizeHook(); + globals::features::upscaling.perfMode.InstallRenderTargetSizeHook(); } - // Open DLSSperf's enlarge window across the engine's Create() so + // Open PerfMode's enlarge window across the engine's Create() so // its 3 per-site thunks override props for the displayRes RTs. - auto& dlssPerf = globals::features::upscaling.dlssPerf; - dlssPerf.BeginCreateRTEnlarge(); + auto& perfMode = globals::features::upscaling.perfMode; + perfMode.BeginCreateRTEnlarge(); func(); - dlssPerf.EndCreateRTEnlarge(); + perfMode.EndCreateRTEnlarge(); globals::ReInit(); globals::state->Setup(); - // DLSSperf is not in the Feature list (it's a worker driven by the + // PerfMode is not in the Feature list (it's a worker driven by the // upscaling toggle), so SetupResources runs here directly. - if (dlssPerf.IsHookActive()) - dlssPerf.SetupResources(); + if (perfMode.IsHookActive()) + perfMode.SetupResources(); } static inline REL::Relocation func; }; @@ -901,7 +906,7 @@ namespace Hooks stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA25, 0xA25, 0xCD2)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA59, 0xA59, 0xD13)); - globals::features::upscaling.dlssPerf.InstallCreateRTThunks(); + globals::features::upscaling.perfMode.InstallCreateRTThunks(); #ifdef TRACY_ENABLE stl::write_thunk_call(REL::RelocationID(35551, 36544).address() + REL::Relocate(0x11F, 0x160)); From e34c1133c6946758f8114cb54993c8bdf496ec13 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 25 May 2026 22:31:24 -0700 Subject: [PATCH 20/24] feat(llf): expose contact-shadow settings (#43) Co-authored-by: Claude --- .../Shaders/Features/LightLimitFix.ini | 2 +- .../Shaders/LightLimitFix/LightLimitFix.hlsli | 47 ++++++---- package/Shaders/Common/SharedData.hlsli | 8 ++ package/Shaders/Lighting.hlsl | 51 +++++++--- src/Features/LightLimitFix.cpp | 93 +++++++++++++++++++ src/Features/LightLimitFix.h | 33 ++++++- 6 files changed, 201 insertions(+), 33 deletions(-) diff --git a/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini b/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini index 21a23ad267..0cb32375a0 100644 --- a/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini +++ b/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini @@ -1,5 +1,5 @@ [Info] -Version = 3-1-0 +Version = 3-2-0 [Nexus] autoupload = false diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli b/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli index 3527bef030..0df8c4e977 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli +++ b/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli @@ -47,19 +47,17 @@ namespace LightLimitFix return IsSaturated(value.x) && IsSaturated(value.y); } - // Chooses the contact-shadow noise sample coordinate. In VR we derive it - // from screenUV (which FrameBuffer::ViewToUV already returns per-eye via - // CameraProj[eye]) so both eyes sample the same noise pattern at the same - // world position — using the raw rasterized pixel position in VR makes - // each eye hash a different value, producing per-eye jitter that reads as - // flicker on contact-shadow recipients. + // Per-eye stereo-stable IGN coord. In VR we use screenUV (per-eye via + // CameraProj[eye]) instead of SV_Position so both eyes hash the same + // value at the same world pixel — SV_Position differs between eyes in + // a packed stereo buffer, producing per-eye jitter that reads as flicker + // on contact-shadow recipients. // - // BufferDim.x is the full packed stereo width (State::UpdateSharedData - // reads it from the kMAIN texture, which spans both eyes side-by-side), - // so we halve X in VR to match the per-eye pixel grid. Without the - // halving, the per-eye sample steps by ~2 pixels in X — still stereo- - // consistent, but at half the effective noise resolution. Flat keeps the - // raw pixel position to match the original implementation byte-for-byte. + // BufferDim.x is the full packed stereo width (kMAIN spans both eyes + // side-by-side), so the 0.5 factor lands us on the per-eye integer + // pixel grid — same IGN frequency as flat mode for a buffer sized + // (BufferDim.x/2, BufferDim.y). Do not drop the 0.5: that over-samples + // IGN by ~2x in X, giving a higher-frequency noise pattern, not lower. float2 GetContactShadowNoiseCoord(float2 screenPosition, float2 screenUV) { #if defined(VR) @@ -69,15 +67,28 @@ namespace LightLimitFix #endif } + // Skyrim's first-person viewmodel renders in a compressed depth range below this + // linearized value; reject occluders there since the viewmodel isn't in the world. + static const float CONTACT_SHADOW_FIRST_PERSON_MAX_DEPTH = 16.5; + + // Reference view-space depth for perspective-correct stride. At/below this depth, + // stride matches its prior view-space meaning; beyond it, stride and the depth-delta + // band scale linearly with depth so each step covers ~constant screen-space distance + // and the shadow-thickness band tracks the same screen-space extent. + static const float CONTACT_SHADOW_REFERENCE_DEPTH = 100.0; + float ContactShadows(float3 viewPosition, float noise2D, float3 lightDirectionVS, uint contactShadowSteps, uint a_eyeIndex = 0) { if (contactShadowSteps == 0) return 1.0; - float2 depthDeltaMult = float2(0.20, 0.05); - - // Extend contact shadow distance - lightDirectionVS *= 2.0; + // Perspective-correct stride: scale view-space step length with depth so each step + // covers ~constant screen-space distance. Inverse-scale the thickness/fade band so + // the depth-delta window tracks the same screen-space extent across depths. + float perspectiveScale = max(viewPosition.z, CONTACT_SHADOW_REFERENCE_DEPTH) / CONTACT_SHADOW_REFERENCE_DEPTH; + float depthDeltaThickness = SharedData::lightLimitFixSettings.ContactShadowThickness / perspectiveScale; + float depthDeltaFade = SharedData::lightLimitFixSettings.ContactShadowDepthFade / perspectiveScale; + lightDirectionVS *= SharedData::lightLimitFixSettings.ContactShadowStride * perspectiveScale; // Offset starting position with interleaved gradient noise viewPosition += lightDirectionVS * noise2D; @@ -99,8 +110,8 @@ namespace LightLimitFix // Difference between the current ray distance and the marched light float depthDelta = viewPosition.z - rayDepth; - if (rayDepth > 16.5) // First person - contactShadow = max(contactShadow, saturate(depthDelta * depthDeltaMult.x) - saturate(depthDelta * depthDeltaMult.y)); + if (rayDepth > CONTACT_SHADOW_FIRST_PERSON_MAX_DEPTH) + contactShadow = max(contactShadow, saturate(depthDelta * depthDeltaThickness) - saturate(depthDelta * depthDeltaFade)); if (contactShadow == 1.0) break; } diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 13bb01e239..b72dc1c787 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -76,9 +76,17 @@ namespace SharedData struct LightLimitFixSettings { uint EnableContactShadows; + uint ContactShadowMaxSteps; + float ContactShadowMaxDistance; + float ContactShadowStride; + float ContactShadowThickness; + float ContactShadowDepthFade; + float ContactShadowMinIntensity; uint EnableLightsVisualisation; uint LightsVisualisationMode; float pad0; + float pad1; + float pad2; uint4 ClusterSize; }; diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 55b1c21975..72ad97d490 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -2687,7 +2687,8 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float contactShadowNoise = 0.0; [branch] if (SharedData::lightLimitFixSettings.EnableContactShadows) { - contactShadowSteps = round(4.0 * (1.0 - saturate(viewPosition.z / 1024.0))); + contactShadowSteps = round(SharedData::lightLimitFixSettings.ContactShadowMaxSteps * + (1.0 - saturate(viewPosition.z / SharedData::lightLimitFixSettings.ContactShadowMaxDistance))); // The helper stays stereo-stable in VR — see // LightLimitFix::GetContactShadowNoiseCoord for the eye-buffer math. contactShadowNoise = Random::InterleavedGradientNoise( @@ -2741,19 +2742,43 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float contactShadow = 1.0; # if defined(DEFERRED) - [branch] if ( - SharedData::lightLimitFixSettings.EnableContactShadows && - !(light.lightFlags & LightLimitFix::LightFlags::Simple) && - shadowComponent != 0.0 && - lightAngle > 0.0) + // Outer guard: contactShadowSteps > 0 covers both "feature off" and "pixel past + // MaxDistance", so all per-light intensity-gate math is paid only when a raymarch + // is actually possible. Without this, the falloff math fires for every clustered + // light even in the default-off case. + [branch] if (contactShadowSteps > 0) { - // The current LightLimitFix Light struct stores positionWS only; derive view-space - // from CameraView so the raymarch direction matches viewPosition. The pre-removal - // call site referenced light.positionVS, but that field did not exist on the Light - // struct even then — the original code was commented out and unreachable. - float3 lightPositionVS = mul(FrameBuffer::CameraView[eyeIndex], float4(light.positionWS[eyeIndex].xyz, 1)).xyz; - float3 normalizedLightDirectionVS = normalize(lightPositionVS - viewPosition.xyz); - contactShadow = LightLimitFix::ContactShadows(viewPosition, contactShadowNoise, normalizedLightDirectionVS, contactShadowSteps, eyeIndex); + // Strict lights always raymarch -- skip the falloff math for them entirely. + // Clustered lights need a normalized falloff to compare against MinIntensity; + // derive it from intensityMultiplier on the non-ISL path (where it IS already + // 1 - (d/r)^2) and re-compute on the ISL path (where GetAttenuation isn't + // [0,1]-normalized, so the threshold would mean different things otherwise). + const bool isClusteredLight = lightIndex >= LightLimitFix::NumStrictLights; + bool passesIntensityGate = !isClusteredLight; + if (isClusteredLight) { +# if defined(ISL) + float falloffFactor = saturate(lightDist * light.invRadius); + passesIntensityGate = (1.0 - falloffFactor * falloffFactor) > + SharedData::lightLimitFixSettings.ContactShadowMinIntensity; +# else + passesIntensityGate = intensityMultiplier > + SharedData::lightLimitFixSettings.ContactShadowMinIntensity; +# endif + } + + [branch] if ( + !(light.lightFlags & LightLimitFix::LightFlags::Simple) && + shadowComponent != 0.0 && + lightAngle > 0.0 && + passesIntensityGate) + { + // Derive view-space position via CameraView; the Light struct only carries positionWS + // (camera-relative) so the matrix multiply here is the cheapest path until positionVS + // is added to the struct + populated CPU-side. + float3 lightPositionVS = mul(FrameBuffer::CameraView[eyeIndex], float4(light.positionWS[eyeIndex].xyz, 1)).xyz; + float3 normalizedLightDirectionVS = normalize(lightPositionVS - viewPosition.xyz); + contactShadow = LightLimitFix::ContactShadows(viewPosition, contactShadowNoise, normalizedLightDirectionVS, contactShadowSteps, eyeIndex); + } } # endif diff --git a/src/Features/LightLimitFix.cpp b/src/Features/LightLimitFix.cpp index a18f52361c..ed1f28d529 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -8,6 +8,21 @@ #include "Util.h" #include "Utils/ExternalEmittance.h" +// EnableLightsVisualisation / LightsVisualisationMode are intentionally NOT +// persisted -- they're debug toggles, and a user who enabled visualization +// to inspect something shouldn't get stuck with it on after restart. The +// _WITH_DEFAULT variant of the macro means omitted fields fall back to the +// struct's default-member-initializers on load, which is the desired reset. +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + LightLimitFix::Settings, + EnableContactShadows, + ContactShadowMaxSteps, + ContactShadowMaxDistance, + ContactShadowStride, + ContactShadowThickness, + ContactShadowDepthFade, + ContactShadowMinIntensity) + static constexpr uint CLUSTER_MAX_LIGHTS = 128; static constexpr uint MAX_LIGHTS = 1024; @@ -29,6 +44,53 @@ void LightLimitFix::DrawSettings() ImGui::Text("All point lights (strict and clustered, except simple lights) cast short screen-space shadows. Performance impact."); } + if (settings.EnableContactShadows && ImGui::TreeNode("Contact Shadow Tuning")) { + // SliderScalar with ImGuiDataType_U32 instead of `SliderInt + (int*)cast`: + // the cast violates strict aliasing (UB) and would also misinterpret any + // transient negative value inside ImGui before clamp. SliderScalar + // reads/writes the uint storage directly with explicit min/max bounds. + constexpr uint32_t kMinSteps = 1, kMaxSteps = 16; + ImGui::SliderScalar("Max Steps", ImGuiDataType_U32, &settings.ContactShadowMaxSteps, + &kMinSteps, &kMaxSteps, "%u", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Raymarch steps at zero depth. Higher = longer / more accurate contact shadows, linearly more cost.\nVR users should consider 2 to halve per-eye cost."); + } + + // AlwaysClamp on every float slider too: without it, Ctrl+Click text entry can + // land arbitrary out-of-range values in settings before GetCommonBufferData's + // boundary clamp catches them at the GPU side. + ImGui::SliderFloat("Max Distance", &settings.ContactShadowMaxDistance, 64.0f, 4096.0f, "%.0f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("View-space depth at which contact shadows fade to zero steps. Avoids paying for shadows on distant surfaces where they don't read."); + } + + ImGui::SliderFloat("Stride", &settings.ContactShadowStride, 0.5f, 8.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Per-step march length in view-space units at near depth (auto-scales linearly past ~100 units so far surfaces don't undersample). Larger = longer screen-space reach with coarser detail."); + } + + ImGui::SliderFloat("Thickness", &settings.ContactShadowThickness, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Depth-delta multiplier for shadow onset. Larger = darker contact at occluder edges."); + } + + ImGui::SliderFloat("Depth Fade", &settings.ContactShadowDepthFade, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Depth-delta multiplier for shadow falloff. Larger = shadows truncate sooner behind thick occluders."); + } + + ImGui::SliderFloat("Min Light Intensity", &settings.ContactShadowMinIntensity, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Skip contact shadows for CLUSTERED lights whose normalized distance falloff " + "`1 - (lightDist/radius)^2` at the pixel is below this threshold. " + "Strict lights are always raymarched regardless of this threshold. " + "Higher = larger perf win, may drop subtle shadows from weak lights at their reach edge."); + } + + ImGui::TreePop(); + } + /////////////////////////////// ImGui::SeparatorText("Debug"); @@ -73,8 +135,29 @@ void LightLimitFix::DrawOverlay() LightLimitFix::PerFrame LightLimitFix::GetCommonBufferData() { + // Defensive sanitization before the values hit the constant buffer. The + // sliders enforce ImGuiSliderFlags_AlwaysClamp at the UI, but Settings + // can be mutated through other paths (JSON persistence, mod overrides, + // remote-control / MCP server, or just an internal logic bug) -- a few + // of these fields will produce divisions, infinite loops, or visual + // corruption if they arrive non-finite or out-of-range, so we re-validate + // at the shader boundary rather than trusting upstream callers. + // + // std::clamp passes NaN through unchanged (every NaN comparison is false), + // so reject non-finite values explicitly first; fall back to the lower + // bound on NaN/inf to produce degraded but stable behavior. + auto sanitizeFloat = [](float v, float lo, float hi) { + return std::isfinite(v) ? std::clamp(v, lo, hi) : lo; + }; + PerFrame perFrame{}; perFrame.EnableContactShadows = settings.EnableContactShadows; + perFrame.ContactShadowMaxSteps = std::clamp(settings.ContactShadowMaxSteps, 1u, 16u); + perFrame.ContactShadowMaxDistance = sanitizeFloat(settings.ContactShadowMaxDistance, 64.0f, 4096.0f); + perFrame.ContactShadowStride = sanitizeFloat(settings.ContactShadowStride, 0.5f, 8.0f); + perFrame.ContactShadowThickness = sanitizeFloat(settings.ContactShadowThickness, 0.0f, 1.0f); + perFrame.ContactShadowDepthFade = sanitizeFloat(settings.ContactShadowDepthFade, 0.0f, 1.0f); + perFrame.ContactShadowMinIntensity = sanitizeFloat(settings.ContactShadowMinIntensity, 0.0f, 1.0f); perFrame.EnableLightsVisualisation = settings.EnableLightsVisualisation; perFrame.LightsVisualisationMode = settings.LightsVisualisationMode; std::copy(clusterSize, clusterSize + 3, perFrame.ClusterSize); @@ -186,6 +269,16 @@ void LightLimitFix::RestoreDefaultSettings() settings = {}; } +void LightLimitFix::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void LightLimitFix::SaveSettings(json& o_json) +{ + o_json = settings; +} + RE::NiNode* GetParentRoomNode(RE::NiAVObject* object) { if (object == nullptr) { diff --git a/src/Features/LightLimitFix.h b/src/Features/LightLimitFix.h index a1569f2de9..375019d2b5 100644 --- a/src/Features/LightLimitFix.h +++ b/src/Features/LightLimitFix.h @@ -95,12 +95,27 @@ struct LightLimitFix : OverlayFeature struct alignas(16) PerFrame { uint EnableContactShadows; + uint ContactShadowMaxSteps; + float ContactShadowMaxDistance; + float ContactShadowStride; + float ContactShadowThickness; + float ContactShadowDepthFade; + float ContactShadowMinIntensity; uint EnableLightsVisualisation; uint LightsVisualisationMode; - float pad0; + float pad0[3]; uint ClusterSize[4]; }; STATIC_ASSERT_ALIGNAS_16(PerFrame); + // Compile-time size lock catches CPU/GPU cbuffer layout drift. STATIC_ASSERT_ALIGNAS_16 + // only enforces the 16-byte alignment / multiple-of-16 contract that HLSL constant + // buffers require; it doesn't notice if a field is added, removed, or resized in a + // way that still happens to land on a 16-byte boundary. The shader-side mirror is + // SharedData::LightLimitFixSettings in package/Shaders/Common/SharedData.hlsli + // (embedded in the shared FeatureData cbuffer at b6), and must match this layout + // field-for-field. Update both sides when the layout changes, then bump this constant. + static_assert(sizeof(PerFrame) == 64, + "LightLimitFix::PerFrame layout drifted -- update SharedData::LightLimitFixSettings in package/Shaders/Common/SharedData.hlsli to match, then update this assert."); PerFrame GetCommonBufferData(); @@ -149,6 +164,8 @@ struct LightLimitFix : OverlayFeature virtual void SetupResources() override; virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json& o_json) override; + virtual void SaveSettings(json& o_json) override; virtual void DrawSettings() override; virtual void DrawOverlay() override; @@ -171,6 +188,20 @@ struct LightLimitFix : OverlayFeature struct Settings { bool EnableContactShadows = false; + // Max raymarch steps at zero depth; linearly ramps to 0 at MaxDistance. + uint ContactShadowMaxSteps = 4; + // View-space depth at which contact shadows fade fully off. + float ContactShadowMaxDistance = 1024.0f; + // Per-step march length in view-space units. Larger -> longer shadows, coarser detail. + float ContactShadowStride = 2.0f; + // Depth-delta multiplier for shadow onset (higher -> darker contact). + float ContactShadowThickness = 0.20f; + // Depth-delta multiplier for shadow falloff (higher -> shorter shadow). + float ContactShadowDepthFade = 0.05f; + // Skip contact shadows for CLUSTERED lights whose normalized distance falloff + // (1 - (lightDist/radius)^2) at the pixel is below this threshold. Strict + // lights always raymarch. 0 = never skip; 1 = always skip. + float ContactShadowMinIntensity = 0.25f; bool EnableLightsVisualisation = false; uint LightsVisualisationMode = 0; }; From 4e246fb216d53d7f5ccf927dd8086eefd8371bd4 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Tue, 26 May 2026 20:26:09 -0700 Subject: [PATCH 21/24] feat(slf): shadow limit fix (#35) Co-authored-by: doodlum <15017472+doodlum@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> --- CMakeLists.txt | 2 + .../Shaders/LightLimitFix/Common.hlsli | 141 +- .../Shaders/LightLimitFix/LightLimitFix.hlsli | 294 +- features/VR/Shaders/Features/VR.ini | 5 +- .../Shaders/Features/VolumetricShadows.ini | 2 +- .../VolumetricShadows/VolumetricShadows.hlsli | 5 +- package/Shaders/Common/FrameBuffer.hlsli | 3 - package/Shaders/Common/ShadowSampling.hlsli | 11 +- package/Shaders/Common/SharedData.hlsli | 9 +- package/Shaders/Effect.hlsl | 99 +- package/Shaders/Lighting.hlsl | 115 +- package/Shaders/Particle.hlsl | 46 +- package/Shaders/RunGrass.hlsl | 42 +- package/Shaders/Utility.hlsl | 4 +- src/Deferred.cpp | 32 + src/Deferred.h | 33 +- src/Features/InverseSquareLighting.cpp | 8 +- src/Features/LightLimitFix.cpp | 377 +- src/Features/LightLimitFix.h | 75 +- .../LightLimitFix/ShadowCasterManager.cpp | 5463 +++++++++++++++++ .../LightLimitFix/ShadowCasterManager.h | 666 ++ src/Features/LightLimitFix/ShadowRenderer.cpp | 353 ++ src/Features/VR.cpp | 15 + src/Features/VR.h | 4 + src/Globals.cpp | 13 + src/Globals.h | 7 + src/Utils/UI.h | 8 +- src/XSEPlugin.cpp | 2 +- vcpkg.json | 1 + 29 files changed, 7624 insertions(+), 211 deletions(-) create mode 100644 src/Features/LightLimitFix/ShadowCasterManager.cpp create mode 100644 src/Features/LightLimitFix/ShadowCasterManager.h create mode 100644 src/Features/LightLimitFix/ShadowRenderer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e7b606ade3..a070df5f27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,7 @@ find_package(pystring CONFIG REQUIRED) find_package(cppwinrt CONFIG REQUIRED) find_package(unordered_dense CONFIG REQUIRED) find_package(efsw CONFIG REQUIRED) +find_path(EXPRTK_INCLUDE_DIRS "exprtk.hpp" REQUIRED) find_package(Tracy CONFIG REQUIRED) find_package(directx-headers CONFIG REQUIRED) add_subdirectory(${CMAKE_SOURCE_DIR}/cmake/Streamline) @@ -128,6 +129,7 @@ target_include_directories( ${CLIB_UTIL_INCLUDE_DIRS} "${CMAKE_SOURCE_DIR}/package/Shaders" ${DETOURS_INCLUDE_DIRS} + ${EXPRTK_INCLUDE_DIRS} ) target_link_libraries( diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli b/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli index f2388d9ddd..4baa0e8389 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli +++ b/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli @@ -43,9 +43,144 @@ struct Light float4 positionWS[2]; uint4 roomFlags; uint lightFlags; - uint shadowLightIndex; - uint pad0; - uint pad1; + uint shadowMapIndex; + float2 pad0; }; +// --------------------------------------------------------------------------- +// LLFDEBUG visualization helpers — only compiled when the debug macro is set +// (pixel shaders only; compute shaders that include this file don't define it) +// --------------------------------------------------------------------------- +#if defined(LLFDEBUG) + +// Accumulated per-pixel debug counters filled during the light loop. +// Declare with: LLFDebugInfo di = LLFDebugInfoInit(); +// Update with: LLFDebugAccumulate(di, light, shadowComponent, shadowCoverage); +struct LLFDebugInfo +{ + uint PLShadowCount; // shadow-flagged lights seen (valid + overflow) + float MinPLShadow; // darkest shadow value (1.0 = none seen yet) + uint UnshadowedPLCount; // point/spot lights without shadow maps + uint OverflowCount; // shadow lights whose slot index exceeded ShadowMapSlots + uint FirstShadowIndex; // shadowMapIndex of first valid shadow light + bool HasFirstShadow; + uint SpotCount; // ShadowLightParam.x == 0 + uint HemiCount; // ShadowLightParam.x == 1 + uint OmniCount; // ShadowLightParam.x == 2 +}; + +LLFDebugInfo LLFDebugInfoInit() +{ + LLFDebugInfo di; + di.PLShadowCount = 0; + di.MinPLShadow = 1.0; + di.UnshadowedPLCount = 0; + di.OverflowCount = 0; + di.FirstShadowIndex = 0; + di.HasFirstShadow = false; + di.SpotCount = 0; + di.HemiCount = 0; + di.OmniCount = 0; + return di; +} + +// Call once per clustered/strict light after sampling its shadow. +// shadowCoverage should be the hasCoverage output from GetShadowLightShadow. +// shadowType should be (uint)LightLimitFix::Shadows[light.shadowMapIndex].ShadowLightParam.x +// when light.shadowMapIndex < ShadowMapSlots, or any value otherwise (it won't be read). +void LLFDebugAccumulate(inout LLFDebugInfo di, Light light, float shadowComponent, bool shadowCoverage, + uint shadowType) +{ + if (light.lightFlags & LightFlags::Shadow) { + di.PLShadowCount++; + if (shadowCoverage) + di.MinPLShadow = min(di.MinPLShadow, shadowComponent); + if (light.shadowMapIndex >= SharedData::lightLimitFixSettings.ShadowMapSlots) { + di.OverflowCount++; + } else { + if (!di.HasFirstShadow) { + di.FirstShadowIndex = light.shadowMapIndex; + di.HasFirstShadow = true; + } + if (shadowType == 0) + di.SpotCount++; + else if (shadowType == 1) + di.HemiCount++; + else + di.OmniCount++; + } + } else { + di.UnshadowedPLCount++; + } +} + +// Returns the debug visualization color for this pixel. +// Callers supply the small set of per-shader-variant values: +// mode0Color — output for mode 0 (e.g. TurboColormap(strictLightsOverflow)) +// mode1Color — output for mode 1 (e.g. TurboColormap(strictLightCount/15)) +// mode2Color — output for mode 2 (e.g. TurboColormap(clusteredCount/MAX)) +// mode3Color — output for mode 3 (e.g. float3(dirSoftShadow, dirDetailedShadow, 0)) +// lumaColor — accumulated lighting color used as luma source for mode 8 +float3 LLFDebugGetVizColor(LLFDebugInfo di, + float3 mode0Color, float3 mode1Color, float3 mode2Color, float3 mode3Color, + float3 lumaColor) +{ + uint mode = SharedData::lightLimitFixSettings.LightsVisualisationMode; + + if (mode == 0) + return mode0Color; + else if (mode == 1) + return mode1Color; + else if (mode == 2) + return mode2Color; + else if (mode == 3) + return mode3Color; + else if (mode == 4) + return Color::TurboColormap((float)di.PLShadowCount / 8.0); + else if (mode == 5) + return float3(di.MinPLShadow, di.MinPLShadow, di.MinPLShadow); + else if (mode == 6) + return Color::TurboColormap((float)di.UnshadowedPLCount / 8.0); + else if (mode == 7) { + if (di.OverflowCount > 0) + return float3(1.0, 0.0, 0.0); + uint validCount = di.PLShadowCount - di.OverflowCount; + uint slots = SharedData::lightLimitFixSettings.ShadowMapSlots; + float t; + if (validCount == 0) + t = 0.0; + else if (validCount <= 4) + t = float(validCount - 1) / 3.0 * 0.3; + else { + uint extSlots = max(slots, 6u) - 5u; + t = 0.3 + saturate(float(validCount - 5) / float(extSlots)) * 0.5; + } + return Color::TurboColormap(t); + } else if (mode == 8) { + float luma = dot(lumaColor, float3(0.2126, 0.7152, 0.0722)); + if (di.OverflowCount > 0) + return float3(1.0, 0.0, 0.0); + else if (!di.HasFirstShadow) + return luma.xxx; + float hue = frac(float(di.FirstShadowIndex) * 0.618033988); + float3 rgb = saturate(abs(frac(hue + float3(0.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - 3.0) - 1.0); + return rgb * luma; + } else { + // Mode 9 — light type visualization + if (di.OverflowCount > 0) + return float3(1.0, 0.0, 0.0); + float scale = 1.0 / 4.0; + float3 typeColor = float3( + saturate(float(di.SpotCount) * scale), + saturate(float(di.HemiCount) * scale), + saturate(float(di.OmniCount) * scale)); + bool hasShadowLights = (di.SpotCount + di.HemiCount + di.OmniCount) > 0; + if (!hasShadowLights) + typeColor = saturate(float(di.UnshadowedPLCount) * scale) * 0.35; + return typeColor; + } +} + +#endif // defined(LLFDEBUG) + #endif //__LLF_COMMON_DEPENDENCY_HLSL__ \ No newline at end of file diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli b/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli index 0df8c4e977..82d86247bc 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli +++ b/features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli @@ -4,6 +4,11 @@ namespace LightLimitFix #include "LightLimitFix/Common.hlsli" + static const float DirectionalBias = 0.5f * (0.00025f) / 3.0f; + + // Shadow Radius for PCF + static const float PCFRadius2D = 0.002; + cbuffer StrictLightData : register(b3) { uint NumStrictLights; @@ -121,10 +126,6 @@ namespace LightLimitFix bool IsLightIgnored(Light light) { - if (light.lightFlags & LightLimitFix::LightFlags::Shadow) { - return !(ShadowBitMask & (1 << light.shadowLightIndex)); - } - bool lightIgnored = false; if ((light.lightFlags & LightFlags::PortalStrict) && RoomIndex >= 0) { lightIgnored = true; @@ -142,4 +143,289 @@ namespace LightLimitFix } return lightIgnored; } + + struct ShadowLightData + { + column_major float4x4 ShadowProj; + column_major float4x4 InvShadowProj; + float4 ShadowLightParam; + }; + + // t100/t101 are reserved for Grass Collision (its Collision texture binds at + // t100, and shaders like RunGrass include both features). LLF shadow data + // uses t102/t103 to avoid the collision; keep the C++ PSSetShaderResources + // slots in src/Features/LightLimitFix/ShadowRenderer.cpp in sync. + StructuredBuffer Shadows : register(t102); + Texture2DArray ShadowMaps : register(t103); + Texture2DArray DirectionalShadowCascades : register(t99); + + // engineMaskShadow: the engine's pre-rendered 4-cascade shadow mask sample + // at this pixel (TexShadowMaskSampler.Load(int3(Position.xy, 0)).x). LLF's + // DirectionalShadowLightData carries only cascades 0/1 (ShadowProj[2] / + // EndSplitDistances.xy); past EndSplitDistances.y we have no LLF data and + // must fall through to the engine mask. Returning 1.0 there leaves distant + // pixels fully lit -- visible as global scene brightening with shadows + // disappearing past a depth that varies with camera position. + float GetDirectionalShadow(float3 worldPosition, float3 worldPositionWS, float2x2 rotationMatrix, uint eyeIndex, float engineMaskShadow) + { + DirectionalShadowLightData shadowLightData = DirectionalShadowLights[0]; + + float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(worldPosition, eyeIndex)); + + // Past cascade 1 -- defer to the engine's 4-cascade mask. + if (shadowMapDepth > shadowLightData.EndSplitDistances.y) + return engineMaskShadow; + + // Blend from LLF PCF deep in cascade 1 toward the engine mask as we + // approach cascade 1's far edge, avoiding a hard discontinuity at the + // boundary where LLF stops and engine sampling takes over. + // + // Previous formula used `dot(worldPosition, worldPosition) / + // EndSplitDistances.y` -- dimensionally wrong (length^2 / length) + // AND inverted (close pixels got engineMaskShadow, far got LLF). + // Because `worldPosition` is camera-relative in Skyrim's vertex + // output, that produced a visible ~sqrt(EndSplitDistances.y)-radius + // ring around the camera that moved with the player -- a clear + // HMD-tracked artifact in VR. Switching to linear `shadowMapDepth` + // and reversing the blend direction makes the handoff a smooth + // world-anchored transition at the cascade boundary. + float fadeFactor = smoothstep(shadowLightData.EndSplitDistances.y * 0.8, + shadowLightData.EndSplitDistances.y, + shadowMapDepth); + + // Compute cascade blend factor + float cascadeSelect = smoothstep(shadowLightData.StartSplitDistances.y, shadowLightData.EndSplitDistances.x, shadowMapDepth); + + // Determine which cascade(s) to sample + uint primaryCascade = cascadeSelect; + bool needsBlending = (cascadeSelect > 0.0) && (cascadeSelect < 1.0); + + // Transform ray to light space for primary cascade + float3 positionLS = mul(shadowLightData.ShadowProj[primaryCascade], float4(worldPositionWS, 1)).xyz; + positionLS.z -= DirectionalBias; + + // Sample primary cascade + float shadow = 0.0; + + [unroll] for (int i = 0; i < 8; i++) + { + float2 sampleOffset = mul(Random::SpiralSampleOffsets8[i], rotationMatrix); + float2 sampleUV = positionLS.xy + sampleOffset * PCFRadius2D; + shadow += dot(float4(DirectionalShadowCascades.GatherRed(LinearSampler, float3(saturate(sampleUV), primaryCascade)) > positionLS.z), 0.25); + } + + shadow /= 8.0; + + // Blend with secondary cascade if needed + [branch] if (needsBlending) + { + uint secondaryCascade = 1 - primaryCascade; + + positionLS = mul(shadowLightData.ShadowProj[secondaryCascade], float4(worldPositionWS, 1)).xyz; + positionLS.z -= DirectionalBias; + + float shadowBlend = 0.0; + + [unroll] for (int i = 0; i < 8; i++) + { + float2 sampleOffset = mul(Random::SpiralSampleOffsets8[i], rotationMatrix); + float2 sampleUV = positionLS.xy + sampleOffset * PCFRadius2D; + shadowBlend += dot(float4(DirectionalShadowCascades.GatherRed(LinearSampler, float3(saturate(sampleUV), secondaryCascade)) > positionLS.z), 0.25); + } + + shadowBlend /= 8.0; + + shadow = lerp(shadow, shadowBlend, cascadeSelect); + } + + // Within cascade 1's far edge, blend LLF's PCF toward the engine + // mask instead of fading to fully-lit -- avoids a hard brightness + // discontinuity at the cascade boundary. + shadow = lerp(shadow, engineMaskShadow, fadeFactor); + + // Focus shadows: high-resolution actor shadows the engine renders to + // kSHADOWMAPS slices [kFocusShadowBaseSlotIndex .. +FocusShadowCount). + // Each focus matrix projects worldPositionWS into the actor's clip + // space; pixels outside [0,1] UV or [0,1] depth aren't covered and + // contribute no occlusion. Combine via min() so any occluding actor + // wins. Without this, the player's own shadow vanishes when LLF is + // on (the cascade has it at lower resolution; focus made it visible). + // + // Guards: + // - SharedData::lightLimitFixSettings.ShadowMapSlots bounds the slice + // index so we never sample past the texture's allocated array size + // (a real concern with ShadowLightCount < 8 in extended mode). + // - focusClip.w > EPSILON_DIVISION avoids div-by-zero NaN when the focus + // matrix hasn't been populated yet (first frame of a scene load + // before the engine's RenderShadowmaps has run the focus loop). + [unroll] for (uint fi = 0; fi < 4; fi++) + { + [branch] if (fi >= shadowLightData.FocusShadowCount) break; + const uint focusSlice = 4 + fi; // kFocusShadowBaseSlotIndex + [branch] if (focusSlice >= SharedData::lightLimitFixSettings.ShadowMapSlots) break; + float4 focusClip = mul(shadowLightData.FocusShadowProj[fi], float4(worldPositionWS, 1)); + [branch] if (focusClip.w <= EPSILON_DIVISION) continue; + focusClip.xyz /= focusClip.w; + float2 focusUV = focusClip.xy * 0.5 + 0.5; + [branch] if (all(focusUV >= 0.0) && all(focusUV <= 1.0) && focusClip.z >= 0.0 && focusClip.z <= 1.0) + { + float focusDepth = focusClip.z - DirectionalBias; + float focusVis = 0.0; + [unroll] for (int fs = 0; fs < 8; fs++) + { + float2 fsOffset = mul(Random::SpiralSampleOffsets8[fs], rotationMatrix); + float2 fsUV = focusUV + fsOffset * PCFRadius2D; + focusVis += dot(float4(ShadowMaps.GatherRed(LinearSampler, float3(saturate(fsUV), focusSlice)) > focusDepth), 0.25); + } + focusVis /= 8.0; + shadow = min(shadow, focusVis); + // Fully occluded -- remaining focus actors can only multiply + // by zero, so skip their 8-tap GatherRed work on this pixel. + [branch] if (shadow <= 0.0) break; + } + } + + return shadow; + } + + // Convenience overload: callers without TexShadowMaskSampler bound + // (e.g. Particle.hlsl) get the lit-fallback behaviour (1.0) past + // cascade 1, matching the pre-engine-mask behaviour for those paths. + float GetDirectionalShadow(float3 worldPosition, float3 worldPositionWS, float2x2 rotationMatrix, uint eyeIndex) + { + return GetDirectionalShadow(worldPosition, worldPositionWS, rotationMatrix, eyeIndex, 1.0); + } + + float GetDirectionalShadow(float3 worldPosition, float3 worldPositionWS, float2x2 rotationMatrix) + { + return GetDirectionalShadow(worldPosition, worldPositionWS, rotationMatrix, 0, 1.0); + } + + float SampleShadowGather(uint shadowIndex, float2 uv, float receiverDepth) + { + float4 samples = ShadowMaps.GatherRed(LinearSampler, float3(uv, shadowIndex)); + return dot(float4(samples > receiverDepth), 0.25); + } + + float GetSpotlightShadow(ShadowLightData shadowLightData, uint shadowIndex, float4 positionLS, float2x2 rotationMatrix) + { + positionLS.xyz /= positionLS.w; + positionLS.xy = positionLS.xy * 0.5 + 0.5; + positionLS.z -= shadowLightData.ShadowLightParam.z; + + float shadow = 0.0; + + [unroll] for (int i = 0; i < 8; i++) + { + float2 sampleOffset = mul(Random::SpiralSampleOffsets8[i], rotationMatrix); + float2 sampleUV = positionLS.xy + sampleOffset * PCFRadius2D; + shadow += SampleShadowGather(shadowIndex, sampleUV, positionLS.z); + } + + return shadow / 8.0; + } + + // PCF sample around a paraboloid UV. + // isDualParaboloid = true : the slice contains two stacked paraboloids + // (omni: upper in y∈[0,0.5], lower in y∈[0.5,1]). + // Clamp PCF samples to the originating half so we + // don't bleed across the seam. + // isDualParaboloid = false : the slice contains a single paraboloid filling + // the whole y∈[0,1] (hemi). No clamping needed — + // the entire slice is valid shadow data. + float SampleParaboloidShadow(uint shadowIndex, float2 sampleUV, float depth, float2x2 rotationMatrix, bool isDualParaboloid) + { + float shadow = 0.0; + + [unroll] for (int i = 0; i < 8; i++) + { + float2 offset = mul(Random::SpiralSampleOffsets8[i], rotationMatrix) * PCFRadius2D; + float2 uv = sampleUV + offset; + + if (isDualParaboloid) { + // Clamp PCF samples to the originating paraboloid half. + uv.y = (sampleUV.y >= 0.5) ? max(uv.y, 0.5) : min(uv.y, 0.5); + } + + shadow += SampleShadowGather(shadowIndex, uv, depth); + } + + return shadow / 8.0; + } + + float GetOmnidirectionalShadow(ShadowLightData shadowLightData, uint shadowIndex, float4 positionLS, float2x2 rotationMatrix) + { + // ShadowLightParam.x: + // 0 = spot/frustum (handled in GetShadowLightShadow before reaching here) + // 1 = hemisphere — engine renders ONE paraboloid filling the slice + // 2 = omnidirectional (dual paraboloid) — TWO paraboloids stacked in slice + // + // Verified against kSHADOWMAPS slice contents in RenderDoc: hemi slices show + // a single continuous depth gradient across y=0.5 with no seam, while omni + // slices show two distinct paraboloid renderings stacked. Treating hemi + // like omni applies a Y-axis compression / mirror that visibly distorts + // (the "inverted or rotated 90°" symptom). + const bool isOmni = (shadowLightData.ShadowLightParam.x == 2); + + bool lowerHalf = positionLS.z < 0; + + // Hemi only renders the +Z paraboloid; behind the light has no shadow data. + // Returning 1.0 (fully lit) lets the light's own attenuation handle falloff + // for points the engine never wrote shadow data for. + if (!isOmni && lowerHalf) + return 1.0; + + positionLS.xyz /= positionLS.w; + + float3 posOffset = lowerHalf ? float3(0, 0, -1) : float3(0, 0, 1); + float3 lightDirection = normalize(normalize(positionLS.xyz) + posOffset); + float2 sampleUV = lightDirection.xy / lightDirection.z * 0.5 + 0.5; + + // Y compression only applies to omni's dual layout. Hemi fills the whole + // slice so its sampleUV.y stays in [0, 1] directly. + if (isOmni) + sampleUV.y = lowerHalf ? 1.0 - 0.5 * sampleUV.y : 0.5 * sampleUV.y; + + float depth = saturate(length(positionLS.xyz) / shadowLightData.ShadowLightParam.y); + depth -= shadowLightData.ShadowLightParam.z; + + return SampleParaboloidShadow(shadowIndex, sampleUV, depth, rotationMatrix, isOmni); + } + + // Single-assignment of hasCoverage at function entry keeps FXC's flow + // analyser quiet: prior versions used an early-return overflow guard + // that wrote hasCoverage on two paths, which tripped X4000 "potentially + // uninitialized" warnings at the post-merge point across 360 permutations + // (both `out` and `inout` signatures hit the same false positive). + // + // Overflow handling: `shadowIndex >= ShadowMapSlots` can occur transiently + // when a light was promoted to shadow on a frame where the texture-array + // allocation hadn't extended to cover it yet. StructuredBuffer reads beyond + // declared bounds return zero per the D3D11 spec, so the Shadows[shadowIndex] + // read is safe -- it falls into the `ShadowLightParam.y == 0` branch below + // and returns 1.0. The `hasCoverage` flag tells the caller whether the + // sample was real, so suppression still works correctly upstream. + float GetShadowLightShadow(uint shadowIndex, float3 worldPositionWS, float2x2 rotationMatrix, out bool hasCoverage) + { + hasCoverage = shadowIndex < SharedData::lightLimitFixSettings.ShadowMapSlots; + + ShadowLightData shadowLightData = Shadows[shadowIndex]; + + [flatten] if (shadowLightData.ShadowLightParam.y == 0) return 1.0; + [flatten] if (shadowLightData.ShadowLightParam.y < 0) return 0.0; + + float4 positionLS = mul(shadowLightData.ShadowProj, float4(worldPositionWS, 1)); + + [branch] if (shadowLightData.ShadowLightParam.x == 0) + { + float shadowBaseVisibility = GetSpotlightShadow(shadowLightData, shadowIndex, positionLS, rotationMatrix); + positionLS.xyz /= positionLS.w; + + float spotFalloff = saturate(1.0 - dot(positionLS.xy, positionLS.xy)); + + return shadowBaseVisibility * spotFalloff; + } + + return GetOmnidirectionalShadow(shadowLightData, shadowIndex, positionLS, rotationMatrix); + } } diff --git a/features/VR/Shaders/Features/VR.ini b/features/VR/Shaders/Features/VR.ini index 0bc0292971..9a09bb7e17 100644 --- a/features/VR/Shaders/Features/VR.ini +++ b/features/VR/Shaders/Features/VR.ini @@ -1,5 +1,2 @@ [Info] -Version = 1-1-0 - -[Nexus] -autoupload = false +Version = 1-1-1 diff --git a/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini b/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini index e9d66d302c..14a4f48fa2 100644 --- a/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini +++ b/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini @@ -1,5 +1,5 @@ [Info] -Version = 2-0-1 +Version = 2-1-0 [Nexus] autoupload = false diff --git a/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli b/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli index cdfb339ba2..9f002d4723 100644 --- a/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli +++ b/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli @@ -130,7 +130,7 @@ namespace VolumetricShadows return ComputeVSM(moments, positionLS.z); } - float GetVSMShadow2D(float3 position, uint eyeIndex, out float detailedShadow) + float GetVSMShadow2D(float3 position, float3 positionWS, uint eyeIndex, out float detailedShadow) { DirectionalShadowLightData directionalShadowLightData = DirectionalShadowLights[0]; @@ -145,9 +145,6 @@ namespace VolumetricShadows // Reduce over distance float fade = saturate(shadowMapDepth / directionalShadowLightData.EndSplitDistances.y); - // Cascade projections are world-space; position comes in camera-relative. - float3 positionWS = position + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; - // Compute cascade blend factor with smoothstep float cascadeSelect = saturate((shadowMapDepth - directionalShadowLightData.StartSplitDistances.y) / (directionalShadowLightData.EndSplitDistances.x - directionalShadowLightData.StartSplitDistances.y)); diff --git a/package/Shaders/Common/FrameBuffer.hlsli b/package/Shaders/Common/FrameBuffer.hlsli index 68f0f90371..775d41f2d6 100644 --- a/package/Shaders/Common/FrameBuffer.hlsli +++ b/package/Shaders/Common/FrameBuffer.hlsli @@ -85,9 +85,6 @@ namespace FrameBuffer return clamp(screenPositionDR, minValue, maxValue); } - // Projects a world-space (camera-relative) point into NDC using the eye's CameraViewProj - // and returns the post-perspective z (NDC depth). Combine with SharedData::GetScreenDepth - // to get a linear view-space distance suitable for cascade-split comparisons. float GetShadowDepth(float3 positionWS, uint eyeIndex) { float4 positionCS = mul(FrameBuffer::CameraViewProj[eyeIndex], float4(positionWS, 1)); diff --git a/package/Shaders/Common/ShadowSampling.hlsli b/package/Shaders/Common/ShadowSampling.hlsli index 70e7143f54..2dcba97856 100644 --- a/package/Shaders/Common/ShadowSampling.hlsli +++ b/package/Shaders/Common/ShadowSampling.hlsli @@ -30,6 +30,12 @@ struct DirectionalShadowLightData column_major float4x4 InvShadowProj[2]; float2 EndSplitDistances; float2 StartSplitDistances; + // Focus shadow projections (per FocusShadowActor, max 4). Sample at + // kSHADOWMAPS slice (4 + i) using FocusShadowProj[i]; only entries with + // index < FocusShadowCount are valid. + column_major float4x4 FocusShadowProj[4]; + uint FocusShadowCount; + uint3 _pad0; }; StructuredBuffer DirectionalShadowLights : register(t98); @@ -112,10 +118,7 @@ namespace ShadowSampling surfaceShadow *= vsmSurfaceShadow; return worldShadow * shadow; } -#else - return worldShadow; #endif - return worldShadow; } @@ -127,7 +130,7 @@ namespace ShadowSampling } #if defined(VOLUMETRIC_SHADOWS) - float shadow = VolumetricShadows::GetVSMShadow2D(worldPosition, eyeIndex, detailedShadow); + float shadow = VolumetricShadows::GetVSMShadow2D(worldPosition, worldPosition + FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex, detailedShadow); return shadow; #else detailedShadow = 1.0; diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index b72dc1c787..8530f295d6 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -82,12 +82,13 @@ namespace SharedData float ContactShadowThickness; float ContactShadowDepthFade; float ContactShadowMinIntensity; + uint ShadowMapSlots; // total shadow map texture-array capacity + // Cluster config (computed) + uint4 ClusterSize; + // Debug (last) uint EnableLightsVisualisation; uint LightsVisualisationMode; - float pad0; - float pad1; - float pad2; - uint4 ClusterSize; + uint2 pad0; }; struct WetnessEffectsSettings diff --git a/package/Shaders/Effect.hlsl b/package/Shaders/Effect.hlsl index fcd9dfb2e8..1ea8713bd3 100644 --- a/package/Shaders/Effect.hlsl +++ b/package/Shaders/Effect.hlsl @@ -503,14 +503,6 @@ cbuffer PerGeometry : register(b2) # endif }; -# if defined(LIGHT_LIMIT_FIX) -# include "LightLimitFix/LightLimitFix.hlsli" -# endif - -# if defined(ISL) && defined(LIGHT_LIMIT_FIX) -# include "InverseSquareLighting/InverseSquareLighting.hlsli" -# endif - # define LinearSampler SampBaseSampler # if defined(SKYLIGHTING) @@ -528,6 +520,14 @@ cbuffer PerGeometry : register(b2) # include "Common/ShadowSampling.hlsli" +# if defined(LIGHT_LIMIT_FIX) +# include "LightLimitFix/LightLimitFix.hlsli" +# endif + +# if defined(ISL) && defined(LIGHT_LIMIT_FIX) +# include "InverseSquareLighting/InverseSquareLighting.hlsli" +# endif + # if defined(LIGHTING) float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPosition, uint eyeIndex, inout float shadowVariance) { @@ -598,7 +598,7 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo return color; } # else -float3 GetLightingShadow(float3 color, float3 worldPosition, float2 screenPosition, float depth, uint eyeIndex, inout float shadowVariance) +float3 GetLightingShadow(float3 color, float3 worldPosition, float2 screenPosition, float depth, uint eyeIndex, inout float shadowVariance, float noise) { float3 dirColor; float3 ambientColor; @@ -612,12 +612,6 @@ float3 GetLightingShadow(float3 color, float3 worldPosition, float2 screenPositi static const uint sampleCount = 8; static const float rcpSampleCount = 1.0 / float(sampleCount); - float noise = Random::InterleavedGradientNoise(screenPosition, SharedData::FrameCount); - float noiseTransform = noise * 2.0 - 1.0; - float2 rotation; - sincos(Math::TAU * noise, rotation.y, rotation.x); - float2x2 rotationMatrix = float2x2(rotation.x, rotation.y, -rotation.y, rotation.x); - // Enough for sky statics float maxDistance = max(0, SharedData::GetScreenDepth(depth)); float viewRayLength = 2048.0; @@ -711,41 +705,76 @@ PS_OUTPUT main(PS_INPUT input) float3 propertyColor = Color::Effect(PropertyColor.xyz); float shadowVariance = 1.0; + float screenNoise = Random::InterleavedGradientNoise(input.Position.xy, SharedData::FrameCount); + + float2 rotation; + sincos(Math::TAU * screenNoise, rotation.y, rotation.x); + float2x2 rotationMatrix = float2x2(rotation.x, rotation.y, -rotation.y, rotation.x); + # if defined(LIGHTING) propertyColor = GetLightingColor(input.MSPosition.xyz, input.WorldPosition.xyz, input.Position.xy, eyeIndex, shadowVariance); # if defined(LIGHT_LIMIT_FIX) - uint lightCount = 0; - float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); bool inWorld = Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld; + uint numClusteredLights = 0; + uint lightOffset = 0; uint clusterIndex = 0; - if (inWorld && LightLimitFix::GetClusterIndex(screenUV, viewPosition.z, clusterIndex)) { - lightCount = LightLimitFix::lightGrid[clusterIndex].lightCount; - uint lightOffset = LightLimitFix::lightGrid[clusterIndex].offset; - [loop] for (uint i = 0; i < lightCount; i++) - { - uint clusteredLightIndex = LightLimitFix::lightList[lightOffset + i]; - LightLimitFix::Light light = LightLimitFix::lights[clusteredLightIndex]; - if (LightLimitFix::IsLightIgnored(light) || light.lightFlags & LightLimitFix::LightFlags::Shadow) { + uint numStrictLights = 0; + if (inWorld) { + // Gate strict lights behind inWorld too -- they live in + // LightLimitFix::StrictLights which is populated from world-space + // CB data. Including them on non-world passes (UI overlays, blood + // splatter on screen-space surfaces, etc.) leaks world lighting + // into effects that shouldn't be lit by point/spot lights at all. + // Clustered lights are already inWorld-gated below; strict needs + // the same treatment for symmetry. + numStrictLights = LightLimitFix::NumStrictLights; + if (LightLimitFix::GetClusterIndex(screenUV, viewPosition.z, clusterIndex)) { + numClusteredLights = LightLimitFix::lightGrid[clusterIndex].lightCount; + lightOffset = LightLimitFix::lightGrid[clusterIndex].offset; + } + } + uint totalLightCount = numStrictLights + numClusteredLights; + + [loop] for (uint i = 0; i < totalLightCount; i++) + { + LightLimitFix::Light light; + if (i < numStrictLights) { + light = LightLimitFix::StrictLights[i]; + } else { + uint clusteredLightIndex = LightLimitFix::lightList[lightOffset + (i - numStrictLights)]; + light = LightLimitFix::lights[clusteredLightIndex]; + if (LightLimitFix::IsLightIgnored(light)) continue; - } - float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WorldPosition.xyz; - float lightDist = length(lightDirection); + } + + float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WorldPosition.xyz; + float lightDist = length(lightDirection); # if defined(ISL) - float intensityMultiplier = InverseSquareLighting::GetAttenuation(lightDist, light); + float intensityMultiplier = InverseSquareLighting::GetAttenuation(lightDist, light); + if (intensityMultiplier < 1e-5) + continue; # else - float intensityFactor = saturate(lightDist / light.radius); - float intensityMultiplier = 1 - intensityFactor * intensityFactor; + float intensityFactor = saturate(lightDist / light.radius); + if (intensityFactor == 1) + continue; + float intensityMultiplier = 1 - intensityFactor * intensityFactor; # endif - const bool isPointLightLinear = light.lightFlags & LightLimitFix::LightFlags::Linear; - float3 lightColor = Color::PointLight(light.color.xyz, isPointLightLinear) * intensityMultiplier * 0.5 * light.fade * Color::EffectLightingMult(); - propertyColor += lightColor; + float shadowMul = 1.0; + if (inWorld && (light.lightFlags & LightLimitFix::LightFlags::Shadow)) { + bool shadowCoverage = false; + float3 worldPositionWS = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + shadowMul = LightLimitFix::GetShadowLightShadow(light.shadowMapIndex, worldPositionWS, rotationMatrix, shadowCoverage); } + + const bool isPointLightLinear = light.lightFlags & LightLimitFix::LightFlags::Linear; + float3 lightColor = Color::PointLight(light.color.xyz, isPointLightLinear) * intensityMultiplier * 0.5 * light.fade * Color::EffectLightingMult() * shadowMul; + propertyColor += lightColor; } # endif @@ -840,7 +869,7 @@ PS_OUTPUT main(PS_INPUT input) # if !defined(LIGHTING) && defined(VC) && defined(TEXCOORD) && defined(NORMALS) && defined(TEXTURE) && defined(FALLOFF) && defined(SOFT) if (Permutation::PixelShaderDescriptor & Permutation::EffectFlags::GrayscaleToAlpha && lightingInfluence == 1.0) - lightColor = GetLightingShadow(lightColor, input.WorldPosition.xyz, input.Position.xy, depth, eyeIndex, shadowVariance); + lightColor = GetLightingShadow(lightColor, input.WorldPosition.xyz, input.Position.xy, depth, eyeIndex, shadowVariance, screenNoise); # endif lightColor = Color::EffectMult(lightColor); diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 72ad97d490..4270c648f2 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -449,8 +449,6 @@ SamplerState SampLandLodBlend2Sampler : register(s15); SamplerState SampLandLodNoiseSampler : register(s15); # endif -SamplerState SampShadowMaskSampler : register(s14); - # if defined(LANDSCAPE) Texture2D TexColorSampler : register(t0); @@ -896,14 +894,6 @@ float GetSnowParameterY(float texProjTmp, float alpha) # include "ScreenSpaceShadows/ScreenSpaceShadows.hlsli" # endif -# if defined(LIGHT_LIMIT_FIX) -# include "LightLimitFix/LightLimitFix.hlsli" -# endif - -# if defined(ISL) && defined(LIGHT_LIMIT_FIX) -# include "InverseSquareLighting/InverseSquareLighting.hlsli" -# endif - # if defined(TREE_ANIM) # undef WETNESS_EFFECTS # endif @@ -941,6 +931,14 @@ float GetSnowParameterY(float texProjTmp, float alpha) # include "Common/ShadowSampling.hlsli" +# if defined(LIGHT_LIMIT_FIX) +# include "LightLimitFix/LightLimitFix.hlsli" +# endif + +# if defined(ISL) && defined(LIGHT_LIMIT_FIX) +# include "InverseSquareLighting/InverseSquareLighting.hlsli" +# endif + # if defined(IBL) # include "IBL/IBL.hlsli" # endif @@ -2023,15 +2021,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // SPARKLE # endif // defined (MODELSPACENORMALS) && !defined (SKINNED) - float2 baseShadowUV = 1.0.xx; - float4 shadowColor = 1.0; - if ((Permutation::PixelShaderDescriptor & Permutation::LightingFlags::DefShadow) && ((Permutation::PixelShaderDescriptor & Permutation::LightingFlags::ShadowDir) || inWorld) || numShadowLights > 0) { - baseShadowUV = input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy; - float2 adjustedShadowUV = baseShadowUV * VPOSOffset.xy + VPOSOffset.zw; - float2 shadowUV = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(adjustedShadowUV); - shadowColor = TexShadowMaskSampler.Sample(SampShadowMaskSampler, shadowUV); - } - float projectedMaterialWeight = 0; float projWeight = 0; @@ -2508,20 +2497,53 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float dirDetailedShadow = 1.0; - if ((Permutation::PixelShaderDescriptor & Permutation::LightingFlags::DefShadow) && (Permutation::PixelShaderDescriptor & Permutation::LightingFlags::ShadowDir)) { - dirDetailedShadow *= shadowColor.x; + float2 rotation; + sincos(Math::TAU * screenNoise, rotation.y, rotation.x); + float2x2 rotationMatrix = float2x2(rotation.x, rotation.y, -rotation.y, rotation.x); + float3 worldPositionWS = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + + // Engine pre-renders the 4-cascade directional shadow into a screen-space + // mask at t14. LLF samples only cascades 0/1; we pass the engine mask + // through so LLF::GetDirectionalShadow can fall back to it past + // EndSplitDistances.y instead of returning fully-lit. + float4 shadowColor = (Permutation::PixelShaderDescriptor & Permutation::LightingFlags::DefShadow) ? TexShadowMaskSampler.Load(int3(input.Position.xy, 0)) : 1.0; + + // Mirrors #2319 for VOLUMETRIC_SHADOWS: use HasDirectionalShadows() (= !IsInterior() || + // InteriorSun::IsActive) instead of the bare !InInterior gate, so Interior Sun cells + // reach the LLF cascade + engine-mask sampling path. Without this, interior scenes + // with active Interior Sun render with zero directional contribution and no sun shadow. + if (inWorld && !inReflection && ShadowSampling::HasDirectionalShadows()) { +# if !defined(LOD) + // On non-deferred passes, use the cheaper VSM shadows if available +# if defined(LIGHT_LIMIT_FIX) && (defined(DEFERRED) || !defined(VOLUMETRIC_SHADOWS)) + dirDetailedShadow = LightLimitFix::GetDirectionalShadow(input.WorldPosition.xyz, worldPositionWS, rotationMatrix, eyeIndex, + (Permutation::PixelShaderDescriptor & Permutation::LightingFlags::ShadowDir) ? shadowColor.x : 1.0); +# elif !defined(LIGHT_LIMIT_FIX) + dirDetailedShadow = (Permutation::PixelShaderDescriptor & Permutation::LightingFlags::ShadowDir) ? shadowColor.x : 1.0; +# endif // LIGHT_LIMIT_FIX + +# if defined(VOLUMETRIC_SHADOWS) + float vsmDetailedShadow = 1.0; + dirSoftShadow = VolumetricShadows::GetVSMShadow2D(input.WorldPosition.xyz, worldPositionWS, eyeIndex, vsmDetailedShadow); + dirSoftShadow = max(dirSoftShadow, dirDetailedShadow); + +# if !defined(LIGHT_LIMIT_FIX) + if (!(Permutation::PixelShaderDescriptor & Permutation::LightingFlags::ShadowDir)) + dirDetailedShadow = vsmDetailedShadow; +# elif !(defined(DEFERRED)) + dirDetailedShadow = vsmDetailedShadow; +# endif -# if !defined(VOLUMETRIC_SHADOWS) +# else dirSoftShadow = dirDetailedShadow; +# endif // VOLUMETRIC_SHADOWS # endif - } else { - dirDetailedShadow = dirVSMDetailedShadow; - } # if defined(SCREEN_SPACE_SHADOWS) && defined(DEFERRED) - if (!SharedData::InInterior && dirLightAngle >= 0.0) - dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.Position.xyz, screenUV, screenNoise, eyeIndex); -# endif + if (!SharedData::InInterior && dirLightAngle >= 0.0) + dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.Position.xyz, screenUV, screenNoise, eyeIndex); +# endif // SCREEN_SPACE_SHADOWS + } # if defined(EMAT) && (defined(SKINNED) || !defined(MODELSPACENORMALS)) [branch] if (inWorld && SharedData::extendedMaterialSettings.EnableShadows) @@ -2678,6 +2700,10 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lightOffset = LightLimitFix::lightGrid[clusterIndex].offset; } +# if defined(LLFDEBUG) + LightLimitFix::LLFDebugInfo llfDebug = LightLimitFix::LLFDebugInfoInit(); +# endif + # if defined(DEFERRED) // Contact-shadow setup, gated on the runtime toggle so we don't pay the // noise hash + step-count math for every pixel when the feature is off @@ -2729,13 +2755,22 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float lightShadow = 1.0; float shadowComponent = 1.0; - if (Permutation::PixelShaderDescriptor & Permutation::LightingFlags::DefShadow) { + bool shadowCoverage = false; + if (inWorld && !inReflection) { if (light.lightFlags & LightLimitFix::LightFlags::Shadow) { - shadowComponent = shadowColor[light.shadowLightIndex]; + shadowComponent = LightLimitFix::GetShadowLightShadow(light.shadowMapIndex, worldPositionWS, rotationMatrix, shadowCoverage); lightShadow *= shadowComponent; } } +# if defined(LLFDEBUG) + uint llfShadowType = (light.lightFlags & LightLimitFix::LightFlags::Shadow && + light.shadowMapIndex < SharedData::lightLimitFixSettings.ShadowMapSlots) ? + (uint)LightLimitFix::Shadows[light.shadowMapIndex].ShadowLightParam.x : + 0; + LightLimitFix::LLFDebugAccumulate(llfDebug, light, shadowComponent, shadowCoverage, llfShadowType); +# endif + float3 normalizedLightDirection = normalize(lightDirection); float lightAngle = dot(worldNormal.xyz, normalizedLightDirection.xyz); @@ -2862,7 +2897,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if !defined(LANDSCAPE) if (Permutation::PixelShaderDescriptor & Permutation::LightingFlags::CharacterLight) { float charLightMul = saturate(dot(viewDirection, worldNormal.xyz)) * CharacterLightParams.x + CharacterLightParams.y * saturate(dot(float2(0.164398998, -0.986393988), worldNormal.yz)); - float charLightColor = min(CharacterLightParams.w, max(0, CharacterLightParams.z * TexCharacterLightProjNoiseSampler.Sample(SampCharacterLightProjNoiseSampler, baseShadowUV).x)); + float charLightColor = min(CharacterLightParams.w, max(0, CharacterLightParams.z * TexCharacterLightProjNoiseSampler.Sample(SampCharacterLightProjNoiseSampler, screenUV).x)); diffuseColor += (charLightMul * charLightColor).xxx; } # endif @@ -3313,15 +3348,13 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(LIGHT_LIMIT_FIX) && defined(LLFDEBUG) if (SharedData::lightLimitFixSettings.EnableLightsVisualisation) { - if (SharedData::lightLimitFixSettings.LightsVisualisationMode == 0) { - psout.Diffuse.xyz = Color::TurboColormap(LightLimitFix::NumStrictLights >= 7.0); - } else if (SharedData::lightLimitFixSettings.LightsVisualisationMode == 1) { - psout.Diffuse.xyz = Color::TurboColormap((float)LightLimitFix::NumStrictLights / 15.0); - } else if (SharedData::lightLimitFixSettings.LightsVisualisationMode == 2) { - psout.Diffuse.xyz = Color::TurboColormap((float)numClusteredLights / MAX_CLUSTER_LIGHTS); - } else { - psout.Diffuse.xyz = shadowColor.xyz; - } + psout.Diffuse.xyz = LightLimitFix::LLFDebugGetVizColor( + llfDebug, + Color::TurboColormap(LightLimitFix::NumStrictLights >= 7.0), + Color::TurboColormap((float)LightLimitFix::NumStrictLights / 15.0), + Color::TurboColormap((float)numClusteredLights / MAX_CLUSTER_LIGHTS), + float3(dirSoftShadow, dirDetailedShadow, 0.0), + color.xyz); baseColor.xyz = 0.0; } else { psout.Diffuse.xyz = color.xyz; @@ -3388,7 +3421,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif # if !defined(HDR_OUTPUT) // Do not apply gamma correction before we pass to ISHDR. - if ((!inWorld && !inReflection) && SharedData::linearLightingSettings.enableLinearLighting && !(Permutation::PixelShaderDescriptor & Permutation::LightingFlags::DefShadow)) { + if ((!inWorld && !inReflection) && SharedData::linearLightingSettings.enableLinearLighting) { psout.Diffuse.xyz = Color::LinearToSrgb(psout.Diffuse.xyz); } # endif diff --git a/package/Shaders/Particle.hlsl b/package/Shaders/Particle.hlsl index c35f68861c..9a15e7ebc0 100644 --- a/package/Shaders/Particle.hlsl +++ b/package/Shaders/Particle.hlsl @@ -215,14 +215,6 @@ struct PS_OUTPUT #ifdef PSHADER -# if defined(LIGHT_LIMIT_FIX) -# include "LightLimitFix/LightLimitFix.hlsli" -# endif - -# if defined(ISL) && defined(LIGHT_LIMIT_FIX) -# include "InverseSquareLighting/InverseSquareLighting.hlsli" -# endif - SamplerState SampSourceTexture : register(s0); # if defined(GRAYSCALE_TO_COLOR) || defined(GRAYSCALE_TO_ALPHA) SamplerState SampGrayscaleTexture : register(s1); @@ -248,8 +240,17 @@ cbuffer PerGeometry : register(b2) }; # define LinearSampler SampSourceTexture + # include "Common/ShadowSampling.hlsli" +# if defined(LIGHT_LIMIT_FIX) +# include "LightLimitFix/LightLimitFix.hlsli" +# endif + +# if defined(ISL) && defined(LIGHT_LIMIT_FIX) +# include "InverseSquareLighting/InverseSquareLighting.hlsli" +# endif + PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; @@ -295,11 +296,34 @@ PS_OUTPUT main(PS_INPUT input) positionWS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionWS); positionWS.xyz = positionWS.xyz / positionWS.w; - float unusedDetailedShadow; - float3 dirLightColor = SharedData::DirLightColor.xyz * ShadowSampling::GetLightingShadow(positionWS.xyz, eyeIndex, unusedDetailedShadow); + float screenNoise = Random::InterleavedGradientNoise(input.Position.xy, SharedData::FrameCount); + float2 rotation; + sincos(Math::TAU * screenNoise, rotation.y, rotation.x); + float2x2 rotationMatrix = float2x2(rotation.x, rotation.y, -rotation.y, rotation.x); + + float3 worldPositionWS = positionWS.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + + float dirSoftShadow = 1.0; + float dirDetailedShadow = 1.0; + + float3 dirLightColor = SharedData::DirLightColor.xyz; + + // Mirrors #2319 / Lighting.hlsl: HasDirectionalShadows() admits Interior Sun cells + // to the directional shadow sampling path. + if (ShadowSampling::HasDirectionalShadows()) { + // Use the cheaper VSM shadows if available +# if defined(VOLUMETRIC_SHADOWS) + dirSoftShadow = VolumetricShadows::GetVSMShadow2D(positionWS.xyz, worldPositionWS, eyeIndex, dirDetailedShadow); +# elif defined(LIGHT_LIMIT_FIX) + dirDetailedShadow = LightLimitFix::GetDirectionalShadow(positionWS.xyz, worldPositionWS, rotationMatrix, eyeIndex); +# endif + } + float3 ambientColor = max(0, SharedData::GetAmbient(float3(0, 0, 1))); - propertyColor += dirLightColor; + // Exactly one of dirSoftShadow / dirDetailedShadow is < 1.0 (the two paths + // above are mutually exclusive); the other stays at its default 1.0. + propertyColor += dirLightColor * dirSoftShadow * dirDetailedShadow; propertyColor += ambientColor; # if defined(LIGHT_LIMIT_FIX) diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index e7ef06f9f8..c7ada6ef78 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -420,6 +420,12 @@ cbuffer AlphaTestRefCB : register(b11) # include "ScreenSpaceShadows/ScreenSpaceShadows.hlsli" # endif +// ShadowSampling.hlsli must be included before LightLimitFix.hlsli because +// LightLimitFix.hlsli references DirectionalShadowLightData / DirectionalShadowLights +// which are declared in ShadowSampling.hlsli. +# define LinearSampler SampBaseSampler +# include "Common/ShadowSampling.hlsli" + # if defined(LIGHT_LIMIT_FIX) # include "LightLimitFix/LightLimitFix.hlsli" # endif @@ -446,10 +452,6 @@ cbuffer AlphaTestRefCB : register(b11) # include "ExponentialHeightFog/ExponentialHeightFog.hlsli" # endif -# define LinearSampler SampBaseSampler - -# include "Common/ShadowSampling.hlsli" - # ifdef GRASS_LIGHTING # if defined(TRUE_PBR) @@ -602,11 +604,13 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float dirDetailedShadow = 1.0; - if (!SharedData::InInterior) + // HasDirectionalShadows() admits Interior Sun cells; mirrors the + // same swap in Lighting.hlsl / Particle.hlsl. + if (ShadowSampling::HasDirectionalShadows()) dirDetailedShadow *= shadowColor.x; # if defined(SCREEN_SPACE_SHADOWS) - if (!SharedData::InInterior && dirLightAngle >= 0.0) + if (ShadowSampling::HasDirectionalShadows() && dirLightAngle >= 0.0) dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.HPosition.xyz, screenUV, screenNoise, eyeIndex); # endif // SCREEN_SPACE_SHADOWS @@ -686,8 +690,17 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float lightShadow = 1.0; float shadowComponent = 1.0; + bool shadowCoverage = false; if (light.lightFlags & LightLimitFix::LightFlags::Shadow) { - shadowComponent = shadowColor[light.shadowLightIndex]; + // Per-pixel PCF rotation + world-space position for new SLF shadow API. + // Replaces the old shadowColor[light.shadowLightIndex] vanilla path which + // referenced a now-renamed Light field (shadowLightIndex -> shadowMapIndex) + // and bypassed the SLF shadow infrastructure. + float2 rotation; + sincos(Math::TAU * screenNoise, rotation.y, rotation.x); + float2x2 rotationMatrix = float2x2(rotation.x, rotation.y, -rotation.y, rotation.x); + float3 worldPositionWS = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + shadowComponent = LightLimitFix::GetShadowLightShadow(light.shadowMapIndex, worldPositionWS, rotationMatrix, shadowCoverage); lightShadow *= shadowComponent; } @@ -837,11 +850,13 @@ PS_OUTPUT main(PS_INPUT input) float dirDetailedShadow = 1.0; - if (!SharedData::InInterior) + // HasDirectionalShadows() admits Interior Sun cells; mirrors the + // same swap in Lighting.hlsl / Particle.hlsl. + if (ShadowSampling::HasDirectionalShadows()) dirDetailedShadow = shadowColor.x; # if defined(SCREEN_SPACE_SHADOWS) - if (!SharedData::InInterior) + if (ShadowSampling::HasDirectionalShadows()) dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.HPosition.xyz, screenUV, screenNoise, eyeIndex); # endif // SCREEN_SPACE_SHADOWS @@ -882,8 +897,15 @@ PS_OUTPUT main(PS_INPUT input) float lightShadow = 1.0; float shadowComponent = 1.0; + bool shadowCoverage = false; if (light.lightFlags & LightLimitFix::LightFlags::Shadow) { - shadowComponent = shadowColor[light.shadowLightIndex]; + // Per-pixel PCF rotation + world-space position for new SLF shadow API. + // Replaces the old shadowColor[light.shadowLightIndex] vanilla path. + float2 rotation; + sincos(Math::TAU * screenNoise, rotation.y, rotation.x); + float2x2 rotationMatrix = float2x2(rotation.x, rotation.y, -rotation.y, rotation.x); + float3 worldPositionWS = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + shadowComponent = LightLimitFix::GetShadowLightShadow(light.shadowMapIndex, worldPositionWS, rotationMatrix, shadowCoverage); lightShadow *= shadowComponent; } diff --git a/package/Shaders/Utility.hlsl b/package/Shaders/Utility.hlsl index 95522d81c9..356ab59b07 100644 --- a/package/Shaders/Utility.hlsl +++ b/package/Shaders/Utility.hlsl @@ -538,7 +538,7 @@ PS_OUTPUT main(PS_INPUT input) # elif SHADOWFILTER == 1 shadowVisibility = TexShadowMapSamplerComp.SampleCmpLevelZero(SampShadowMapSamplerComp, float3(positionLS.xy, cascadeIndex), positionLS.z).x; # elif SHADOWFILTER == 3 - shadowVisibility = SampleShadowPCF(TexShadowMapSamplerComp, SampShadowMapSamplerComp, positionLS.xy, cascadeIndex, positionLS.z, rotationMatrix, ShadowSampleParam.z); + shadowVisibility = SampleShadowPCF(TexShadowMapSamplerComp, SampShadowMapSamplerComp, positionLS.xy, cascadeIndex, positionLS.z, rotationMatrix, ShadowSampleParam.z * 0.5); # endif if (cascadeIndex < 1 && StartSplitDistances.y < shadowMapDepth) { @@ -554,7 +554,7 @@ PS_OUTPUT main(PS_INPUT input) # elif SHADOWFILTER == 1 cascade1ShadowVisibility = TexShadowMapSamplerComp.SampleCmpLevelZero(SampShadowMapSamplerComp, float3(cascade1PositionLS.xy, 1), cascade1PositionLS.z).x; # elif SHADOWFILTER == 3 - cascade1ShadowVisibility = SampleShadowPCF(TexShadowMapSamplerComp, SampShadowMapSamplerComp, cascade1PositionLS.xy, 1, cascade1PositionLS.z, rotationMatrix, ShadowSampleParam.z); + cascade1ShadowVisibility = SampleShadowPCF(TexShadowMapSamplerComp, SampShadowMapSamplerComp, cascade1PositionLS.xy, 1, cascade1PositionLS.z, rotationMatrix, ShadowSampleParam.z * 0.5); # endif float cascade1BlendFactor = smoothstep(0, 1, (shadowMapDepth - StartSplitDistances.y) / (EndSplitDistances.x - StartSplitDistances.y)); diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 870d1354e3..ac6d831dc0 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -8,6 +8,7 @@ #include "Features/DynamicCubemaps.h" #include "Features/IBL.h" +#include "Features/LightLimitFix/ShadowCasterManager.h" #include "Features/ScreenSpaceGI.h" #include "Features/Skylighting.h" #include "Features/SubsurfaceScattering.h" @@ -564,6 +565,33 @@ void Deferred::SetShadowCascadeParameters(T& lightData, DirectionalShadowLightDa DirectX::XMMATRIX invProj = DirectX::XMMatrixInverse(nullptr, proj); DirectX::XMStoreFloat4x4(&dd.InvShadowProj[i], invProj); } + + // Focus shadow matrices (one per active focus actor; engine writes them + // to focusShadowmapDescriptors[i].lightTransform during its per-cascade + // render). The shader samples kSHADOWMAPS slice (4 + i) for each entry + // to recover the player/NPC high-resolution shadow. + const auto focusCount = std::min( + static_cast(std::size(lightData.focusShadowmapDescriptors)), + static_cast(std::size(dd.FocusShadowProj))); + // Preserve descriptor->slice correspondence by writing FocusShadowProj[i] + // for descriptor[i] -- the LLF shader samples kSHADOWMAPS slice (4 + fi) + // using fi as the matrix index, so packing densely (e.g. via a separate + // counter) would pair matrix N with the wrong shadow slice when there are + // disabled holes between descriptors. Disabled descriptors leave their + // FocusShadowProj slot at the default-zero matrix; the shader's existing + // `focusClip.w <= EPSILON_DIVISION` guard treats that as "no actor in + // this slice" and skips sampling. FocusShadowCount is the upper iteration + // bound (last enabled index + 1) so the shader still exits early when + // trailing slots are empty. + dd.FocusShadowCount = 0; + for (uint32_t i = 0; i < focusCount; i++) { + const auto& desc = lightData.focusShadowmapDescriptors[i]; + if (!desc.isEnabled) + continue; // descriptor unused this frame -- leave FocusShadowProj[i] at zero + auto proj = DirectX::XMLoadFloat4x4(reinterpret_cast(&desc.lightTransform)); + DirectX::XMStoreFloat4x4(&dd.FocusShadowProj[i], proj); + dd.FocusShadowCount = i + 1; + } } void Deferred::CopyShadowLightData() @@ -598,6 +626,10 @@ void Deferred::CopyShadowLightData() ID3D11ShaderResourceView* srv = directionalShadowLights->srv.get(); context->PSSetShaderResources(98, 1, &srv); + + // t99: cascade depth array used by LightLimitFix::GetDirectionalShadow for PCF sampling. + ID3D11ShaderResourceView* cascadeSRV = globals::game::renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGET_DEPTHSTENCIL::kSHADOWMAPS_ESRAM].depthSRV; + context->PSSetShaderResources(99, 1, &cascadeSRV); } void Deferred::ClearShaderCache() diff --git a/src/Deferred.h b/src/Deferred.h index c95f28ed2a..841ce082e0 100644 --- a/src/Deferred.h +++ b/src/Deferred.h @@ -4,6 +4,7 @@ #include "Buffer.h" #include "RE/B/BSShadowDirectionalLight.h" +#include "RE/B/BSShadowLight.h" #define ALBEDO RE::RENDER_TARGETS::kINDIRECT #define SPECULAR RE::RENDER_TARGETS::kINDIRECT_DOWNSCALED @@ -27,8 +28,34 @@ class Deferred float4x4 InvShadowProj[2]; float2 EndSplitDistances; float2 StartSplitDistances; + // Focus shadow projection matrices, written by SCM each frame for the + // active FocusShadowActors (player + tracked NPCs, max 4). Each matrix + // projects world-space to the focus shadow's clip space; HLSL samples + // kSHADOWMAPS slice (4 + i) for matrix i to get the per-actor high-res + // shadow. FocusShadowCount in [0..4]; entries beyond it are ignored. + float4x4 FocusShadowProj[4]; + uint FocusShadowCount; + uint pad0[3]; }; STATIC_ASSERT_ALIGNAS_16(DirectionalShadowLightData); + // Size guard catches silent layout drift between this and the HLSL mirror + // in ShadowSampling.hlsli; any size change here corrupts every uploaded + // directional shadow record so we want it to fail at compile time. + // 8 float4x4 (Shadow + Inv + Focus) + 2 float4 (splits + FocusCount/pad). + static_assert(sizeof(DirectionalShadowLightData) == 8 * sizeof(float4x4) + 2 * sizeof(float4), + "DirectionalShadowLightData layout drifted from ShadowSampling.hlsli mirror"); + + struct alignas(16) ShadowLightData + { + float4x4 ShadowProj; + float4x4 InvShadowProj; + float4 ShadowParam; + }; + + STATIC_ASSERT_ALIGNAS_16(ShadowLightData); + // Same guard for the per-slot point/spot shadow record (LightLimitFix.hlsli). + static_assert(sizeof(ShadowLightData) == 2 * sizeof(float4x4) + sizeof(float4), + "ShadowLightData layout drifted from LightLimitFix.hlsli mirror"); void SetupResources(); void ReflectionsPrepasses(); @@ -43,9 +70,6 @@ class Deferred void ClearShaderCache(); - ID3D11ComputeShader* GetComputeMainComposite(); - ID3D11ComputeShader* GetComputeMainCompositeInterior(); - // Reads directional shadow parameters from BSShadowDirectionalLight and uploads // to the structured buffer at t98 (DirectionalShadowLightData — cascade splits + // world-to-shadow projections). Called during EarlyPrepasses once shadow maps @@ -53,6 +77,9 @@ class Deferred // constant-buffer fields into a UAV. void CopyShadowLightData(); + ID3D11ComputeShader* GetComputeMainComposite(); + ID3D11ComputeShader* GetComputeMainCompositeInterior(); + ID3D11BlendState* deferredBlendStates[7][2][13][2]; ID3D11BlendState* forwardBlendStates[7][2][13][2]; diff --git a/src/Features/InverseSquareLighting.cpp b/src/Features/InverseSquareLighting.cpp index a602079470..ab16e2ebcd 100644 --- a/src/Features/InverseSquareLighting.cpp +++ b/src/Features/InverseSquareLighting.cpp @@ -66,7 +66,13 @@ void InverseSquareLighting::ProcessLight(LightLimitFix::LightData& light, RE::BS if (bsLight->pointLight && isInvSq) { const float intensity = runtimeData->fade * 4; - light.radius = CalculateRadius(intensity, bsLight->IsShadowLight(), runtimeData->cutoffOverride, runtimeData->size); + // Use the type-based helper rather than the virtual IsShadowLight(): + // SCM's Hook_IsShadowLight reports false for shadow lights converted + // to normal-light overflow handling (issue #2121 #3). If we followed + // that hook here the cutoff would flip from DefaultShadowCasterCutoff + // (0.022) to DefaultCutoff (0.05) when a light is converted, shrinking + // its effective radius by ~33% and visibly reducing its lit area. + light.radius = CalculateRadius(intensity, ShadowCasterManager::IsShadowLightType(bsLight), runtimeData->cutoffOverride, runtimeData->size); runtimeData->radius = light.radius; light.invRadius = 1.f / light.radius; light.fadeZone = 1.f / (light.radius * std::clamp(FadeZoneBase * light.invRadius, 0.f, 1.f)); diff --git a/src/Features/LightLimitFix.cpp b/src/Features/LightLimitFix.cpp index ed1f28d529..98a6e136bb 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -1,18 +1,20 @@ #include "LightLimitFix.h" #include "InverseSquareLighting.h" #include "LinearLighting.h" +#include "Utils/UI.h" +#include "Deferred.h" #include "Menu/ThemeManager.h" #include "Shadercache.h" #include "State.h" #include "Util.h" #include "Utils/ExternalEmittance.h" -// EnableLightsVisualisation / LightsVisualisationMode are intentionally NOT -// persisted -- they're debug toggles, and a user who enabled visualization -// to inspect something shouldn't get stuck with it on after restart. The -// _WITH_DEFAULT variant of the macro means omitted fields fall back to the -// struct's default-member-initializers on load, which is the desired reset. +// Debug visualisation state (EnableLightsVisualisation / LightsVisualisationMode) +// is intentionally NOT in Settings -- it lives as instance members on the +// LightLimitFix class so it resets per session and can't accidentally end +// up in a shipped JSON config that would force every load to compile the +// heavier LLFDEBUG shader permutation. NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( LightLimitFix::Settings, EnableContactShadows, @@ -21,21 +23,38 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ContactShadowStride, ContactShadowThickness, ContactShadowDepthFade, - ContactShadowMinIntensity) - -static constexpr uint CLUSTER_MAX_LIGHTS = 128; -static constexpr uint MAX_LIGHTS = 1024; + ContactShadowMinIntensity, + ShowShadowOverlay, + ShadowSettings) void LightLimitFix::DrawSettings() { auto shaderCache = globals::shaderCache; - if (ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text(std::format("Clustered Light Count : {}", lightCount).c_str()); + ShadowCasterManager::DrawSettings(settings.ShadowSettings); - ImGui::TreePop(); + // ---- Active Shadow Casters -------------------------------------- + // One cohesive section: overlay toggle, then ALL the stats grouped + // together (summary + scheduler stats + budget verdict), then the + // table below. Same layout as the overlay so testers see the same + // thing in both views with the stats above the (potentially long) + // table -- no scrolling required to find the headline numbers. + ImGui::SeparatorText("Shadow Limit Fix -- Active Casters"); + + ImGui::Checkbox("Show Shadow Overlay", &settings.ShowShadowOverlay); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Pop out an always-visible overlay window with the shadow caster table.\n" + "Without this, the overlay only appears when a light is suppressed\n" + "or a visualisation mode is active. Enable to access the table's\n" + "debug controls (cycle button, solo, Shift+hover pulse) any time."); } + ShadowCasterManager::DrawShadowSummary(lightCount, MAX_LIGHTS, shadowUnshadowedLightCount); + ShadowCasterManager::DrawShadowSchedulerStats(); + ImGui::Separator(); + ShadowCasterManager::DrawShadowLightTable(true, false); + /////////////////////////////// ImGui::SeparatorText("Shadows"); @@ -95,25 +114,45 @@ void LightLimitFix::DrawSettings() ImGui::SeparatorText("Debug"); if (ImGui::TreeNode("Light Limit Visualization")) { - ImGui::Checkbox("Enable Lights Visualisation", &settings.EnableLightsVisualisation); + ImGui::Checkbox("Enable Lights Visualisation", &EnableLightsVisualisation); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Enables visualization of the light limit\n"); } { - static const char* comboOptions[] = { "Light Limit", "Strict Lights Count", "Clustered Lights Count", "Shadow Mask" }; - ImGui::Combo("Lights Visualisation Mode", (int*)&settings.LightsVisualisationMode, comboOptions, 4); + static const char* comboOptions[] = { + "Light Limit", + "Strict Lights Count", + "Clustered Lights Count", + "Shadow Mask", + "Shadow Light Count", + "Point Light Shadow Factor", + "Unshadowed Point Lights", + "Shadow Caster Density", + "Shadow Slot Index Color", + "Light Type Visualization", + }; + // Round-trip through int instead of `(int*)&uint` to avoid strict-aliasing UB + // (ImGui has no ComboScalar). Clamp on the way in defends against any stale + // persisted value that might still exist from older builds. + int visMode = std::clamp(static_cast(LightsVisualisationMode), + 0, IM_ARRAYSIZE(comboOptions) - 1); + ImGui::Combo("Lights Visualisation Mode", &visMode, comboOptions, IM_ARRAYSIZE(comboOptions)); + LightsVisualisationMode = static_cast(visMode); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( - " - Visualise the light limit. Red when the \"strict\" light limit is reached (portal-strict lights).\n" - " - Visualise the number of strict lights.\n" - " - Visualise the number of clustered lights.\n" - " - Visualize the Shadow Mask.\n"); + "Light Limit: Red when the strict light limit is reached (>=7 portal-strict lights).\n" + "\n" + "Strict Lights Count: Heatmap of portal-strict lights per pixel (blue=0, red=15).\n" + "\n" + "Clustered Lights Count: Heatmap of dynamic lights in each screen tile (blue=0, red=128)."); + ShadowCasterManager::DrawVisualisationTooltipShadowModes(); } } - currentEnableLightsVisualisation = settings.EnableLightsVisualisation; + + currentEnableLightsVisualisation = EnableLightsVisualisation; if (previousEnableLightsVisualisation != currentEnableLightsVisualisation) { - globals::state->SetDefines(settings.EnableLightsVisualisation ? "LLFDEBUG" : ""); + globals::state->SetDefines(EnableLightsVisualisation ? "LLFDEBUG" : ""); shaderCache->Clear(RE::BSShader::Type::Lighting); previousEnableLightsVisualisation = currentEnableLightsVisualisation; } @@ -122,17 +161,6 @@ void LightLimitFix::DrawSettings() } } -void LightLimitFix::DrawOverlay() -{ - if (!settings.EnableLightsVisualisation) - return; - const float pos = ThemeManager::Constants::OVERLAY_WINDOW_POSITION * Util::GetUIScale(); - ImGui::SetNextWindowPos(ImVec2(pos, pos), ImGuiCond_Always); - ImGui::Begin("##LLFDebug", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "DEBUG FEATURE - LIGHT LIMIT VISUALISATION ENABLED"); - ImGui::End(); -} - LightLimitFix::PerFrame LightLimitFix::GetCommonBufferData() { // Defensive sanitization before the values hit the constant buffer. The @@ -158,9 +186,10 @@ LightLimitFix::PerFrame LightLimitFix::GetCommonBufferData() perFrame.ContactShadowThickness = sanitizeFloat(settings.ContactShadowThickness, 0.0f, 1.0f); perFrame.ContactShadowDepthFade = sanitizeFloat(settings.ContactShadowDepthFade, 0.0f, 1.0f); perFrame.ContactShadowMinIntensity = sanitizeFloat(settings.ContactShadowMinIntensity, 0.0f, 1.0f); - perFrame.EnableLightsVisualisation = settings.EnableLightsVisualisation; - perFrame.LightsVisualisationMode = settings.LightsVisualisationMode; + perFrame.ShadowMapSlots = ShadowCasterManager::GetInstalledSlotCount(); std::copy(clusterSize, clusterSize + 3, perFrame.ClusterSize); + perFrame.EnableLightsVisualisation = EnableLightsVisualisation; + perFrame.LightsVisualisationMode = LightsVisualisationMode; return perFrame; } @@ -272,11 +301,18 @@ void LightLimitFix::RestoreDefaultSettings() void LightLimitFix::LoadSettings(json& o_json) { settings = o_json; + // iShadowMapResolution:Display is owned by Skyrim's INI, not our JSON. + ShadowCasterManager::LoadINISettings(); + + // Raise saved values below the current floor so older configs migrate. + if (settings.ShadowSettings.MaxRedrawPerFrame < ShadowCasterManager::Settings::kMinMaxRedrawPerFrame) + settings.ShadowSettings.MaxRedrawPerFrame = ShadowCasterManager::Settings::kMinMaxRedrawPerFrame; } void LightLimitFix::SaveSettings(json& o_json) { o_json = settings; + ShadowCasterManager::SaveINISettings(); } RE::NiNode* GetParentRoomNode(RE::NiAVObject* object) @@ -354,23 +390,33 @@ void LightLimitFix::BSLightingShader_SetupGeometry_GeometrySetupConstantPointLig if (i < a_pass->numShadowLights) { auto* shadowLight = static_cast(bsLight); - GET_INSTANCE_MEMBER(maskIndex, shadowLight); - light.shadowMaskIndex = maskIndex; - light.lightFlags.set(LightFlags::Shadow); + // Use SCM's stable container-slot index instead of reading the + // live `shadowmapDescriptors[0].shadowmapIndex`. The descriptor + // field can be corrupted mid-frame by ReturnShadowmaps() (called + // via Hook_DisableColorMask) after ScheduleShadowCasters fixed + // it but before this strict-light setup runs -- a stale-but-in + // -range index would still pass an upper-bound check yet point + // strict-light shader sampling at the wrong kSHADOWMAPS slice. + // GetShadowSlot reads from the SCM's own pool (s_lights, set in + // ScheduleShadowCasters and never touched by ReturnShadowmaps), + // so it stays consistent with CopyShadowLightData and + // UpdateLights, which also key off it. Returns -1 for the sun + // or inactive lights; both cases skip setting the Shadow flag. + const int32_t slot = ShadowCasterManager::GetShadowSlot(shadowLight); + if (slot >= 0 && static_cast(slot) < ShadowCasterManager::GetInstalledSlotCount()) { + light.shadowMapIndex = static_cast(slot); + light.lightFlags.set(LightFlags::Shadow); + } } strictLightDataTemp.StrictLights[writeIdx++] = light; } strictLightDataTemp.NumStrictLights = writeIdx; - for (uint32_t i = 0; i < a_pass->numShadowLights; i++) { - auto bsLight = a_pass->sceneLights[i + 1]; - if (!bsLight) - continue; - auto* shadowLight = static_cast(bsLight); - GET_INSTANCE_MEMBER(maskIndex, shadowLight); - strictLightDataTemp.ShadowBitMask |= (1u << maskIndex); - } + // Don't reinstate a build loop for strictLightDataTemp.ShadowBitMask: + // no shader reads it (the IsLightIgnored bit-mask branch was replaced by + // per-light shadowMapIndex sampling). The field stays for cbuffer ABI + // stability and is zero-initialised above. } void LightLimitFix::BSLightingShader_SetupGeometry_After(RE::BSRenderPass*) @@ -389,14 +435,12 @@ void LightLimitFix::BSLightingShader_SetupGeometry_After(RE::BSRenderPass*) const auto isEmpty = strictLightDataTemp.NumStrictLights == 0; const bool isWorld = accumulator->GetRuntimeData().activeShadowSceneNode == shadowSceneNode; const auto roomIndex = strictLightDataTemp.RoomIndex; - const auto shadowBitMask = strictLightDataTemp.ShadowBitMask; - if (!isEmpty || (isEmpty && !wasEmpty) || isWorld != wasWorld || previousRoomIndex != roomIndex || shadowBitMask != previousShadowBitMask) { + if (!isEmpty || (isEmpty && !wasEmpty) || isWorld != wasWorld || previousRoomIndex != roomIndex) { strictLightDataCB->Update(strictLightDataTemp); wasEmpty = isEmpty; wasWorld = isWorld; previousRoomIndex = roomIndex; - previousShadowBitMask = shadowBitMask; } if (frameChecker.IsNewFrame()) { @@ -432,6 +476,7 @@ void LightLimitFix::Prepass() ZoneScoped; TracyD3D11Zone(globals::state->tracyCtx, "LightLimitFix Prepass"); state->BeginPerfEvent("LightLimitFix Prepass"); + ShadowCasterManager::Update(settings.ShadowSettings, globals::game::smState->shadowSceneNode[0], nullptr); UpdateLights(); ID3D11ShaderResourceView* views[3]{}; @@ -445,7 +490,7 @@ void LightLimitFix::Prepass() bool LightLimitFix::IsValidLight(RE::BSLight* a_light) { - return a_light && !a_light->light->GetFlags().any(RE::NiAVObject::Flag::kHidden); + return a_light && a_light->light && a_light->light.get() && !a_light->light->GetFlags().any(RE::NiAVObject::Flag::kHidden); } bool LightLimitFix::IsGlobalLight(RE::BSLight* a_light) @@ -456,6 +501,8 @@ bool LightLimitFix::IsGlobalLight(RE::BSLight* a_light) void LightLimitFix::PostPostLoad() { Hooks::Install(); + ShadowCasterManager::Init(settings.ShadowSettings); + ShadowCasterManager::Install(settings.ShadowSettings); } void LightLimitFix::DataLoaded() @@ -484,6 +531,7 @@ void LightLimitFix::ClearShaderCache() void LightLimitFix::UpdateLights() { + ZoneScopedN("LLF::UpdateLights"); auto smState = globals::game::smState; auto& isl = globals::features::inverseSquareLighting; @@ -514,9 +562,27 @@ void LightLimitFix::UpdateLights() light.roomFlags.SetBit(roomIndex, 1); }; + // Hover-pulse helper: if the table has a hovered row matching this light's + // pointer, replace the cluster colour with a magenta pulse so the user can + // see which light a row corresponds to in 3D. Pulse cycles ~once per second + // using ImGui::GetTime() for a stable visual signal. + auto applyDebugOverrides = [](LightData& light, const void* lightPtr) { + auto hoverKey = ShadowCasterManager::GetHoveredLight(); + if (hoverKey != 0 && reinterpret_cast(lightPtr) == hoverKey) { + float t = 0.5f + 0.5f * std::sin(static_cast(ImGui::GetTime()) * 6.2831853f); + light.color = { 1.0f, 0.0f, 1.0f }; // magenta + light.fade = 4.0f + t * 4.0f; // pulsed intensity + } + }; + auto addLight = [&](const RE::NiPointer& e) { if (auto bsLight = e.get()) { if (auto niLight = bsLight->light.get()) { + // IsSuppressed includes solo (every key except the soloed one is + // implicitly suppressed). This filters every non-shadow cluster + // light through the user's debug overrides. + if (ShadowCasterManager::IsSuppressed(reinterpret_cast(bsLight))) + return; if (IsValidLight(bsLight)) { auto& runtimeData = niLight->GetLightRuntimeData(); @@ -546,33 +612,153 @@ void LightLimitFix::UpdateLights() light.lightFlags.set(LightFlags::PortalStrict); } - if (bsLight->IsShadowLight()) { - auto* shadowLight = static_cast(bsLight); - GET_INSTANCE_MEMBER(maskIndex, shadowLight); - light.shadowMaskIndex = maskIndex; - light.lightFlags.set(LightFlags::Shadow); + SetLightPosition(light, niLight->world.translate); + + applyDebugOverrides(light, bsLight); + + if ((light.color.x + light.color.y + light.color.z) * light.fade > 1e-4 && light.radius > 1e-4) { + lightsData.push_back(light); } + } + } + } + }; - // Check for inactive shadow light - if (light.shadowMaskIndex != 255) { - SetLightPosition(light, niLight->world.translate); + auto addShadowLight = [&](RE::BSShadowLight* shadowLight, bool castsShadow, uint32_t shadowSlot = 0) { + if (IsValidLight(shadowLight)) { + if (auto niLight = shadowLight->light.get()) { + auto& runtimeData = niLight->GetLightRuntimeData(); + + LightData light{}; + light.color = { runtimeData.diffuse.red, runtimeData.diffuse.green, runtimeData.diffuse.blue }; + light.lightFlags = std::bit_cast(runtimeData.ambient.red); + + if (isl.loaded) { + isl.ProcessLight(light, shadowLight, niLight); + } else { + light.radius = runtimeData.radius.x; + // light.color *= runtimeData.fade; + light.fade = runtimeData.fade; + } - if ((light.color.x + light.color.y + light.color.z) * light.fade > 1e-4 && light.radius > 1e-4) { - lightsData.push_back(light); - } + light.fade *= shadowLight->lodDimmer; + + if (!IsGlobalLight(shadowLight)) { + // List of BSMultiBoundRooms affected by a light + for (const auto& roomPtr : shadowLight->rooms) { + addRoom(roomPtr, light); } + // List of BSPortals affected by a light + for (const auto& portalPtr : shadowLight->portals) { + addRoom(portalPtr->portalSharedNode.get(), light); + } + light.lightFlags.set(LightFlags::PortalStrict); + } + + if (castsShadow) { + // Use the caller-provided stable slot index from s_lights + // rather than shadowmapDescriptors[0].shadowmapIndex, which + // can drift relative to our scheduler-assigned slot when + // ReturnShadowmaps fires between scheduling and lighting. + light.shadowMapIndex = shadowSlot; + light.lightFlags.set(LightFlags::Shadow); + } + + SetLightPosition(light, niLight->world.translate); + + applyDebugOverrides(light, shadowLight); + + if ((light.color.x + light.color.y + light.color.z) * light.fade > 1e-4 && light.radius > 1e-4) { + lightsData.push_back(light); } } } }; + // Single pass over shadowLightsAccum: + // - Builds shadowLightPtrs so activeLights below skips lights already added here. + // - Calls addShadowLight for each logical light. + // EnableLight calls both GameEnableLight (→ activeLights) and + // GameSetShadowCasterSlot (→ shadowLightsAccum) for redrawn lights, so without + // the skip below each redrawn shadow light would be added twice. + // + // Static reuses the bucket array across frames -- a local set would + // destroy + recreate its buckets every frame, defeating the reserve(). + // Dense layout avoids the per-insert node allocation a std::unordered_set + // would incur. Upper bound is the configured kSHADOWMAPS slot count; + // shadowLightsAccum is sized to hold at most that many distinct point/spot + // lights (sun occupies one logical entry but no kSHADOWMAPS slice, hence + // the belt-and-braces +1). + static ankerl::unordered_dense::set shadowLightPtrs; + shadowLightPtrs.clear(); + shadowLightPtrs.reserve(ShadowCasterManager::GetInstalledSlotCount() + 1); + ShadowCasterManager::ForEachShadowLight(shadowSceneNode->GetRuntimeData().shadowLightsAccum, + [&](RE::BSShadowLight* light) { + shadowLightPtrs.insert(light); + // GetShadowSlot returns the kSHADOWMAPS texture slot: + // -1 : sun (no kSHADOWMAPS slice — sun shadows live in kSHADOWMAPS_ESRAM + // and are sampled via the directional cascade path, not the cluster + // loop). Skip cluster injection entirely. The sun stays in + // shadowLightPtrs so the activeLights loop below doesn't re-add it. + // >=0: kSHADOWMAPS slice index (0..ShadowMapSlots-1) post-reclaim. + int32_t stableSlot = ShadowCasterManager::GetShadowSlot(light); + if (stableSlot < 0) + return; + bool castsShadow = static_cast(stableSlot) < ShadowCasterManager::GetInstalledSlotCount(); + addShadowLight(light, castsShadow, castsShadow ? static_cast(stableSlot) : 0u); + }); + for (auto& e : shadowSceneNode->GetRuntimeData().activeLights) { - addLight(e); - } - for (auto& e : shadowSceneNode->GetRuntimeData().activeShadowLights) { + if (auto bsLight = e.get(); bsLight && shadowLightPtrs.count(bsLight)) + continue; // shadow light: already added above with correct Shadow flag addLight(e); } + // Converted shadow lights (shadow lights demoted to normal-light overflow handling + // via SCM's ConvertExcessToNormal) live in the engine's activeShadowLights list + // (offset 0x148) — verified via Ghidra against ShadowSceneNode AE 1.6.1170. They + // are NOT migrated to activeLights (0x130) when our Hook_IsShadowLight reports + // false, because the engine's AddLight just searches the existing wrappers and + // activates the matching one in-place rather than moving entries between lists. + // + // Iterate SCM's s_normalConvert directly rather than scanning activeShadowLights: + // only lights actually in s_normalConvert are intended to render as non-shadow. + // activeShadowLights also contains BSShadowLights that are merely active shadow + // casters this frame (already handled via shadowLightsAccum above), and could in + // principle contain disabled-but-not-yet-removed entries. Iterating the convert + // list is both tighter (no false positives) and cheaper. + // + // Without this, ConvertExcessToNormal lights have no entry in the cluster + // lightsData[] and never render — the user-visible "converted lights are + // invisible" symptom of issue #2121 #3. + ShadowCasterManager::ForEachConvertedLight([&](RE::BSShadowLight* light) { + auto* asBs = static_cast(light); + if (shadowLightPtrs.count(asBs)) + return; // simultaneously a shadow caster this frame; already added + // Honour the user's suppression toggle in the shadow caster table: + // converted lights share the same lightKey suppression set as shadow + // lights, so suppressing one in the table hides it whether it's + // rendering as a shadow caster or demoted to non-shadow. + if (ShadowCasterManager::IsSuppressed(reinterpret_cast(light))) + return; + // Engine zeroes lodDimmer when its shadow-distance LOD cull fires + // (BSShadowParabolicLight_UpdateCamera test 2, gated on the lodFade + // flag -- not a visibility test, see ShadowCasterManager.cpp's + // Ghidra-verified comment). Without restoration, addLight()'s + // `light.fade *= lodDimmer` would zero the contribution and the + // (color*fade > 1e-4) filter would drop the light entirely. + // + // Restore only when fully zeroed. Any smooth fade value the engine + // set (between 0 and 1) is preserved -- those represent the engine's + // own gradual distance attenuation, which is correct to honour for + // cluster lighting. Overriding unconditionally was producing + // distant always-full-bright converted lights that ignored the + // engine's intended fade-with-distance. + if (light->lodDimmer == 0.0f) + light->lodDimmer = 1.0f; + addLight(RE::NiPointer(asBs)); + }); + auto context = globals::d3d::context; lightCount = std::min((uint)lightsData.size(), MAX_LIGHTS); @@ -584,6 +770,13 @@ void LightLimitFix::UpdateLights() context->Unmap(lights->resource.get(), 0); UpdateStructure(); + + // Single-shot consumption: clear the hover key after the cluster has read it. + // The table re-sets it every frame the cursor is hovering a row with Shift + // held, so the pulse continues smoothly while hovering. As soon as the menu + // closes (or the cursor leaves the table, or Shift is released), the table + // stops re-setting the key and the pulse vanishes on the next frame. + ShadowCasterManager::SetHoveredLight(0); } void LightLimitFix::UpdateStructure() @@ -666,6 +859,64 @@ void LightLimitFix::Hooks::BSLightingShader_SetupGeometry::thunk(RE::BSShader* T void LightLimitFix::Hooks::BSEffectShader_SetupGeometry::thunk(RE::BSShader* This, RE::BSRenderPass* Pass, uint32_t RenderFlags) { + // Defensive pre-call guard: BSEffectShader::SetupGeometry iterates + // Pass->sceneLights[i] and dereferences bsLight->light->fade + // (BSLight+0x48 -> NiLight+0x134) with NO null check. Stale entries are + // possible because Pass->sceneLights[] is a raw BSLight** (not + // NiPointer<>): the engine's pass cache can outlive individual lights + // or capture them after their NiLight has been cleared. Crashes seen in + // the wild include garbage data (BSLight memory recycled as a string + // buffer) and outright NULL NiLight (engine half-destroyed the BSLight + // but it's still ref-counted alive in some list). + // + // Walk the array and clamp numLights to the count of entries that the + // engine can safely dereference. Validation: + // - BSLight* is canonical, 8-byte aligned, non-null + // - bsLight->light pointer is canonical, 8-byte aligned, non-null + // Entries failing either check stop the loop; the engine's own loop + // bails on the first bad entry too, so clamping matches its contract. + if (Pass && Pass->sceneLights && Pass->numLights > 0) { + const auto isPlausible = [](const void* p) { + const auto v = reinterpret_cast(p); + return v >= 0x10000 && v < 0x800000000000ull && (v & 0x7) == 0; + }; + std::uint8_t validCount = 0; + for (std::uint8_t i = 0; i < Pass->numLights; ++i) { + RE::BSLight* bsLight = Pass->sceneLights[i]; + if (!isPlausible(bsLight)) { + static int loggedBsLight = 0; + if (loggedBsLight++ < 10) { + logger::warn( + "[LLF] BSEffectShader_SetupGeometry: bad BSLight* at " + "sceneLights[{}]=0x{:x} numLights={}; clamping to {}", + i, reinterpret_cast(bsLight), Pass->numLights, validCount); + } + break; + } + RE::NiLight* niLight = bsLight->light.get(); + if (!isPlausible(niLight)) { + // Catches both NULL (engine cleared the NiPointer) and + // garbage (BSLight memory recycled). NULL is the more common + // observed failure -- the engine's loop has no null check + // before reading [+0x134]. + static int loggedNiLight = 0; + if (loggedNiLight++ < 10) { + logger::warn( + "[LLF] BSEffectShader_SetupGeometry: bad NiLight at " + "sceneLights[{}] (BSLight=0x{:x} NiLight=0x{:x}); clamping to {}", + i, + reinterpret_cast(bsLight), + reinterpret_cast(niLight), + validCount); + } + break; + } + ++validCount; + } + if (validCount < Pass->numLights) + Pass->numLights = validCount; + } + func(This, Pass, RenderFlags); ExternalEmittance::UpdatePermutation(Pass); auto& singleton = globals::features::lightLimitFix; diff --git a/src/Features/LightLimitFix.h b/src/Features/LightLimitFix.h index 375019d2b5..c71b0dc736 100644 --- a/src/Features/LightLimitFix.h +++ b/src/Features/LightLimitFix.h @@ -1,10 +1,15 @@ #pragma once #include "Buffer.h" +#include "LightLimitFix/ShadowCasterManager.h" #include "OverlayFeature.h" struct LightLimitFix : OverlayFeature { +private: + static constexpr uint32_t MAX_LIGHTS = 1024; + static constexpr uint32_t CLUSTER_MAX_LIGHTS = 128; + public: virtual inline std::string GetName() override { return "Light Limit Fix"; } virtual inline std::string GetShortName() override { return "LightLimitFix"; } @@ -14,13 +19,12 @@ struct LightLimitFix : OverlayFeature virtual std::pair> GetFeatureSummary() override { return { - "Light Limit Fix removes the vanilla game's 4-light limit, allowing unlimited dynamic lights in scenes.\n" - "This dramatically improves lighting quality and enables more realistic illumination scenarios.", + "Light Limit Fix removes the vanilla game's 4-light limit, allowing unlimited dynamic lights in scenes. " + "It also extends shadow support to all point and spot lights.", { "Removes 4-light limit", "Unlimited dynamic lights", - "Improved lighting quality", - "Enhanced visual realism", - "Enhanced visual realism" } + "Shadow support for point and spot lights", + "Improved lighting quality" } }; } @@ -55,9 +59,8 @@ struct LightLimitFix : OverlayFeature PositionOpt positionWS[2]; uint128_t roomFlags = uint32_t(0); stl::enumeration lightFlags; - uint32_t shadowMaskIndex = 0; - uint pad0; - uint pad1; + uint32_t shadowMapIndex = 0; + float2 pad0; }; STATIC_ASSERT_ALIGNAS_16(LightData); @@ -101,10 +104,13 @@ struct LightLimitFix : OverlayFeature float ContactShadowThickness; float ContactShadowDepthFade; float ContactShadowMinIntensity; + uint32_t ShadowMapSlots; // total shadow map texture-array capacity + // Cluster config (computed) + uint ClusterSize[4]; + // Debug (last) uint EnableLightsVisualisation; uint LightsVisualisationMode; - float pad0[3]; - uint ClusterSize[4]; + uint pad0[2]; }; STATIC_ASSERT_ALIGNAS_16(PerFrame); // Compile-time size lock catches CPU/GPU cbuffer layout drift. STATIC_ASSERT_ALIGNAS_16 @@ -134,8 +140,16 @@ struct LightLimitFix : OverlayFeature ConstantBuffer* strictLightDataCB = nullptr; int eyeCount = !REL::Module::IsVR() ? 1 : 2; - bool previousEnableLightsVisualisation = settings.EnableLightsVisualisation; - bool currentEnableLightsVisualisation = settings.EnableLightsVisualisation; + + // Debug-only visualization state. Lives on the instance rather than in + // Settings so it can't accidentally persist into a user's config: a + // shipped JSON with `EnableLightsVisualisation = true` would force every + // load to compile the heavier LLFDEBUG shader permutation. These reset to + // off on each session. + bool EnableLightsVisualisation = false; + uint LightsVisualisationMode = 0; + bool previousEnableLightsVisualisation = false; + bool currentEnableLightsVisualisation = false; ID3D11ComputeShader* clusterBuildingCS = nullptr; ID3D11ComputeShader* clusterCullingCS = nullptr; @@ -157,10 +171,23 @@ struct LightLimitFix : OverlayFeature bool wasEmpty = false; bool wasWorld = false; int previousRoomIndex = -1; - uint previousShadowBitMask = 0; Util::FrameChecker frameChecker; + // Point/spot shadow resources (t102, t103 -- t100/t101 reserved for Grass Collision) + // shadowLights is lazily allocated in CopyShadowLightData() since shadowMapSlots + // is not known until Deferred::SetupResources() runs (after Feature::SetupResources()). + Buffer* shadowLights = nullptr; + uint32_t shadowLightsCapacity = 0; + + // Per-frame shadow accounting (displayed in DrawSettings Statistics tree). + uint32_t shadowLightCount = 0; // distinct lights processed (including dropped) + uint32_t shadowUnshadowedLightCount = 0; // lights that exceeded slot capacity + + /// Generate a text legend mapping each shadow-map slot index to its golden-ratio hue + /// and light type. Used for RenderDoc capture comments when mode 8 is active. + std::string BuildShadowSlotColorLegend() const; + virtual void SetupResources() override; virtual void RestoreDefaultSettings() override; @@ -169,7 +196,11 @@ struct LightLimitFix : OverlayFeature virtual void DrawSettings() override; virtual void DrawOverlay() override; - virtual bool IsOverlayVisible() const override { return settings.EnableLightsVisualisation; } + virtual bool IsOverlayVisible() const override + { + return EnableLightsVisualisation || settings.ShowShadowOverlay || + ShadowCasterManager::HasSuppressedLights() || ShadowCasterManager::HasAnyOverrides(); + } virtual void PostPostLoad() override; virtual void DataLoaded() override; @@ -179,7 +210,11 @@ struct LightLimitFix : OverlayFeature void SetLightPosition(LightLimitFix::LightData& a_light, RE::NiPoint3 a_initialPosition, bool a_cached = true); void UpdateLights(); void UpdateStructure(); + virtual void EarlyPrepass() override; virtual void Prepass() override; + void CopyShadowLightData(); + + // Shadow rendering helpers (implemented in LightLimitFix/ShadowRenderer.cpp) static inline float3 Saturation(float3 color, float saturation); static inline bool IsValidLight(RE::BSLight* a_light); @@ -202,8 +237,16 @@ struct LightLimitFix : OverlayFeature // (1 - (lightDist/radius)^2) at the pixel is below this threshold. Strict // lights always raymarch. 0 = never skip; 1 = always skip. float ContactShadowMinIntensity = 0.25f; - bool EnableLightsVisualisation = false; - uint LightsVisualisationMode = 0; + + /// Show the shadow caster overlay (suppression / debug-override table) + /// independently of the visualization mode and suppression state. + /// Without this, the overlay only appeared when a light was suppressed + /// or visualisation was active — making it hard to access the overlay's + /// debug controls (cycle button, solo, hover-pulse) in the default state. + bool ShowShadowOverlay = false; + + // Shadow caster scheduling (ShadowCasterManager) + ShadowCasterManager::Settings ShadowSettings; }; uint clusterSize[3] = { 16 }; diff --git a/src/Features/LightLimitFix/ShadowCasterManager.cpp b/src/Features/LightLimitFix/ShadowCasterManager.cpp new file mode 100644 index 0000000000..2bd5a146d4 --- /dev/null +++ b/src/Features/LightLimitFix/ShadowCasterManager.cpp @@ -0,0 +1,5463 @@ +// ShadowCasterManager.cpp +// Shadow caster scheduling for LightLimitFix. +// +// Based on Intellightent by meh321 +// https://www.nexusmods.com/skyrimspecialedition/mods/172423 +// +// Ported and adapted for Community Shaders by the Community Shaders team with permission. + +#include "ShadowCasterManager.h" +#include "../../Deferred.h" +#include "../../Globals.h" +#include "../../State.h" +#include "../../Utils/Game.h" +#include "../../Utils/UI.h" +#include "../Upscaling.h" +#include "../VR.h" + +#include + +namespace ShadowCasterManager +{ + // ========================================================================= + // Formula evaluator (exprtk) + // ========================================================================= + + struct FormulaWrapper + { + exprtk::expression expression; + exprtk::parser parser; + }; + + static double s_formulaParams[kFormulaParam_Max]; + static exprtk::symbol_table s_symbolTable; + static bool s_formulaInited = false; + + struct FormulaVarInfo + { + const char* name; + const char* description; + int32_t index; + }; + + // Single authoritative list of formula variables. + // Drives both symbol table registration and the formula editor help text. + static constexpr FormulaVarInfo kFormulaVars[] = { + { "lightindex", "sequential index of this candidate light", kFormulaParam_LightIndex }, + { "lightintensity", "NiLight fade/intensity", kFormulaParam_LightIntensity }, + { "lightdistance", "camera-to-light distance (game units; 1 unit ~= 1.428 cm)", kFormulaParam_LightDistance }, + { "lightradius", "light radius/range (game units; 1 unit ~= 1.428 cm)", kFormulaParam_LightRadius }, + { "lightx", "light world X", kFormulaParam_LightX }, + { "lighty", "light world Y", kFormulaParam_LightY }, + { "lightz", "light world Z", kFormulaParam_LightZ }, + { "lightr", "diffuse red", kFormulaParam_LightR }, + { "lightg", "diffuse green", kFormulaParam_LightG }, + { "lightb", "diffuse blue", kFormulaParam_LightB }, + { "lightambientr", "ambient red", kFormulaParam_LightAmbientR }, + { "lightambientg", "ambient green", kFormulaParam_LightAmbientG }, + { "lightambientb", "ambient blue", kFormulaParam_LightAmbientB }, + { "lightchosenlastframe", "1 if this light held a slot last frame", kFormulaParam_LightChosenLastFrame }, + { "lightframessincerender", "frames since this light's slot was last actually rendered into the shadow atlas; 1e6 sentinel when never rendered or unassigned", kFormulaParam_LightFramesSinceRender }, + { "lightneverfades", "1 if lodFade disabled (permanent light)", kFormulaParam_LightNeverFades }, + { "lightportalstrict", "1 if portal-strict (always 1 for shadow casters)", kFormulaParam_LightPortalStrict }, + { "lightns", "1 if promoted from normal light (PromoteNormalToShadow)", kFormulaParam_LightNS }, + { "lightconverted", "1 if light is in the converted (non-shadow) slot range", kFormulaParam_LightConverted }, + { "lightdisplacement", "distance this light moved since its last shadow map render (game units; 0 when not yet tracked or in score formula)", kFormulaParam_LightDisplacement }, + { "playerlightdistance", "distance from the player character to the light (game units; falls back to lightdistance when player unavailable)", kFormulaParam_PlayerLightDistance }, + { "lightimportance", "contribution score: lum(diffuse*fade) * max(att_cam,att_plr) where att=(1-(dist/radius)^2)^2; 0 in score formula", kFormulaParam_LightImportance }, + { "lightisspot", "1 if this is a spot/frustum shadow light (BSShadowFrustumLight); 0 for omni / hemi / sun", kFormulaParam_LightIsSpot }, + { "lightspotvisible", "1 if the spot's cone plausibly reaches the camera frustum, 0 otherwise. Always 1 for non-spot lights so existing omni-only formulas are unaffected", kFormulaParam_LightSpotVisible }, + { "camerax", "camera world X", kFormulaParam_CameraX }, + { "cameray", "camera world Y", kFormulaParam_CameraY }, + { "cameraz", "camera world Z", kFormulaParam_CameraZ }, + { "isinterior", "1 in interior cells, 0 outdoors", kFormulaParam_IsInterior }, + { "timeofday", "in-game hour (0.0-24.0)", kFormulaParam_TimeOfDay }, + { "frametime", "EMA-smoothed frame time (ms)", kFormulaParam_FrameTime }, + { "frametarget", "90th-percentile recent frame time (ms) -- headroom ceiling", kFormulaParam_FrameTarget }, + { "stableframes", "consecutive frames EMA has been below frametarget", kFormulaParam_StableFrames }, + }; + + static void InitFormulaSystem() + { + if (s_formulaInited) + return; + s_formulaInited = true; + + memset(s_formulaParams, 0, sizeof(double) * kFormulaParam_Max); + + for (const auto& v : kFormulaVars) + s_symbolTable.add_variable(v.name, s_formulaParams[v.index]); + } + + FormulaHelper::FormulaHelper() : + _ptr(nullptr) { InitFormulaSystem(); } + + FormulaHelper::~FormulaHelper() + { + if (_ptr) + delete static_cast(_ptr); + } + + bool FormulaHelper::Parse(const std::string& input) + { + if (_ptr) + return false; + auto* w = new FormulaWrapper(); + w->expression.register_symbol_table(s_symbolTable); + // Defer the _ptr assignment until compile succeeds. Otherwise a + // failed compile leaves the helper in a "parsed" state (Calculate + // would evaluate an uncompiled expression and the early-return + // guard above would block subsequent Parse retries). + if (!w->parser.compile(input, w->expression)) { + delete w; + return false; + } + _ptr = w; + return true; + } + + double FormulaHelper::Calculate() + { + auto* w = static_cast(_ptr); + return w ? w->expression.value() : 0.0; + } + + bool FormulaHelper::Reparse(const std::string& input) + { + std::string err; + if (!Validate(input, err)) + return false; + if (_ptr) + delete static_cast(_ptr); + _ptr = nullptr; + return Parse(input); + } + + bool FormulaHelper::Validate(const std::string& input, std::string& errorOut) + { + InitFormulaSystem(); + FormulaWrapper tmp; + tmp.expression.register_symbol_table(s_symbolTable); + if (tmp.parser.compile(input, tmp.expression)) + return true; + if (tmp.parser.error_count() > 0) + errorOut = tmp.parser.get_error(0).diagnostic; + else + errorOut = "Unknown parse error"; + return false; + } + + void FormulaHelper::SetParam(int32_t index, double value) { s_formulaParams[index] = value; } + double FormulaHelper::GetParam(int32_t index) { return s_formulaParams[index]; } + + // ========================================================================= + // Module-level state + // ========================================================================= + + /// Total LightEntry slots: sun (1) + shadow casters (≥4) + converted pool. + static int32_t LightContainerSize(const Settings& s) + { + return std::max(4, s.ShadowLightCount) + 1 + s.ConvertedShadowSlots; + } + + static Settings s_settings; + static LightContainer s_lights; + static BudgetTracker s_budget; + + // External conflict detection -- set during Install(), checked by Update() and DrawSettings(). + static bool s_externalConflict = false; + static std::string s_conflictMessage; + + // Per-frame count of kSHADOWMAPS slots claimed by the engine's focus + // shadow renderer (player + tracked NPCs, max 4). Read from + // FocusShadowActors.size each frame; values clamp to [0, 4]. Reserves + // the slot range [g_focusShadowBaseSlotIndex .. +s_focusShadowSlots) = + // [4 .. 4+count) from the point-light pool dynamically: zero focus + // actors means the full pool is available, four means slots 4-7 are + // off-limits. Point lights occupying a freshly-claimed slot are + // ejected at scheduling time and re-allocated to a free slot or + // converted as excess. + static int s_focusShadowSlots = 0; + + // Rolling redraw history (128-frame window) for DrawSettings statistics. + static constexpr int kRedrawHistorySize = 128; + static int32_t s_redrawHistory[kRedrawHistorySize] = {}; + static int32_t s_redrawHistoryPos = 0; + static int32_t s_redrawSum = 0; + + // Rolling budget-consumed history (same window) for DrawSettings statistics. + static int32_t s_budgetHistory[kRedrawHistorySize] = {}; + static int32_t s_budgetHistoryPos = 0; + static int64_t s_budgetSum = 0; + + // Frame-time tracking — used by Formula's frametime/frametarget/stableframes + // formula params, the shared frame-state diagnostic block, and stats UI. + // Persists in both Manual and Formula modes; the cost is one float per frame. + static constexpr int kFrameWindow = 120; // ~2 s at 60 fps + static float s_ftRing[kFrameWindow]{}; + static int s_ftHead = 0; + static int s_ftCount = 0; + static float s_ftEMA = 0.0f; + static int s_stableFrames = 0; + static float s_autoBudgetMs = 0.0f; // last computed budget; used by UI, scheduling, and stats + + // "Steady" state thresholds for the shared frame-state diagnostic. + // Mirror the old Auto-mode hysteresis values so the indicator behaves the + // same way users grew used to, just informational rather than driving control. + static constexpr float kFrameHeadroomDeadZoneMs = 0.3f; // |headroom| below this = "steady" + static constexpr float kFrameHeadroomSafetyMs = 0.5f; // headroom must clear this before "growing" + + // Budget tracking for UI display + static int32_t s_redrawnLightsThisFrame = 0; + static int32_t s_totalShadowLightsThisFrame = 0; + static uint32_t s_highImportanceLightCount = 0; + static float s_redrawnLightsSmoothed = 0.0f; // EMA-smoothed for stable UI display + + // Tracy diagnostic counters reset at the start of each scheduler frame. + // Each candidate-handling path increments its bucket; values are emitted + // as TracyPlot at frame end so a capture can be queried to identify + // which paths fire under which budget/setting combinations. Cross- + // reference per-action ZoneText emissions (light pointer, reason) to + // identify *which* lights are hitting each path. + struct SchedDiagCounters + { + int candidates_total = 0; + int candidates_chosen = 0; + int candidates_excess = 0; + int candidates_invalid_camera = 0; + int candidates_invalid_portal = 0; + int candidates_invalid_frustum = 0; // sub-reason: outside camera frustum + int candidates_invalid_lod = 0; // sub-reason: lodDimmer zeroed (engine LOD fade) + int candidates_invalid_other = 0; // invalidCamera but neither frustum nor LOD flag + int converted_invalid = 0; // ConvertLight from c.invalidCamera path + int converted_excess = 0; // ConvertLight from c.excess path + int disabled_invalid = 0; // DisableLight from c.invalid path (portal/spot/no-convert) + int disabled_excess = 0; // DisableLight from c.excess path (spot/no-convert) + int reconciliation_clears = 0; // slot freed because light gone from activeShadowLights + int slots_in_use = 0; // sampled at frame end + int first_render_skips = 0; // chosen lights deferred from shadow set: no valid slice yet + }; + static SchedDiagCounters s_schedDiag; + + static float ComputeFrameTimePercentile90() + { + if (s_ftCount == 0) + return 16.67f; // fallback: 60 fps target + const int n = std::min(s_ftCount, kFrameWindow); + float tmp[kFrameWindow]; + std::copy(s_ftRing, s_ftRing + n, tmp); + const int idx = static_cast(n * 0.9f); + std::nth_element(tmp, tmp + idx, tmp + n); + return tmp[idx]; + } + + // Maximum ShadowLightCount the installed infrastructure supports. + // Set once by Install() to the *requested* count; later refined by + // RefreshInstalledSlotCount() to reflect what the GPU actually allocated. + // Update() clamps the user-facing setting to this. + static int32_t s_installedShadowLightCount; + + // What SCM asked the engine for. Equals settings.ShadowLightCount -- + // the sun lives in a separate texture (kSHADOWMAPS_ESRAM), so there's + // no +1 sun cascade slice in kSHADOWMAPS. Captured at Install so the + // post-allocation verification can detect VRAM-exhaustion fallbacks + // where the actual texture ends up smaller than requested. + static uint32_t s_requestedSlotCount = 0; + + // Total kSHADOWMAPS texture-array capacity *as actually allocated*. + // 0 until kSHADOWMAPS exists and we've read its real ArraySize back. + // Owned here (not in Deferred) because SCM is the only thing that + // modifies the engine's allocation request, and verification of that + // request is the same code path. Consumers (LLF cluster pipeline, + // SCM scheduler clamp, SCM UI) read via GetInstalledSlotCount(). + static uint32_t s_installedSlotCount = 0; + + // True once we've logged a verification result. Prevents spam if the + // SRV stays null forever (vanilla-disabled session) or oscillates. + static bool s_slotCountLogged = false; + + // Formula instances (allocated at Init if formula strings are non-empty) + static std::unique_ptr s_formulaScore; + static std::unique_ptr s_formulaRedrawInterval; + static std::unique_ptr s_formulaRedrawBudget; + + // Lights converted to normal (non-shadow) lights for diffuse-only rendering + struct ConvertedLight + { + RE::BSShadowLight* light; + bool isNS; + }; + static std::vector s_normalConvert; + static std::set s_shadowConvert; + + // User suppression set (lightKey = BSShadowLight pointer cast to uintptr_t). + // Persisted across light lifetimes so suppressing a torch survives the player + // leaving and returning to a cell. + static std::unordered_set s_suppressedLights; + + // Debugging overrides — see header docs for ClearAllOverrides / SetPinnedShadow / etc. + // Declared up here (rather than next to s_shadowSlotInfos) because the scheduler + // reads s_pinShadow / s_pinConvert to bias candidate scoring, and that's compiled + // long before the table-rendering code. + static std::unordered_set s_pinShadow; ///< force chosen (top of score sort) + static std::unordered_set s_pinConvert; ///< force excess + ConvertLight + static uintptr_t s_soloLight = 0; ///< 0 = no solo + static uintptr_t s_hoverLightKey = 0; ///< transient (per table draw) + + // ========================================================================= + // Helpers for depth-target index globals + // SE: 14304EEE8 / AE: n/a (adjacent) / VR: 143180df0 + // ========================================================================= + static int32_t GetDepthTargetType() + { + static REL::RelocationID uid(524780, 388826); + return *reinterpret_cast(uid.address()); + } + + static int32_t GetDepthTargetSubIndex() + { + static REL::RelocationID uid(524780, 388826); + return *reinterpret_cast(uid.address() + 4); + } + + // ========================================================================= + // Hook implementations + // ========================================================================= + + // ------------------------------------------------------------------------- + // Expanded accumulated-lights array + // The game allocates a local array sized for 8 lights (with +1 sentinel). + // When using more than 8 shadow casters we extend RDI (SE) / RBX (AE/VR) + // which is the loop-end counter, and RDX (SE) which is the copy-end counter. + // ------------------------------------------------------------------------- + static void Hook_AccumulatedLightsArray(CONTEXT& ctx) + { + int needed = (s_settings.ShadowLightCount + s_settings.ConvertedShadowSlots + 1) * 2; + int have = 10; // game default: (4+1)*2 + int extra = needed - have; + if (extra > 0) { + ctx.Rdi += extra; + // SE only: RDX is a second counter in the same loop. + if (!REL::Module::IsVR() && REL::Module::GetRuntime() != REL::Module::Runtime::AE) + ctx.Rdx += extra; + } + } + + // ------------------------------------------------------------------------- + // Redirect depth-stencil-view creation to our extended arrays + // The game loops 0..7 creating depth stencil views and stores each pointer + // in a game-managed struct at R9. We redirect R9 to our own arrays so + // views >= 8 land in globals::features::llf::normalDepthBuffer / globals::features::llf::readOnlyDepthBuffer. + // ------------------------------------------------------------------------- + static void Hook_CreateNormalDepthBuffer(CONTEXT& ctx) + { + // R12 (SE/AE) or R13 (VR) holds a_target * 0x13; value 4*19=76 identifies + // the shadow-map depth target. RDI (SE) / RBX (AE/VR) is the loop index. + if (REL::Relocate(ctx.R12, ctx.R12, ctx.R13) != 4 * 19) + return; + int idx = (int)REL::Relocate(ctx.Rdi, ctx.Rbx, ctx.Rbx); + ctx.R9 = reinterpret_cast(&globals::features::llf::normalDepthBuffer[idx]); + } + + static void Hook_CreateReadOnlyDepthBuffer(CONTEXT& ctx) + { + if (REL::Relocate(ctx.R12, ctx.R12, ctx.R13) != 4 * 19) + return; + int idx = (int)REL::Relocate(ctx.Rdi, ctx.Rbx, ctx.Rbx); + ctx.R9 = reinterpret_cast(&globals::features::llf::readOnlyDepthBuffer[idx]); + } + + // ------------------------------------------------------------------------- + // Copy first 8 views into the game's own DepthStencilData array + // Called after the creation loop finishes; syncs the game struct so existing + // code reading depthStencils[4].views[0..7] still works correctly. + // ------------------------------------------------------------------------- + static void Hook_SetupGameArray(CONTEXT& ctx) + { + if (REL::Relocate(ctx.R12, ctx.R12, ctx.R13) != 4 * 19) + return; + auto* renderer = reinterpret_cast(ctx.R15); + for (int i = 0; i < 8; i++) { + renderer->GetDepthStencilData().depthStencils[4].views[i] = reinterpret_cast(globals::features::llf::normalDepthBuffer[i]); + renderer->GetDepthStencilData().depthStencils[4].readOnlyViews[i] = reinterpret_cast(globals::features::llf::readOnlyDepthBuffer[i]); + } + } + + // ------------------------------------------------------------------------- + // Redirect depth-buffer selection at draw time + // When the active depth target is type 4 (shadow maps), route sub-index + // lookups through our extended arrays instead of the game struct. + // Hook #1: renderer in R8, result -> RBX. + // ------------------------------------------------------------------------- + static void Hook_SelectDepthBuffer1(CONTEXT& ctx) + { + auto* data = reinterpret_cast(ctx.R8); + int type = GetDepthTargetType(); + int sub = GetDepthTargetSubIndex(); + + if (type == 4) { + ctx.Rbx = data->readOnlyDepth ? reinterpret_cast(globals::features::llf::readOnlyDepthBuffer[sub]) : reinterpret_cast(globals::features::llf::normalDepthBuffer[sub]); + } else { + ctx.Rbx = data->readOnlyDepth ? reinterpret_cast(RE::BSGraphics::Renderer::GetSingleton()->GetDepthStencilData().depthStencils[type].readOnlyViews[sub]) : reinterpret_cast(RE::BSGraphics::Renderer::GetSingleton()->GetDepthStencilData().depthStencils[type].views[sub]); + } + } + + // Hook #2: VR: renderer in R14, result -> RBP; SE/AE: renderer in RBP, result -> R14. + static void Hook_SelectDepthBuffer2(CONTEXT& ctx) + { + bool isVR = REL::Module::IsVR(); + bool readOnly = isVR ? reinterpret_cast(ctx.R14)->GetRuntimeData().readOnlyDepth : reinterpret_cast(ctx.Rbp)->GetRuntimeData().readOnlyDepth; + + int type = GetDepthTargetType(); + int sub = GetDepthTargetSubIndex(); + + DWORD64 result; + if (type == 4) { + result = readOnly ? reinterpret_cast(globals::features::llf::readOnlyDepthBuffer[sub]) : reinterpret_cast(globals::features::llf::normalDepthBuffer[sub]); + } else { + result = readOnly ? reinterpret_cast(RE::BSGraphics::Renderer::GetSingleton()->GetDepthStencilData().depthStencils[type].readOnlyViews[sub]) : reinterpret_cast(RE::BSGraphics::Renderer::GetSingleton()->GetDepthStencilData().depthStencils[type].views[sub]); + } + + if (isVR) + ctx.Rbp = result; + else + ctx.R14 = result; + } + + // ------------------------------------------------------------------------- + // Release extended depth buffers at renderer shutdown + // ------------------------------------------------------------------------- + static void ReleaseExtendedDepthBuffers(int shadowCount) + { + for (int i = 8; i < shadowCount; i++) { + if (globals::features::llf::normalDepthBuffer[i]) { + reinterpret_cast(globals::features::llf::normalDepthBuffer[i])->Release(); + globals::features::llf::normalDepthBuffer[i] = nullptr; + } + if (globals::features::llf::readOnlyDepthBuffer[i]) { + reinterpret_cast(globals::features::llf::readOnlyDepthBuffer[i])->Release(); + globals::features::llf::readOnlyDepthBuffer[i] = nullptr; + } + } + } + + static void Hook_DeleteDepthBuffers_SE(CONTEXT& ctx) + { + // Only fire when RBX points at depthStencils[4], not at other delete calls. + auto* data = reinterpret_cast(ctx.Rbx); + if (data == &RE::BSGraphics::Renderer::GetSingleton()->GetDepthStencilData().depthStencils[4]) + ReleaseExtendedDepthBuffers(s_settings.ShadowLightCount); + } + + static void Hook_DeleteDepthBuffers_AE(CONTEXT& /*ctx*/) + { + ReleaseExtendedDepthBuffers(s_settings.ShadowLightCount); + } + + // ------------------------------------------------------------------------- + // Force each light to use its assigned shadow map slot + // RenderCascade would otherwise recalculate a slot index from a global + // counter, causing lights that weren't re-rendered this frame to corrupt + // each other's shadow maps. + // SE: light pointer in R15, slot index out in RSI. + // VR: light pointer in R14, slot index out in RDX. + // ------------------------------------------------------------------------- + static void Hook_OverwriteShadowMapIndex(CONTEXT& ctx) + { + // Enabled is a boot-time gate (see Init early-return) -- this + // hook is only installed when SCM is enabled at boot, so it + // runs unconditionally per-frame from there. Toggling Enabled + // off at runtime no longer affects the hook; restart is the + // only safe way to revert. See Hook_CalculateActiveShadowCasters + // comment for the crash rationale. + + auto* light = reinterpret_cast(REL::Relocate(ctx.R15, ctx.R15, ctx.R14)); + int32_t idx = s_lights.FindLight(light, s_settings.ShadowLightCount); + if (idx < 0) + idx = 0; // should not happen; fail-safe to slot 0 + // This hook runs inside BSShadowParabolicLight::RenderCascade's + // `renderTarget == kNONE` block, so it only fires for point/spot + // lights (the sun's RenderShadowmaps presets renderTarget to 2/3/4 + // before each call, skipping the block). FindLight must therefore + // cover the same range as FindFreeIndex; a mismatch means a light + // silently gets idx=0 and corrupts the slot at index 0. + + if (REL::Module::IsVR()) + ctx.Rdx = static_cast(idx); + else + ctx.Rsi = static_cast(idx); + } + + // ------------------------------------------------------------------------- + // Screen-space shadow-mask pass wrapper + // ------------------------------------------------------------------------- + // + // Vanilla wires Main::RenderShadowmasks (100422/107140) to call + // RenderShadowLightsWithUtilityShader (100423/107141) which: + // - binds kSHADOW_MASK as RT, clears it, + // - walks ssn->shadowLightsAccum[] and for each entry emits a full-screen + // BSUtilityShader pass that samples the cascade / parabolic depth maps + // and writes the mask. + // + // The inner loop indexes a hard-coded 4-entry table (DAT_141861380, + // per-slot m_AlphaBlendWriteMode) by BSShadowLight::maskIndex (offset 0x520 + // SE/AE, 0x580 VR -- see CommonLib BSShadowLight.h). Vanilla only ever + // populates 4 kSHADOWMAPS slices so maskIndex stays in [0..3] and the index + // is safe. + // + // SLF's extended scheduler assigns maskIndex up to ShadowLightCount-1 + // (LightContainer / EnableLight; see ShadowField(e.Light, maskIndex) = + // static_cast(slot) below). For any slot >= 4, the engine's + // MOV [R15 + RDX*0x4] OOB-reads garbage out of DAT_141861380 (next dword is + // 0x3F7FFFDE, a float bit pattern) which lands in + // g_RendererShadowState.m_AlphaBlendWriteMode -> undefined D3D state. + // + // Previous fix nopped out the CALL site entirely ("Hook_DisableColorMask", + // misnamed: the patched call IS RenderShadowLightsWithUtilityShader, NOT a + // color-mask call -- verified via Ghidra on SE 1.5.97 (+0x90 -> 0x1412e3b80) + // and SkyrimVR (+0x9E -> 0x141323740), matching RelocationID 100423/107141). + // That killed the screen-space mask globally, removing sun shadows and + // brightening the scene because deferred lighting sampled an undisturbed + // (effectively fully-lit) mask RT. RenderDoc evidence: empty "Shadowmasks" + // engine marker; cascade depth maps still rendered upstream but never + // consumed. + // + // This wrapper restores vanilla behaviour for the first 4 cascade slices + // and silently elides any extended-slot entries by writing a null sentinel + // into shadowLightsAccum at the cutoff. The engine's + // GetShadowCasterLightArrayEntry terminates when the slot pointer is null, + // so the loop stops cleanly without ever indexing DAT_141861380 for slot + // >= 4. The saved pointer is restored after the call. + // + // Under LIGHT_LIMIT_FIX only the mask's R channel (sun cascades) is read by + // the lighting shader (Lighting.hlsl:2516 shadowColor.x); G/B/A and any + // slot >= 4 are handled by LLF's cluster pipeline sampling kSHADOWMAPS + // directly. Restoring the mask therefore fixes the sun-shadow regression + // without interfering with extended shadow casters. + struct Hook_RenderShadowLightsWithUtilityShader + { + // Skip vanilla entirely. + // + // Vanilla's RenderShadowLightsWithUtilityShader iterates + // shadowLightsAccum and emits a full-screen pass per entry, indexing + // a 4-entry per-slot blend-mode table (DAT_141861380) by each light's + // maskIndex (BSShadowLight+0x520). Three failure modes were observed + // with SLF's scheduling: + // 1. Extended slots (maskIndex >= 4) OOB-read the table. + // 2. Vanilla advances `uVar7 += light->shadowMapCount` and reads + // `shadowLightsAccum[uVar7]`; with a 3-cascade sun and + // accum.size() < 4, the next read is past the array buffer. + // Heap garbage that looks like a BSShadowLight* gets + // dereferenced on [+0x520]. Verified crashes: + // crash-2026-05-25-15-16-25.log RDX=0x3B1F3023 + // crash-2026-05-25-15-26-59.log RDX=0x3AA96F53 + // crash-2026-05-25-15-28-04.log RDX=0x3A4A3190 + // crash-2026-05-25-15-36-15.log RDX=0x3A4B11F5 + // all at 107141+0x319. + // 3. shadowLightsAccum entries created by GameAccumulate() (engine + // focus path) bypass SLF's maskIndex assignment in EnableLight, + // so maskIndex stays at uninitialized memory. + // + // Trying to bound vanilla's iteration safely required defending all + // three modes (BSTArray padding, maskIndex clamp, slice-count cap) + // and one of them kept slipping through. The simplest robust answer + // is to skip vanilla entirely. + // + // Under LIGHT_LIMIT_FIX (this fork's shipping configuration) the + // screen-space mask is not on the sun-shadow consumer path: + // - Lighting.hlsl:2515 uses LightLimitFix::GetDirectionalShadow, + // which samples DirectionalShadowCascades (t99) directly. + // - The cluster loop uses LightLimitFix::GetShadowLightShadow, + // which samples kSHADOWMAPS slices directly. + // shadowColor.x is consulted only as a fallback past the cascade + // range and during the !LIGHT_LIMIT_FIX vanilla path. Dropping the + // mask therefore loses no functionality LLF provides. + // + // Critically, unlike the previous Hook_DisableColorMask, we do NOT + // call ReturnShadowmaps. That side-effect cleared shadowmap- + // Descriptors and broke Deferred::CopyShadowLightData's cascade + // matrix upload, which is what produced the original "no sun + // shadow + scene brighter" symptom. + static void thunk() + { + (void)func; // suppress "unused" warning while keeping the relocation + } + static inline REL::Relocation func; + }; + + // ========================================================================= + // LightContainer methods + // ========================================================================= + + // Engine writes focus shadows to kSHADOWMAPS slots + // [kFocusShadowBaseSlotIndex .. +s_focusShadowSlots) (DAT_141867188 = 4 + // in vanilla, max 4 actors). Two predicates separate "could be claimed" + // from "currently claimed": + // IsFocusShadowReservableSlot(i) -- in the full [4..8) range that + // focus might use. FindFreeIndex treats these as last-resort so an + // actor appearance rarely needs to evict anything. + // IsFocusShadowSlot(i) -- currently held by an active focus actor; + // never allocated, and any point light here gets ejected at + // scheduling time. + static constexpr int32_t kFocusShadowBaseSlotIndex = 4; + static constexpr int32_t kFocusShadowMaxSlots = 4; + + static inline bool IsFocusShadowReservableSlot(int32_t i) + { + return i >= kFocusShadowBaseSlotIndex && i < kFocusShadowBaseSlotIndex + kFocusShadowMaxSlots; + } + + static inline bool IsFocusShadowSlot(int32_t i) + { + return i >= kFocusShadowBaseSlotIndex && i < kFocusShadowBaseSlotIndex + s_focusShadowSlots; + } + + int32_t LightContainer::FindFreeIndex(bool shadowSlot, int32_t shadowCount, int32_t convertCount) const + { + // Pool layout when Sun=true: [0]=sun, [1..shadowCount]=point lights, [shadowCount+1..]=converted + // Sun=false: [0..shadowCount-1]=point lights, [shadowCount..]=converted + // + // Slot 0 is reserved for the sun pointer when present (sunOff=1) so + // FindLight can locate the sun. The sun renders to kSHADOWMAPS_ESRAM + // (target 2), not kSHADOWMAPS (target 4), so slot 0 in our pool maps + // to a kSHADOWMAPS slice the sun never writes -- safe to leave as a + // bookkeeping placeholder. + // + // Slots 4..7 are the engine's focus shadow range. Two-pass allocation: + // preferred slots first (avoiding 4..7 entirely so an actor appearance + // rarely needs to evict), then 4..7 as fallback for slots not + // currently claimed by an active focus actor. + const int32_t sunOff = Sun ? 1 : 0; + const int32_t shadowEnd = sunOff + shadowCount; + auto scanShadow = [&](auto reservablePolicy) -> int32_t { + for (int i = sunOff; i < shadowEnd; i++) { + if (reservablePolicy(i)) + continue; + if (IsFocusShadowSlot(i)) + continue; // actively held by focus right now + if (!Lights[i].Light) + return i; + } + return -1; + }; + if (shadowSlot) { + // First pass: skip the entire focus-reservable range so a focus + // actor appearance ideally finds those slots already empty. + if (int32_t i = scanShadow([](int32_t k) { return IsFocusShadowReservableSlot(k); }); i >= 0) + return i; + // Fallback: fill the unclaimed focus-reservable slots from the + // top down. Engine packs FocusShadowActors densely from slot + // kFocusShadowBaseSlotIndex upward (player first, then tracked + // NPCs in priority order), so slot 7 is the LAST to be claimed + // as focus count grows. Placing a point light there has the + // lowest probability of being evicted later. + for (int32_t i = kFocusShadowBaseSlotIndex + kFocusShadowMaxSlots - 1; i >= kFocusShadowBaseSlotIndex; --i) { + if (i >= shadowEnd) + continue; + if (IsFocusShadowSlot(i)) + continue; + if (!Lights[i].Light) + return i; + } + return -1; + } + // Converted lights live past the shadow range and never collide with + // focus slots (focus base is 4, converted base is >= sunOff + shadowCount > 4). + const int32_t convBase = shadowEnd; + for (int i = convBase; i < convBase + convertCount; i++) { + if (!Lights[i].Light) + return i; + } + return -1; + } + + int32_t LightContainer::FindLight(RE::BSShadowLight* light, int32_t shadowCount) const + { + // Search the full allocation range. Hook_OverwriteShadowMapIndex calls + // in for the sun too, so the sun pointer (in slot 0 when Sun=true) must + // be findable. The fallback to idx=0 on -1 silently corrupts slot 0, + // so the search range must match FindFreeIndex's allocation range. + const int32_t sunOff = Sun ? 1 : 0; + const int32_t maxIdx = sunOff + shadowCount; + for (int i = 0; i < maxIdx; i++) + if (Lights[i].Light == light) + return i; + return -1; + } + + std::uint32_t MaxShadowAccumIterationBound() + { + // Each entry advances idx by its shadowMapCount. Worst-case per + // light is the directional sun's cascade count (iNumSplits:Display, + // INI-capped at 3 by ShadowmapRasterizerFix). 4 is a defensive + // upper bound. With ShadowLightCount user-capped at 127 plus one + // sun bookkeeping slot, the walked index never exceeds + // (1 + 127) * 4 = 512; add a margin so a transient mismatch + // between live settings and an already-populated engine array + // doesn't tripwire iteration. + constexpr std::uint32_t kCascadesPerLight = 4; + constexpr std::uint32_t kMargin = 16; + const std::uint32_t lights = static_cast(std::max(1, s_settings.ShadowLightCount)); + return (lights + 1) * kCascadesPerLight + kMargin; + } + + // Verdict for a candidate shadow-array footprint vs the DXGI budget. + // "tight" = free VRAM below 512 MB or shadow array > 25% of budget. + // "over" = free VRAM below 128 MB or shadow array > 50% of budget. + // Driven by free headroom rather than shadow share because a small + // array next to a tight budget is just as risky as a huge one in a + // roomy budget. + struct VRAMVerdict + { + bool tight = false; + bool over = false; + ImVec4 colour{ 0.55f, 0.85f, 0.55f, 1 }; // green by default + }; + static VRAMVerdict EvaluateVRAMVerdict(std::uint64_t shadowBytes, std::uint64_t freeBytes, std::uint64_t budgetBytes) + { + constexpr std::uint64_t kTightFree = 512ull * 1024 * 1024; + constexpr std::uint64_t kOverFree = 128ull * 1024 * 1024; + VRAMVerdict v; + v.tight = freeBytes < kTightFree || shadowBytes * 4 > budgetBytes; + v.over = freeBytes < kOverFree || shadowBytes * 2 > budgetBytes; + v.colour = v.over ? ImVec4(0.95f, 0.35f, 0.35f, 1) : + v.tight ? ImVec4(0.95f, 0.85f, 0.25f, 1) : + ImVec4(0.55f, 0.85f, 0.55f, 1); + return v; + } + + // Reads kSHADOWMAPS's underlying Texture2D desc, bypassing the SRV's + // ViewDimension. Skyrim creates the SRV with a non-array view dimension + // even though the resource itself is a Texture2DArray, so reading + // `desc.Texture2DArray.ArraySize` from the SRV desc returns 0; only the + // texture's own ArraySize is reliable. Returns false on any failure + // stage; out param is left untouched. + static bool TryReadShadowTextureDesc(D3D11_TEXTURE2D_DESC& out) + { + auto* renderer = globals::game::renderer; + if (!renderer) + return false; + auto* srv = renderer->GetDepthStencilData() + .depthStencils[RE::RENDER_TARGET_DEPTHSTENCIL::kSHADOWMAPS] + .depthSRV; + if (!srv) + return false; + winrt::com_ptr resource; + srv->GetResource(resource.put()); + if (!resource) + return false; + winrt::com_ptr tex; + if (FAILED(resource->QueryInterface(IID_PPV_ARGS(tex.put())))) + return false; + D3D11_TEXTURE2D_DESC desc{}; + tex->GetDesc(&desc); + if (desc.ArraySize == 0) + return false; + out = desc; + return true; + } + + // Lazily verifies that the engine's actual kSHADOWMAPS slice count + // matches what SCM patched in. Self-healing: bails until the texture + // is readable, then early-returns. Cross-checks against the requested + // count and clamps the scheduler on mismatch so out-of-bounds slice + // indexing can't occur after a VRAM-exhaustion fallback. + void RefreshInstalledSlotCount() + { + if (s_installedSlotCount > 0) + return; + + D3D11_TEXTURE2D_DESC desc{}; + if (!TryReadShadowTextureDesc(desc)) + return; + + uint32_t actual = desc.ArraySize; + s_installedSlotCount = actual; + if (s_slotCountLogged) + return; + s_slotCountLogged = true; + if (s_requestedSlotCount && actual != s_requestedSlotCount) { + logger::warn( + "[SCM] Requested {} kSHADOWMAPS slots, GPU allocated {} -- " + "clamping scheduler to the actual count.", + s_requestedSlotCount, actual); + s_installedShadowLightCount = std::min(s_installedShadowLightCount, static_cast(actual)); + } else { + logger::info("[SCM] kSHADOWMAPS array verified: {} slots allocated", actual); + } + } + + uint32_t GetInstalledSlotCount() + { + // Lazy-refresh; cheap once verified. Fall back to the requested + // count when verification can't complete -- a non-zero slot count + // is needed for the cluster pipeline to engage shadow handling. + // Out-of-bounds slice indexes are hardware-clamped in D3D11, so a + // transient over-estimate yields stale shadow data rather than a + // crash. + RefreshInstalledSlotCount(); + return s_installedSlotCount > 0 ? s_installedSlotCount : s_requestedSlotCount; + } + + // Resolution actually used to allocate kSHADOWMAPS this session. Captured + // lazily from the real D3D11 texture geometry the first time it becomes + // readable -- NOT from the RE::Setting at Install() time. The engine's + // SkyrimPrefs.ini load happens after our PostPostLoad hook, so a snapshot + // at Install() catches the hardcoded default (e.g. 2048) before the + // user's INI value (e.g. 4096) is applied. Reading from the texture is + // the source of truth either way -- it reflects what was actually + // allocated, regardless of where the setting ended up. + static std::int32_t s_initialShadowMapResolution = 0; + + // kSHADOWMAPS footprint = w*h*bytesPerPixel*ArraySize. Per-slice cost + // is 64 MB at 4K D32_FLOAT; arrays grow linearly with ShadowLightCount. + // Returned info.valid is false only when both the DXGI budget query + // and the texture/INI fallback fail (rare). + VRAMInfo GetVRAMInfo() + { + VRAMInfo info{}; + + // DXGI budget. Prefer Menu's cached adapter; fall back to the + // device-derived path before Menu::Init() has run. + winrt::com_ptr adapter3; + if (auto* menu = Menu::GetSingleton()) + adapter3 = menu->GetDXGIAdapter3(); + if (!adapter3 && globals::d3d::device) { + winrt::com_ptr dxgiDevice; + if (SUCCEEDED(globals::d3d::device->QueryInterface(dxgiDevice.put()))) { + winrt::com_ptr dxgiAdapter; + if (SUCCEEDED(dxgiDevice->GetAdapter(dxgiAdapter.put()))) + dxgiAdapter->QueryInterface(adapter3.put()); + } + } + if (adapter3) { + DXGI_QUERY_VIDEO_MEMORY_INFO vmem{}; + HRESULT hr = adapter3->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &vmem); + if (SUCCEEDED(hr) && vmem.Budget > 0) { + info.currentUsageBytes = vmem.CurrentUsage; + info.budgetBytes = vmem.Budget; + } + } + + // kSHADOWMAPS geometry from the underlying texture (when readable). + D3D11_TEXTURE2D_DESC desc{}; + if (TryReadShadowTextureDesc(desc)) { + info.shadowWidth = desc.Width; + info.shadowHeight = desc.Height; + info.shadowSlices = desc.ArraySize; + // Latch the texture's allocated resolution as the canonical + // "what this session is using" value -- this is what the UI's + // restart-required indicator compares against. Once latched it + // doesn't move for the session (kSHADOWMAPS is allocated once). + if (s_initialShadowMapResolution == 0) + s_initialShadowMapResolution = static_cast(desc.Width); + // Default to 4 B/pixel (R32_TYPELESS / D32_FLOAT — the format + // Skyrim ships with) and override for stencil-packed variants. + std::uint32_t bytesPerPixel = 4; + switch (desc.Format) { + case DXGI_FORMAT_R32G8X24_TYPELESS: + case DXGI_FORMAT_D32_FLOAT_S8X24_UINT: + case DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS: + case DXGI_FORMAT_X32_TYPELESS_G8X24_UINT: + bytesPerPixel = 8; + break; + case DXGI_FORMAT_R16_TYPELESS: + case DXGI_FORMAT_D16_UNORM: + case DXGI_FORMAT_R16_UNORM: + bytesPerPixel = 2; + break; + default: + break; // 4 B fallback covers R24G8 and R32 families + } + info.bytesPerSlice = info.shadowWidth * info.shadowHeight * bytesPerPixel; + info.shadowArrayBytes = static_cast(info.bytesPerSlice) * info.shadowSlices; + } + + // INI-based fallback when the texture isn't readable yet (e.g. + // main menu, before BSShaderRenderTargets_Create). Resolution + // from SkyrimPrefs.ini, slot count from settings; assume the + // stock D32_FLOAT format (4 B/pixel). + if (info.bytesPerSlice == 0) { + std::uint32_t res = 4096; // SkyrimPrefs.ini default + if (auto* prefColl = RE::INIPrefSettingCollection::GetSingleton()) { + if (auto* setting = prefColl->GetSetting("iShadowMapResolution:Display")) { + int v = setting->GetInteger(); + if (v > 0) + res = static_cast(v); + } + } + info.shadowWidth = res; + info.shadowHeight = res; + info.bytesPerSlice = info.shadowWidth * info.shadowHeight * 4; + info.shadowSlices = static_cast(s_settings.ShadowLightCount); + info.shadowArrayBytes = static_cast(info.bytesPerSlice) * info.shadowSlices; + } + + // Budget and per-slice are independent so a partial answer still + // renders (budget alone shows VRAM headroom, per-slice alone shows + // projection from the INI fallback). + info.valid = info.budgetBytes > 0 || info.bytesPerSlice > 0; + + // One-shot log on first valid observation. Any caller trips it. + static bool s_loggedFirstValid = false; + if (info.valid && !s_loggedFirstValid) { + s_loggedFirstValid = true; + const std::uint64_t freeBytes = info.budgetBytes > info.currentUsageBytes ? info.budgetBytes - info.currentUsageBytes : 0; + const float arrayMB = static_cast(info.shadowArrayBytes) / (1024.f * 1024.f); + const float perSliceMB = static_cast(info.bytesPerSlice) / (1024.f * 1024.f); + const float budgetMB = static_cast(info.budgetBytes) / (1024.f * 1024.f); + const float usageMB = static_cast(info.currentUsageBytes) / (1024.f * 1024.f); + logger::info( + "[SCM] kSHADOWMAPS {}x{} x {} slices, {:.2f} MB/slice -> {:.1f} MB " + "(VRAM {:.1f}/{:.1f} MB used, ShadowLightCount={})", + info.shadowWidth, info.shadowHeight, info.shadowSlices, + perSliceMB, arrayMB, usageMB, budgetMB, s_settings.ShadowLightCount); + if (info.shadowArrayBytes > freeBytes) { + logger::warn( + "[SCM] Shadow texture array ({:.1f} MB) exceeds remaining VRAM budget " + "({:.1f} MB). Lower Shadow Light Count or iShadowMapResolution if you " + "see stutter or driver hitches.", + arrayMB, static_cast(freeBytes) / (1024.f * 1024.f)); + } + } + + return info; + } + + std::uint64_t ProjectShadowArrayBytes(std::uint32_t sliceCount) + { + auto info = GetVRAMInfo(); + if (!info.valid) + return 0; + return static_cast(info.bytesPerSlice) * sliceCount; + } + + // ========================================================================= + // BudgetEntry / BudgetTracker methods + // ========================================================================= + + static int64_t GetPerfCounter() + { + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + + int64_t t = (int64_t)counter.QuadPart; + + static int64_t freq = 0; + if (freq == 0) { + LARGE_INTEGER f; + QueryPerformanceFrequency(&f); + freq = f.QuadPart / 1000000; + } + + return t / freq; + } + + void BudgetEntry::BeginStep(int32_t /*step*/) + { + _startTime = GetPerfCounter(); + } + + void BudgetEntry::EndStep(int32_t step, int32_t helperCounter) + { + int64_t diff = GetPerfCounter() - _startTime; + + if (step == 0) { + Progress = static_cast(std::min(diff, (int64_t)0xFFFFFFFF)); + } else if (step == 1) { + diff += Progress; + int32_t ix = TrackedCount % kBudgetWindowSize; + Current -= Tracked[ix]; + Tracked[ix] = static_cast(std::min(diff, (int64_t)0xFFFFFFFF)); + Current += Tracked[ix]; + TrackedCount++; + LastTrackedHelper = helperCounter; + } + } + + bool BudgetEntry::IsExpired(int32_t helperCounter) const + { + return LastTrackedHelper < 0 || (helperCounter - LastTrackedHelper) >= 600; + } + + void BudgetTracker::Begin(int32_t step) + { + if (step == 0) { + _counter++; + // Amortise the GC: a periodic full-map walk that freed every + // expired BudgetEntry in one frame caused ~10s-cadence stutters + // (300 frames at 30 fps) because the heap freed dozens of + // unique_ptr back to back, taking a heap lock for + // each. Run incrementally every 30 frames (~0.5s at 60fps) and + // cap erasures per call so the cost spreads across many frames + // instead of spiking once. + if ((_counter % 30) == 0) + CleanupExpired(); + } + } + + void BudgetTracker::BeginLight(RE::BSShadowLight* light, int32_t step) + { + uint64_t key = reinterpret_cast(light); + auto& e = _map[key]; + if (!e) { + e = std::make_unique(); + e->Key = key; + } + e->BeginStep(step); + } + + void BudgetTracker::EndLight(RE::BSShadowLight* light, int32_t step) + { + uint64_t key = reinterpret_cast(light); + auto it = _map.find(key); + if (it == _map.end()) + return; + it->second->EndStep(step, _counter); + } + + int32_t BudgetTracker::GetCost(RE::BSShadowLight* light) const + { + uint64_t key = reinterpret_cast(light); + auto it = _map.find(key); + if (it == _map.end() || it->second->TrackedCount == 0) + return GetAverageCostUs(); // unknown light: fall back to fleet average + int32_t n = std::min(kBudgetWindowSize, it->second->TrackedCount); + return it->second->Current / std::max(1, n); + } + + void BudgetTracker::CleanupExpired() + { + ZoneScopedN("SCM::BudgetTracker::CleanupExpired"); + // Hard cap on erasures per call so a wave of expirations (e.g. the + // player crossed a cell boundary 600 frames ago and dozens of + // shadow lights all expire on the same tick) spreads its heap-free + // cost across many frames instead of stalling one frame. With + // kMaxErasePerCall=4 and Begin() calling this every 30 frames, the + // tracker can drain ~8 expired entries per second steady-state and + // up to 4 per call worst-case -- enough to keep the map bounded + // in practice without the periodic stutter. + constexpr size_t kMaxErasePerCall = 4; + size_t erased = 0; + for (auto it = _map.begin(); it != _map.end() && erased < kMaxErasePerCall;) { + if (it->second->IsExpired(_counter)) { + it = _map.erase(it); + ++erased; + } else { + ++it; + } + } + } + + int32_t BudgetTracker::GetAverageCostUs() const + { + int64_t sum = 0; + int32_t count = 0; + for (auto& [k, entry] : _map) { + int32_t n = std::min(kBudgetWindowSize, entry->TrackedCount); + if (n == 0) + continue; + sum += entry->Current / std::max(1, n); + count++; + } + return count > 0 ? static_cast(sum / count) : 0; + } + + // ========================================================================= + // Game accessor helpers + // + // Thin wrappers around game globals and engine functions. + // All REL::RelocationID pairs are (SE_id, AE_id). + // VR addresses verified against the VR address library CSV. + // ========================================================================= + + // ---------- globals ---------- + + static RE::ShadowSceneNode* GetShadowSceneNode() + { + static REL::RelocationID uid(513211, 390951); + return *reinterpret_cast(uid.address()); + } + + static RE::NiCamera* GetWorldCamera() + { + // world scene graph -> camera + static REL::RelocationID uid(528087, 415032); + auto* sg = *reinterpret_cast(uid.address()); + return sg ? sg->GetRuntimeData().camera.get() : nullptr; + } + + static bool GetSunBool1() + { + static REL::RelocationID uid(513201, 390932); + return *reinterpret_cast(uid.address()); + } + // Engine's per-frame count of focus shadow actors (player + tracked NPCs); + // max is iNumFocusShadow:Display (default 4). The engine renders one + // high-resolution shadow per entry into kSHADOWMAPS slots + // [g_focusShadowBaseSlotIndex .. +count). Used by the scheduler to + // dynamically reserve that range out of the point-light pool. + static int GetFocusShadowActorCount() + { + static REL::RelocationID uid(527703, 414625); + return *reinterpret_cast(uid.address()); + } + static bool GetSunBool2() + { + static REL::RelocationID uid(528095, 415040); + return *reinterpret_cast(uid.address()); + } + + static bool* GetFocusShadowSelected() + { + static REL::RelocationID uid(528096, 415041); + return reinterpret_cast(uid.address()); + } + static uint64_t* GetSunPtr() + { + static REL::RelocationID uid(528315, 415267); + return reinterpret_cast(uid.address()); + } + + // Current accumulated shadow slot (used as Accumulate() first arg). + static uint32_t* GetAccumLightSlot() + { + static REL::RelocationID uid(528091, 415036); + return reinterpret_cast(uid.address()); + } + // Running mask index counter (incremented each time a light is slotted). + static uint32_t* GetMaskIndex() + { + static REL::RelocationID uid(528091, 415036); + return reinterpret_cast(uid.address() + 4); + } + // Active shadow caster bitmask (ORed per slot). + static uint32_t* GetShadowMask() + { + static REL::RelocationID uid(528093, 415038); + return reinterpret_cast(uid.address()); + } + // Written back to the game at the end of scheduling. + static uint32_t* GetFrameLightCount() + { + static REL::RelocationID uid(528090, 415035); + return reinterpret_cast(uid.address()); + } + + // VR-only globals + static bool GetVRDrawShadows() + { + static REL::Offset uid{ 0x1ed3cb0 }; + return *reinterpret_cast(uid.address()); + } + static bool GetVRAccumFirst() + { + static REL::Offset uid{ 0x1ed4118 }; + return *reinterpret_cast(uid.address()); + } + static float GetVRDRSWidthRatio() + { + static REL::Offset bDis{ 0x3186d28 }, r{ 0x3186d14 }; + return *reinterpret_cast(bDis.address()) ? 1.0f : *reinterpret_cast(r.address()); + } + static float GetVRDRSHeightRatio() + { + static REL::Offset bDis{ 0x3186d28 }, r{ 0x3186d18 }; + return *reinterpret_cast(bDis.address()) ? 1.0f : *reinterpret_cast(r.address()); + } + + // ---------- engine function wrappers ---------- + + static void GameAccumulate(RE::BSShadowLight* light) + { + // BSShadowDirectionalLight::AccumulateFullFrustumCascades / unk_Accumulate + using F = void (*)(RE::BSShadowLight*); + static REL::Relocation func{ REL::RelocationID(100819, 107603) }; + func(light); + } + + static void GameSetupDirectionalLight(RE::BSShadowLight* light, RE::NiCamera* cam) + { + using F = void (*)(RE::BSShadowLight*, RE::NiCamera*); + static REL::Relocation func{ REL::RelocationID(100817, 107601) }; + func(light, cam); + } + + static void GameEnableLight(RE::ShadowSceneNode* ssn, RE::BSLight* light) + { + using F = void (*)(RE::ShadowSceneNode*, RE::BSLight*); + static REL::Relocation func{ REL::RelocationID(99708, 106342) }; + func(ssn, light); + } + + static void GameSetShadowCasterSlot(RE::ShadowSceneNode* ssn, RE::BSLight* light, uint32_t index, uint32_t unk) + { + using F = void (*)(RE::ShadowSceneNode*, RE::BSLight*, uint32_t, uint32_t); + static REL::Relocation func{ REL::RelocationID(99728, 106365) }; + func(ssn, light, index, unk); + } + + static void GameClearPortalVisibility(RE::BSPortalGraphEntry* entry) + { + using F = void (*)(RE::BSPortalGraphEntry*); + static REL::Relocation func{ REL::RelocationID(74395, 76119) }; + func(entry); + } + + static bool GamePortalHasSharedVisibility(RE::BSPortalGraphEntry* a, RE::BSPortalGraphEntry* b) + { + using F = bool (*)(RE::BSPortalGraphEntry*, RE::BSPortalGraphEntry*); + static REL::Relocation func{ REL::RelocationID(74397, 76121) }; + return func(a, b); + } + + static void GameClearGeometryList(RE::BSLight* light) + { + using F = void (*)(RE::BSLight*); + static REL::Relocation func{ REL::RelocationID(101298, 108285) }; + func(light); + } + + static bool GameIsLightAffectingSurface(RE::BSLightingShaderProperty* p, RE::BSLight* light) + { + using F = bool (*)(RE::BSLightingShaderProperty*, RE::BSLight*); + static REL::Relocation func{ REL::RelocationID(98902, 105550) }; + return func(p, light); + } + + static void GameApplyLensFlare(RE::BSLight* light) + { + // SE/AE only -- no VR equivalent (ID 100440) + if (REL::Module::IsVR()) + return; + using F = void (*)(RE::BSLight*); + static REL::Relocation func{ REL::RelocationID(100440, 107157) }; + func(light); + } + + // VR-only + static void GameVRPrepareShadowMaps(RE::BSLight* light) + { + using F = void (*)(RE::BSLight*); + static REL::Relocation func{ REL::Offset(0x1356e50) }; + func(light); + } + + static void GameVRAccumulateShadowMaps(RE::BSLight* light) + { + using F = void (*)(RE::BSLight*); + static REL::Relocation func{ REL::Offset(0x1357450) }; + func(light); + } + + static void GameFrustumOverlap(RE::NiCamera* cam, float* coord, float* r1, float* r2, float eps) + { + // Non-VR: (cam, coord, r1, r2, eps) + // VR: (cam, coord, r1, r2, eyeIndex, eps) -- pass 0xffffffff for combined frustum + static REL::Relocation addr{ REL::RelocationID(69265, 70632) }; + auto ptr = addr.address(); + if (REL::Module::IsVR()) { + using VR = void (*)(RE::NiCamera*, float*, float*, float*, uint32_t, float); + reinterpret_cast(ptr)(cam, coord, r1, r2, 0xffffffffu, eps); + } else { + using SE = void (*)(RE::NiCamera*, float*, float*, float*, float); + reinterpret_cast(ptr)(cam, coord, r1, r2, eps); + } + } + + // Convenience: runtime-aware shadow-light field accessor (SE vs VR RuntimeData differ). + // Usage: ShadowField(light, maskIndex) = 3; +#define ShadowField(light, member) \ + (REL::Module::IsVR() ? (light)->GetVRRuntimeData().member : (light)->GetRuntimeData().member) + + // Returns the culling process for the first shadow descriptor of a light. + static RE::BSCullingProcess* GetLightCullingProcess(RE::BSShadowLight* light) + { + return REL::Module::IsVR() ? light->GetVRRuntimeData().shadowmapDescriptors.front().cullingProcess : light->GetRuntimeData().shadowmapDescriptors.front().cullingProcess; + } + + // ========================================================================= + // Formula helpers + // + // SetupSceneFormula: called once per frame, sets camera/scene params. + // SetupLightFormula: called per candidate light, sets all light params. + // CalculateLightScore: evaluates s_formulaScore if available. + // ========================================================================= + + static void SetupSceneFormula(const RE::NiCamera* camera) + { + if (camera) { + FormulaHelper::SetParam(kFormulaParam_CameraX, camera->world.translate.x); + FormulaHelper::SetParam(kFormulaParam_CameraY, camera->world.translate.y); + FormulaHelper::SetParam(kFormulaParam_CameraZ, camera->world.translate.z); + } else { + FormulaHelper::SetParam(kFormulaParam_CameraX, 0.0); + FormulaHelper::SetParam(kFormulaParam_CameraY, 0.0); + FormulaHelper::SetParam(kFormulaParam_CameraZ, 0.0); + } + + FormulaHelper::SetParam(kFormulaParam_IsInterior, 0); + auto* plr = RE::PlayerCharacter::GetSingleton(); + if (plr) { + auto* cell = plr->parentCell; + if (cell && cell->IsInteriorCell()) + FormulaHelper::SetParam(kFormulaParam_IsInterior, 1); + } + + // Time of day from GameHour global + auto* cal = RE::Calendar::GetSingleton(); + if (cal) + FormulaHelper::SetParam(kFormulaParam_TimeOfDay, cal->GetHour()); + } + + static void SetupLightFormula(const RE::BSShadowLight* light, const RE::NiCamera* camera, int32_t index) + { + FormulaHelper::SetParam(kFormulaParam_LightConverted, 0.0); + FormulaHelper::SetParam(kFormulaParam_LightIndex, index); + FormulaHelper::SetParam(kFormulaParam_LightDisplacement, 0.0); // overridden per-entry in redraw interval loop + FormulaHelper::SetParam(kFormulaParam_PlayerLightDistance, 0.0); // overridden below after light position is known + FormulaHelper::SetParam(kFormulaParam_LightImportance, 0.0); // overridden per-entry in redraw interval loop; 0 in score formula + + // Temporal stickiness signals. Both derived from the slot pool in one + // pass: chosenLastFrame is the boolean kept for backward-compat with + // user formulas; framesSinceRender is a continuous age that decays to + // zero stickiness once the slot has been stale long enough to no + // longer represent a true rank-drift case. Sentinel 1e6 covers the + // "no slot" and "never rendered" branches so the default formula's + // max(0, 1 - age/window) decay term cleanly collapses to 0. + double chosenLastFrame = 0.0; + double framesSinceRender = 1e6; + { + const int32_t now = *globals::game::frameCounter; + for (int i = s_lights.PointLightFirst(); i < s_lights.PointLightEnd(s_settings.ShadowLightCount); i++) { + const auto& e = s_lights.Lights[i]; + if (e.Light != light) + continue; + chosenLastFrame = 1.0; + if (e.LastDrawnFrame >= 0) + framesSinceRender = static_cast(now - e.LastDrawnFrame); + break; + } + } + FormulaHelper::SetParam(kFormulaParam_LightChosenLastFrame, chosenLastFrame); + FormulaHelper::SetParam(kFormulaParam_LightFramesSinceRender, framesSinceRender); + + FormulaHelper::SetParam(kFormulaParam_LightNeverFades, light->lodFade ? 0.0 : 1.0); + FormulaHelper::SetParam(kFormulaParam_LightPortalStrict, light->portalStrict ? 1.0 : 0.0); + FormulaHelper::SetParam(kFormulaParam_LightNS, 0.0); + + // Spot detection + cone-aware visibility prior (option 1 from spot + // preservation analysis). Non-spots get spotvisible=1 so existing + // omni-tuned formulas are unaffected. For spots, we read last + // frame's UpdateCamera verdict (frustumCull / lodDimmer) -- the + // score runs BEFORE this frame's validation pass updates those, + // but cameras move continuously so last-frame's cone-vs-frustum + // is a strong predictor of this-frame's. Trading a one-frame lag + // for not double-calling UpdateCamera is a worthwhile cost since + // the score is a preference, not a gate. + const bool isSpot = (skyrim_cast(light) != nullptr); + double spotVisible = 1.0; // default for non-spots: always "visible" + if (isSpot) { + // frustumCull == 0 means "in frustum"; engine sets 0xff when + // cone-vs-frustum rejects. lodDimmer > 0 means the LOD fader + // hasn't zeroed the light. Both must hold for a spot to count + // as plausibly visible. + // Note: the engine field is misspelled "frustrumCull" in the SDK + // (matches Bethesda's original symbol). 0 = visible, 0xff = culled. + const bool inFrustum = (light->frustrumCull == 0); + const bool lodLit = (light->lodDimmer > 0.0f); + spotVisible = (inFrustum && lodLit) ? 1.0 : 0.0; + } + FormulaHelper::SetParam(kFormulaParam_LightIsSpot, isSpot ? 1.0 : 0.0); + FormulaHelper::SetParam(kFormulaParam_LightSpotVisible, spotVisible); + + float x, y, z; + + auto* nilight = light->light.get(); + if (nilight) { + FormulaHelper::SetParam(kFormulaParam_LightIntensity, nilight->GetLightRuntimeData().fade); + FormulaHelper::SetParam(kFormulaParam_LightRadius, nilight->GetLightRuntimeData().radius.x); + FormulaHelper::SetParam(kFormulaParam_LightR, nilight->GetLightRuntimeData().diffuse.red); + FormulaHelper::SetParam(kFormulaParam_LightG, nilight->GetLightRuntimeData().diffuse.green); + FormulaHelper::SetParam(kFormulaParam_LightB, nilight->GetLightRuntimeData().diffuse.blue); + FormulaHelper::SetParam(kFormulaParam_LightAmbientR, nilight->GetLightRuntimeData().ambient.red); + FormulaHelper::SetParam(kFormulaParam_LightAmbientG, nilight->GetLightRuntimeData().ambient.green); + FormulaHelper::SetParam(kFormulaParam_LightAmbientB, nilight->GetLightRuntimeData().ambient.blue); + x = nilight->world.translate.x; + y = nilight->world.translate.y; + z = nilight->world.translate.z; + + if (s_settings.PromoteNormalToShadow) + FormulaHelper::SetParam(kFormulaParam_LightNS, s_shadowConvert.find(nilight) != s_shadowConvert.end() ? 1.0 : 0.0); + } else { + FormulaHelper::SetParam(kFormulaParam_LightIntensity, 0.0); + FormulaHelper::SetParam(kFormulaParam_LightRadius, 0.0); + FormulaHelper::SetParam(kFormulaParam_LightR, 1.0); + FormulaHelper::SetParam(kFormulaParam_LightG, 1.0); + FormulaHelper::SetParam(kFormulaParam_LightB, 1.0); + FormulaHelper::SetParam(kFormulaParam_LightAmbientR, 1.0); + FormulaHelper::SetParam(kFormulaParam_LightAmbientG, 1.0); + FormulaHelper::SetParam(kFormulaParam_LightAmbientB, 1.0); + x = light->worldTranslate.x; + y = light->worldTranslate.y; + z = light->worldTranslate.z; + } + + FormulaHelper::SetParam(kFormulaParam_LightX, x); + FormulaHelper::SetParam(kFormulaParam_LightY, y); + FormulaHelper::SetParam(kFormulaParam_LightZ, z); + + float camx = camera ? camera->world.translate.x : (float)FormulaHelper::GetParam(kFormulaParam_CameraX); + float camy = camera ? camera->world.translate.y : (float)FormulaHelper::GetParam(kFormulaParam_CameraY); + float camz = camera ? camera->world.translate.z : (float)FormulaHelper::GetParam(kFormulaParam_CameraZ); + + float dx = x - camx, dy = y - camy, dz = z - camz; + FormulaHelper::SetParam(kFormulaParam_LightDistance, sqrtf(dx * dx + dy * dy + dz * dz)); + + // Player-to-light distance: ensures third-person shadow maps redraw when the + // player character is inside a light's radius even if the camera is outside. + double playerLightDist = FormulaHelper::GetParam(kFormulaParam_LightDistance); + auto* plr = RE::PlayerCharacter::GetSingleton(); + if (plr) { + auto pp = plr->GetPosition(); + float pdx = x - pp.x, pdy = y - pp.y, pdz = z - pp.z; + playerLightDist = static_cast(sqrtf(pdx * pdx + pdy * pdy + pdz * pdz)); + } + FormulaHelper::SetParam(kFormulaParam_PlayerLightDistance, playerLightDist); + } + + static double CalculateLightScore(const RE::BSShadowLight* light, const RE::NiCamera* camera, int32_t index) + { + SetupLightFormula(light, camera, index); + + if (s_formulaScore) + return s_formulaScore->Calculate(); + + return 0.0; + } + + // ========================================================================= + // Shadow map content hash for cached-shadow-map detection + // ========================================================================= + + /// Mixes a 32-bit value into a running 64-bit hash. boost::hash_combine + /// constants -- the magic number 0x9e3779b9 is the golden-ratio reciprocal, + /// chosen for good bit distribution. Fast (a few ALU ops) and we don't + /// need cryptographic strength -- only that distinct inputs map to + /// distinct outputs with very high probability. + static inline std::uint64_t HashCombine(std::uint64_t h, std::uint32_t v) noexcept + { + return h ^ (static_cast(v) + 0x9e3779b9ull + (h << 6) + (h >> 2)); + } + static inline std::uint64_t HashCombineFloat(std::uint64_t h, float f) noexcept + { + return HashCombine(h, std::bit_cast(f)); + } + + /// Quantize a float to a step size before hashing. Skyrim's kFlicker / + /// kPulse light flags oscillate animated torches by sub-unit position / + /// radius amounts every frame. Bit-exact hashing on those oscillations + /// produces a fresh hash every frame, defeating cache validity. Quantizing + /// at sub-pixel-precision thresholds folds imperceptible animations into + /// a stable hash bucket so the cached-shadow priority demotion fires + /// correctly for visually-unchanging lights. + static inline float QuantizeFloat(float f, float step) noexcept + { + return std::round(f / step) * step; + } + + /// Hash of inputs that determine a shadow map's content: the light's + /// pose + radius, and each caster's worldBound + identity. worldBound + /// tracks rigid motion and BSDynamicTriShape vertex updates, so mesh + /// data isn't inspected directly. Identical hashes across frames mean + /// the cached slot is byte-for-byte current -- caller can skip the + /// redraw. Returns 0 only on null light/NiLight (sentinel for "never + /// rendered"); HashCombine constants make a real-data 0 essentially + /// impossible. + + static std::uint64_t ComputeShadowGeomHash(RE::BSShadowLight* light) + { + if (!light) + return 0; + auto* ni = light->light.get(); + if (!ni) + return 0; + std::uint64_t h = 0x9e3779b97f4a7c15ull; // arbitrary nonzero seed + + // Quantization thresholds: tuned to be one to two orders of + // magnitude below perceptible difference in the rendered shadow. + // kPosStep = 1.0 game unit (~1.4 cm world space; sub-texel + // at typical 2048 shadow res * 500 unit light radius) + // kRotStep = 0.01 in matrix entries (~0.5 degrees) + // kRadiusStep = 1.0 unit (well under any visible frustum + // resize from torch pulse animations) + constexpr float kPosStep = 1.0f; + constexpr float kRotStep = 0.01f; + constexpr float kRadiusStep = 1.0f; + + // Light pose + const auto& t = ni->world.translate; + h = HashCombineFloat(h, QuantizeFloat(t.x, kPosStep)); + h = HashCombineFloat(h, QuantizeFloat(t.y, kPosStep)); + h = HashCombineFloat(h, QuantizeFloat(t.z, kPosStep)); + const auto& r = ni->world.rotate; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + h = HashCombineFloat(h, QuantizeFloat(r.entry[i][j], kRotStep)); + // Light radius (NiPointLight uses .x; spotlights use direction in + // rotation matrix already hashed above). + const auto& rtd = ni->GetLightRuntimeData(); + h = HashCombineFloat(h, QuantizeFloat(rtd.radius.x, kRadiusStep)); + // Caster set + each caster's worldBound (engine-updated). + for (auto& nip : light->geomList) { + auto* ts = nip.get(); + if (!ts) + continue; + const auto raw = reinterpret_cast(ts); + h = HashCombine(h, static_cast(raw)); + h = HashCombine(h, static_cast(raw >> 32)); + const auto& wb = ts->worldBound; + h = HashCombineFloat(h, QuantizeFloat(wb.center.x, kPosStep)); + h = HashCombineFloat(h, QuantizeFloat(wb.center.y, kPosStep)); + h = HashCombineFloat(h, QuantizeFloat(wb.center.z, kPosStep)); + h = HashCombineFloat(h, QuantizeFloat(wb.radius, kRadiusStep)); + } + return h; + } + + // ========================================================================= + // Light enable / disable helpers + // ========================================================================= + + /// Removes `light` from s_normalConvert and clears its geometry list. + /// No-op if the light is not in the list. + static void EraseFromConvertList(RE::BSShadowLight* light) + { + for (auto it = s_normalConvert.begin(); it != s_normalConvert.end(); ++it) { + if (it->light == light) { + GameClearGeometryList(light); + s_normalConvert.erase(it); + return; + } + } + } + + static void DisableLight(RE::BSShadowLight* light) + { + EraseFromConvertList(light); + auto* cull = light->cullingProcess; + if (cull && cull->portalGraphEntry) + GameClearPortalVisibility(reinterpret_cast(cull->portalGraphEntry)); + light->ReturnShadowmaps(); + } + + // Activates a light as a normal (non-shadow) light by inserting it into + // the scene's active-light list without allocating a shadow slot. + // + // Two paths: "already-converted re-enable" (just GameEnableLight) and + // "first conversion this session" (ReturnShadowmaps + portal-clear + + // track in s_normalConvert + GameEnableLight). Tracy sub-zones split + // the cost so the next capture distinguishes the steady-state cost + // (re-enable only) from the cost of a fresh conversion. + static void ConvertLight(RE::BSShadowLight* light, RE::ShadowSceneNode* ssn, bool isNS) + { + // Already converted: just re-enable so geometry picks it up this frame. + for (auto& c : s_normalConvert) { + if (c.light == light) { + ZoneNamedN(zReEnable, "SCM::Engine::ConvertLight::ReEnable", true); + GameEnableLight(ssn, light); + return; + } + } + + // First conversion this session: release shadow resources, register. + ZoneNamedN(zFirstConv, "SCM::Engine::ConvertLight::FirstConvert", true); + auto* cull = GetLightCullingProcess(light); + if (cull && cull->portalGraphEntry) + GameClearPortalVisibility(reinterpret_cast(cull->portalGraphEntry)); + light->ReturnShadowmaps(); + + s_normalConvert.push_back({ light, isNS }); + GameEnableLight(ssn, light); + } + + // Activates a non-sun shadow light into slot `slotIndex`. + static void EnableLight(RE::BSShadowLight* light, RE::NiCamera* camera, + RE::ShadowSceneNode* ssn, int slotIndex) + { + // Remove from conversion list if it was previously converted to normal. + EraseFromConvertList(light); + + // Focus shadow handling. Gated on s_focusShadowSlots so we only run + // the engine's focus accumulate when ScheduleShadowCasters has + // reserved [kFocusShadowBaseSlotIndex .. +s_focusShadowSlots) this + // frame -- without that reservation the engine would write focus + // depth into texture slices currently held by point lights. With + // it, extended mode (ShadowLightCount > 4) is safe; the previous + // blanket `<= 4` gate is replaced by the reservation contract. + if (s_focusShadowSlots > 0) { + bool drawFocus = ShadowField(light, drawFocusShadows); + if (drawFocus || (!*GetFocusShadowSelected() && light->GetIsFrustumOrDirectionalLight())) { + GameSetupDirectionalLight(light, camera); + GameAccumulate(light); + if (REL::Module::IsVR()) { + for (auto& desc : light->GetVRRuntimeData().focusShadowmapDescriptors) { + desc.vrRenderTarget[0] = RE::RENDER_TARGET_DEPTHSTENCIL::kNONE; + desc.vrRenderTarget[1] = RE::RENDER_TARGET_DEPTHSTENCIL::kNONE; + } + } + ShadowField(light, drawFocusShadows) = true; + *GetFocusShadowSelected() = true; + *GetSunPtr() = reinterpret_cast(light); + } + } + + GameEnableLight(ssn, light); + GameSetShadowCasterSlot(ssn, light, *GetAccumLightSlot(), 1); + + { + uint32_t mi = *GetMaskIndex(); + ShadowField(light, maskIndex) = mi; + *GetMaskIndex() = mi + 1; + } + + // Projected bounding box for shadow map region. + auto* nilight = light->light.get(); + if (nilight) { + auto lpos = nilight->world.translate; + auto cpos = camera->world.translate; + auto delta = lpos - cpos; + float dx = delta.x, dy = delta.y, dz = delta.z; + float dist = lpos.GetDistance(cpos); + float radius = nilight->GetLightRuntimeData().radius.x; + + float left, right, top, bottom; + + if (dist >= radius + camera->GetNearPlane()) { + float inv = 1.0f / dist; + float coord[4] = { + lpos.x - dx * radius * inv, + lpos.y - dy * radius * inv, + lpos.z - dz * radius * inv, + radius + }; + float r1[2], r2[2]; + GameFrustumOverlap(camera, coord, r1, r2, 0.00001f); + + float vw = (float)*globals::game::viewWidth; + float vh = (float)*globals::game::viewHeight; + if (REL::Module::IsVR()) { + vw *= GetVRDRSWidthRatio(); + vh *= GetVRDRSHeightRatio(); + } + + left = (r1[0] + 1.0f) * 0.5f * vw; + right = (r2[0] + 1.0f) * 0.5f * vw; + top = (1.0f - (r1[1] + 1.0f) * 0.5f) * vh; + bottom = (1.0f - (r2[1] + 1.0f) * 0.5f) * vh; + } else { + // Light contains the camera: use full screen. + *GetShadowMask() |= 1u << *GetAccumLightSlot(); + left = right = top = bottom = -1.0f; + } + + ShadowField(light, projectedBoundingBox) = + RE::NiRect((uint32_t)left, (uint32_t)right, (uint32_t)top, (uint32_t)bottom); + } + + // Accumulate into shadow slot. + { + uint32_t idx = static_cast(slotIndex); + light->Accumulate(idx, idx, nullptr); + *GetAccumLightSlot() += light->shadowMapCount; + } + + // Extended mode: pre-set kNONE renderTarget so RenderCascade re-runs + // its slot-allocation block (where Hook_OverwriteShadowMapIndex + // overrides the global counter with our slot index). Without this, + // RenderCascade keeps the slot from a prior frame and lights not + // redrawn this frame would corrupt another light's shadow map. + // Pool index maps 1:1 to texture slot; slice 0 stays unused. + if (s_settings.ShadowLightCount > 4) { + int32_t idx = s_lights.FindLight(light, s_settings.ShadowLightCount); + if (idx < 0) + idx = 0; + if (REL::Module::IsVR()) { + for (auto& desc : light->GetVRRuntimeData().shadowmapDescriptors) { + desc.renderTarget = RE::RENDER_TARGET_DEPTHSTENCIL::kNONE; + desc.shadowmapIndex = static_cast(idx); + } + } else { + for (auto& desc : light->GetRuntimeData().shadowmapDescriptors) { + desc.renderTarget = RE::RENDER_TARGET_DEPTHSTENCIL::kNONE; + desc.shadowmapIndex = static_cast(idx); + } + } + } + + // Only apply lens flare when lensFlareData is non-null; calling it on parabolic lights + // (null lensFlareData) registers them into the lens flare system, causing a crash + // in the lens flare pass when it tries to dereference the null sprite data. + if (light->lensFlareData) + GameApplyLensFlare(light); + } + + // ========================================================================= + // Main shadow caster manager + // + // Replaces the game's CalculateActiveShadowCasterLights entirely. + // Runs via stl::detour_thunk; obtains all inputs from game globals. + // ========================================================================= + + // Lightweight per-frame candidate entry used during scheduling. + // + // After the validation pass, exactly one of {chosen, excess, invalid} + // is true (or none if it's the sun, which is processed separately). + struct CandidateLight + { + RE::BSShadowLight* light{ nullptr }; + double score{ 0.0 }; + bool sun{ false }; + bool chosen{ false }; // valid + within ShadowLightCount budget + bool excess{ false }; // valid but over budget (convert or disable) + bool invalid{ false }; // shorthand: invalidCamera || invalidPortal + bool invalidCamera{ false }; // UpdateCamera returned false -- shorthand for + // branches that don't care which sub-reason + bool invalidPortal{ false }; // portal cull: light's cell not visible from + // camera's cell. Must DisableLight; converting + // routes through cluster lighting which has no + // portal awareness and would bleed through walls. + + // Sub-reasons for invalidCamera, recovered from engine side-band flags: + // frustrumCull == 0xff -> off-screen, ConvertLight wasted -> drop + // lodDimmer == 0.0f -> past LOD fade end, still visible -> ConvertLight + // (resets lodDimmer so cluster lighting picks it up) + // Both can fire together; frustum-out wins (contribution is zero either way). + bool invalidFrustum{ false }; // BSMultiBoundSphere::WithinFrustum / cone-frustum cull + bool invalidLod{ false }; // engine's LOD-fade zeroed lodDimmer + }; + + static void ScheduleShadowCasters() + { + ZoneScopedN("SCM::ScheduleShadowCasters"); + // Per-frame diagnostic counters; emitted via TracyPlot at function exit. + s_schedDiag = SchedDiagCounters{}; + // VR calls CalculateAndDrawShadowCasterLights twice per frame (once per + // eye). Block the second call: s_lights isn't reentrancy-safe. + static std::atomic s_inSchedule{ false }; + if (s_inSchedule.exchange(true, std::memory_order_acquire)) + return; + struct Guard + { + ~Guard() { s_inSchedule.store(false, std::memory_order_release); } + } guard; + + // VR display guard: skip scheduling when the HMD display is not active. + if (REL::Module::IsVR() && !GetVRDrawShadows()) + return; + + auto* ssn = GetShadowSceneNode(); + auto* camera = GetWorldCamera(); + if (!ssn || !camera) + return; + + // Read the engine's per-frame focus-shadow actor count and reserve + // matching pool slots. Eject any point lights that occupy a slot the + // engine now claims for focus rendering -- the displaced lights are + // reassigned to a free slot or fall through to the existing excess + // path. When the count drops, the slots naturally rejoin the pool's + // FindFreeIndex range on the next allocation. + s_focusShadowSlots = std::clamp(GetFocusShadowActorCount(), 0, kFocusShadowMaxSlots); + for (int i = kFocusShadowBaseSlotIndex; i < kFocusShadowBaseSlotIndex + s_focusShadowSlots && i < s_lights.Size; ++i) { + if (s_lights.Lights[i].Light) + s_lights.Lights[i].Clear(); + } + + // Do NOT clear shadowLightsAccum or reset the slot counter here. The + // outer CalculateAndDrawShadowCasterLights calls ResetCalculatedShadow- + // CasterLights before our hook fires, and that function clears the + // array, resets the counter, AND installs the sun at slot 0. Re- + // clearing here wipes the sun (sun->Accumulate is the focus vfunc, + // not a slot allocator) and the engine then skips the directional + // cascade pass entirely. + + s_budget.Begin(0); + + int doneLightCount = 0; + RE::BSShadowLight* sunLight = nullptr; + + // ---- Sun / directional light ---- + if (!GetSunBool2()) { + auto* sun = ssn->GetRuntimeData().sunShadowDirLight; + if (sun) { + static REL::Relocation vrUpdateFlag{ REL::Offset(0x1ed62f8) }; + uint8_t vrFlag = REL::Module::IsVR() ? static_cast(*vrUpdateFlag) + 1 : 0; + sun->Accumulate(*GetAccumLightSlot(), 0, nullptr, vrFlag); + + if (sun->lensFlareData && !REL::Module::IsVR()) + GameApplyLensFlare(sun); + + if (REL::Module::IsVR() && !GetVRAccumFirst()) { + GameVRPrepareShadowMaps(sun); + GameVRAccumulateShadowMaps(sun); + } + + sunLight = sun; + } + } + + // Extended mode: scrub drawFocusShadows on every active light and the + // sun. A stale flag on a parabolic (point/spot) light occupying a + // kSHADOWMAPS slot in [4..7] sends BSShadowParabolicLight::Render + // into its focus-shadow loop on a non-directional light and CTDs. + // Mirrors Intellightent's mitigation (see main.cpp:1411-1420); the + // byte patches at SetupResources are belt-and-braces for the engine's + // global gate, this is belt-and-braces for the per-light flag. + if (s_settings.ShadowLightCount > 4) { + for (auto& sp : ssn->GetRuntimeData().activeShadowLights) { + if (auto* l = sp.get()) + ShadowField(l, drawFocusShadows) = false; + } + if (auto* sun2 = ssn->GetRuntimeData().sunShadowDirLight) + ShadowField(sun2, drawFocusShadows) = false; + } + + *GetSunPtr() = 0; + + // ---- Score all candidate lights ---- + // Reuse a static vector so we don't allocate per frame -- the + // scheduler runs every frame and the candidate list is the same + // shape size each call (a few hundred lights at most). + static std::vector candidates; + + { + ZoneScopedN("SCM::ScoreCandidates"); + SetupSceneFormula(camera); + + candidates.clear(); + candidates.reserve(ssn->GetRuntimeData().activeShadowLights.size()); + + int32_t tmpIndex = 0; + for (auto& sp : ssn->GetRuntimeData().activeShadowLights) { + auto* l = sp.get(); + if (!l || l == sunLight) + continue; + auto& c = candidates.emplace_back(); + c.light = l; + c.sun = false; + c.score = CalculateLightScore(l, camera, tmpIndex++); + } +#ifdef TRACY_ENABLE + char buf[32]; + const int n = snprintf(buf, sizeof(buf), "candidates=%zu", candidates.size()); + if (n > 0) + ZoneText(buf, static_cast(n)); +#endif + } + + // Validation, redraw-interval scoring, and RedrawFrame marking all + // happen before the atomic loop. Tracy capture analysis showed this + // block dominates SCM::ScheduleShadowCasters (98%+ of the function's + // runtime), so a dedicated zone scopes that cost separately from + // ScoreCandidates and ScheduleLoop. Named variant because the + // enclosing function already declares a ZoneScopedN. + ZoneNamedN(zoneValBudget, "SCM::ValidateAndScheduleBudget", true); + + // Apply debug pins: bias scoring so pinned-shadow lights sort to the + // top (forced into the chosen pool up to ShadowLightCount) and + // pinned-convert lights sort to the bottom (forced into the excess pool + // where ConvertLight runs unconditionally — see c.excess branch below). + // Pin sets are mutually exclusive (SetPinned* enforces that), but if a + // stale entry slips through, pin-shadow wins because the bias is checked + // first. + for (auto& c : candidates) { + auto key = reinterpret_cast(c.light); + if (s_pinShadow.count(key)) + c.score += 1e15; + else if (s_pinConvert.count(key)) + c.score -= 1e15; + } + + // Sort descending by score (highest priority first); sun always first. + std::sort(candidates.begin(), candidates.end(), + [](const CandidateLight& a, const CandidateLight& b) { + if (a.sun != b.sun) + return a.sun; + return a.score > b.score; + }); + + // ---- Validation pass (no game mutations) ---- + // + // Mirrors Intellightent's per-iteration validation gates. Splitting + // validation from mutation lets us defer all game-state changes + // (DisableLight / ConvertLight / EnableLight) to a single atomic loop + // later, eliminating the dangling-pointer crash window where mutations + // in an earlier phase invalidated raw pointers held in s_lights[]. + // + // Slot 0 is reserved for the sun; point lights fill slots 1..ShadowLightCount. + // Do not count the sun against ShadowLightCount -- it uses focus cascade DSV slots, + // not parabolic point-light slots. + auto* globalCull = *reinterpret_cast( + *reinterpret_cast( + REL::RelocationID(528077, 415022).address())); + + int wantCount = 0; + + // Per-candidate UpdateCamera vfunc + portal-graph visibility walk + // + chosen/excess tagging. Captured separately so memoization or + // caching of UpdateCamera/portal verdicts can be measured. + { + ZoneNamedN(zoneCandVal, "SCM::CandidateValidation", true); + for (auto& c : candidates) { + auto* l = c.light; + // UpdateCamera (vfunc 16, +0x80) is the engine's type-aware visibility + // test. Verified via Ghidra (BSShadowParabolicLight_UpdateCamera at + // 0x14151b620 in 1.6.1170, 0x14132ddf0 in 1.6.640, 0x141370c80 in VR): + // + // - BSShadowParabolicLight: TWO cull conditions, both setting + // frustrumCull=0xff: + // (1) BSMultiBoundSphere::WithinFrustum (BSMultiBoundShape + // vfunc 0x29) -- sphere(niLight.pos, niLight.Radius.x) + // vs camera frustum. Geometrically correct; + // failure means no visible pixel can be lit because the + // light's bounding sphere doesn't touch the camera frustum. + // The radius source matches what the cluster builder reads + // (LightLimitFix.cpp's `runtimeData.radius.x`). + // (2) Shadow-distance LOD -- if (lodFade flag set on + // BSShadowLight) AND + // ((camDist^2 - radius^2) * camera.LodAdjust) > + // ShadowDistanceSquared_Current => cull. + // ShadowDistanceSquared_Current = fShadowDistance^2 + // (8000^2 outdoors, 3000^2 indoors by default). + // This is NOT a visibility test -- it's "skip per-light + // shadow rendering at this distance". A light past + // shadow distance can still be IN the camera frustum and + // illuminating visible pixels via cluster lighting. + // + // - BSShadowFrustumLight: cone-vs-frustum test (cone-aware so an + // off-screen spot pointing INTO the frustum is correctly kept). + // + // - BSShadowDirectionalLight: cascades, separate code path. + // + // Implication for SCM: a `frustrumCull != 0` verdict does NOT mean + // "geometrically off-screen". The convertOrDisable path below treats + // all c.invalid cases uniformly (omnis convert, spots disable, portal + // disable) so distant lights past shadow distance still reach the + // cluster pipeline. The cluster builder's own + // `(color * fade) > 1e-4 && radius > 1e-4` filter discards lights + // that genuinely don't contribute. + if (!l->UpdateCamera(camera)) { + c.invalidCamera = true; + c.invalid = true; + // Recover the sub-reason from the engine's side-band flags. + // Both can be true (a light off-screen AND LOD-faded); + // recorded as independent bits for analysis. Action loop + // below treats frustum-out as terminal (drop) and + // LOD-faded-in-frustum as convert. + c.invalidFrustum = (l->frustrumCull != 0); + c.invalidLod = (l->lodDimmer == 0.0f); + continue; + } + // Portal culling only applies in interior cells where a portal graph exists. + // Lights with no culling process (e.g. WSU spotlights outside cell bounds) + // or no portal are unconditionally visible; skip the check for them. + auto* cull = GetLightCullingProcess(l); + if (cull) { + auto* portal = reinterpret_cast(cull->portalGraphEntry); + if (portal) { + auto* gPortal = globalCull ? reinterpret_cast(globalCull->portalGraphEntry) : nullptr; + if (gPortal && !GamePortalHasSharedVisibility(gPortal, portal)) { + c.invalidPortal = true; + c.invalid = true; + continue; + } + } + } + + // Effective point-light capacity excludes the engine-claimed + // focus shadow slots; excess candidates fall through to the + // existing convert/disable path. + if (wantCount < s_settings.ShadowLightCount - s_focusShadowSlots) { + c.chosen = true; + wantCount++; + } else { + c.excess = true; + } + } + + // Tracy candidate breakdown: emits per-frame so a capture can be + // queried alongside the per-action counters to verify the math + // (chosen + excess + invalid_camera + invalid_portal == total). + for (auto& c : candidates) { + s_schedDiag.candidates_total++; + if (c.chosen) + s_schedDiag.candidates_chosen++; + if (c.excess) + s_schedDiag.candidates_excess++; + if (c.invalidCamera) + s_schedDiag.candidates_invalid_camera++; + if (c.invalidPortal) + s_schedDiag.candidates_invalid_portal++; + // Sub-reason breakdown of invalidCamera. A single light may + // be both frustum-out AND LOD-faded -- both bits are counted + // so the sum can exceed candidates_invalid_camera. The + // "other" bucket catches UpdateCamera failures where the + // engine cleared frustrumCull and left lodDimmer > 0 (rare + // edge cases like internal state changes). + if (c.invalidCamera) { + if (c.invalidFrustum) + s_schedDiag.candidates_invalid_frustum++; + if (c.invalidLod) + s_schedDiag.candidates_invalid_lod++; + if (!c.invalidFrustum && !c.invalidLod) + s_schedDiag.candidates_invalid_other++; + } + } + } // end SCM::CandidateValidation + + // Pool membership update: drop expired pointers, drop unchosen, + // add newly chosen, sync sun slot. + { + ZoneNamedN(zonePoolMem, "SCM::UpdatePoolMembership", true); + // ---- Sync s_lights (our active pool) ---- + // + // First drop entries whose pointers are no longer in the scene's + // activeShadowLights (game-side may have freed them since last frame). + // This protects subsequent slot-stability lookups from dereferencing + // dangling pointers. + std::unordered_set aliveSet; + { + auto& alive = ssn->GetRuntimeData().activeShadowLights; + aliveSet.reserve(alive.size() + 1); + if (sunLight) + aliveSet.insert(sunLight); + for (auto& sp : alive) + if (auto* l = sp.get()) + aliveSet.insert(l); + } + for (int i = 0; i < s_lights.Size; i++) { + if (!s_lights.Lights[i].Light) + continue; + if (aliveSet.find(s_lights.Lights[i].Light) == aliveSet.end()) { + s_schedDiag.reconciliation_clears++; + s_lights.Lights[i].Clear(); + } + } + + // ---- Sync s_normalConvert (converted-to-non-shadow set) ---- + // + // Two-tier filter: + // + // Tier 1: drop entries the engine has removed from BOTH active + // lists. Hook_ConvertLights_Remove fires on individual RemoveLight + // calls but the engine's bulk cell-teardown path bypasses it, so + // this is our safety net for dangling pointers. + // + // Tier 2: drop entries that are functionally dead -- still in + // activeShadowLights / activeLights (because GameEnableLight from + // ConvertLight activates an entry that the engine never + // auto-deactivates), but with fade=0 / lodDimmer=0 / null NiLight + // so addLight in LightLimitFix would skip them anyway. + // + // Without tier 2 the set grows unbounded across a session: every + // converted light stays pinned in s_normalConvert until the engine + // triggers a removal we can hook. Heavy modlists hit 400+ entries, + // keeping freed-then-recycled BSLight memory referenced by + // downstream pass captures longer than necessary. The criteria + // mirror addLight's discard filter -- entries failing it + // contribute nothing to the cluster or engine lighting paths and + // have no business staying in our set. + if (!s_normalConvert.empty()) { + std::unordered_set normalAlive; + normalAlive.reserve(aliveSet.size() + ssn->GetRuntimeData().activeLights.size()); + for (auto* p : aliveSet) + normalAlive.insert(static_cast(p)); + for (auto& sp : ssn->GetRuntimeData().activeLights) + if (auto* l = sp.get()) + normalAlive.insert(l); + + const std::size_t before = s_normalConvert.size(); + std::erase_if(s_normalConvert, [&](const ConvertedLight& c) { + // Tier 1: dangling / engine-removed. + if (!c.light || normalAlive.find(static_cast(c.light)) == normalAlive.end()) + return true; + // Tier 2: functionally dead. Cheap derefs only -- no + // virtual calls or extra hash lookups. + auto* niLight = c.light->light.get(); + if (!niLight) + return true; + const auto& rt = niLight->GetLightRuntimeData(); + const float colorSum = rt.diffuse.red + rt.diffuse.green + rt.diffuse.blue; + if (colorSum * rt.fade <= 1e-4f) + return true; + if (rt.radius.x <= 1e-4f) + return true; + return false; + }); + const std::size_t after = s_normalConvert.size(); + if (before != after) { + static int loggedShrink = 0; + if (loggedShrink++ < 20 || (before - after) > 32) { + logger::debug("[SCM] s_normalConvert reconcile: {} -> {} ({} dropped)", + before, after, before - after); + } + } + } + + // Drop entries no longer chosen. Rank-drift suppression now lives + // in CalculateLightScore via the lightframessincerender decay term + // in the default ScoreFormula; the slot pool itself is a dumb + // container that follows the chosen set without policy of its own. + // The atomic loop's c.excess / c.invalid branches handle the + // engine-side ConvertLight / DisableLight call for the dropped + // occupants on the same frame. + for (int i = 0; i < s_lights.Size; i++) { + if (!s_lights.Lights[i].Light) + continue; + bool stillChosen = (i == 0 && s_lights.Sun); // sun slot + if (!stillChosen) { + for (auto& c : candidates) { + if (c.light == s_lights.Lights[i].Light && c.chosen) { + stillChosen = true; + break; + } + } + } + if (!stillChosen) + s_lights.Lights[i].Clear(); + } + + // Add newly chosen lights (assigned to first free slot; keeps existing chosen lights in place). + for (auto& c : candidates) { + if (!c.chosen) + continue; + bool alreadyIn = false; + for (int i = 0; i < s_lights.Size && !alreadyIn; i++) + if (s_lights.Lights[i].Light == c.light) + alreadyIn = true; + if (alreadyIn) + continue; + + int idx = s_lights.FindFreeIndex(true, s_settings.ShadowLightCount, s_settings.ConvertedShadowSlots); + if (idx < 0) + continue; + // Eviction nulls Light* but leaves the rest of LightEntry intact + // so it can serve as a cache key. Clear at acquire so the new + // occupant doesn't inherit LastDrawnFrame / lastGeomHash from the + // previous owner (which would skip its first render and let the + // cluster pipeline sample stale kSHADOWMAPS[idx] content). + s_lights.Lights[idx].Clear(); + s_lights.Lights[idx].Light = c.light; + } + + // Update sun slot (slot 0). + if (sunLight) { + if (s_lights.Lights[0].Light != sunLight) { + s_lights.Lights[0].Clear(); + s_lights.Lights[0].Light = sunLight; + } + s_lights.Sun = true; + } else { + // Sun is gone. If slot 0 was tracking the sun, clear the stale + // pointer. If Sun was already false coming in, slot 0 holds a + // regular point light (sun-aware FindFreeIndex allocates point + // lights to slot 0 when Sun=false) -- do NOT wipe it. This + // matches Intellightent's reference behaviour (no unconditional + // slot-0 clear in the no-sun branch). + if (s_lights.Sun) + s_lights.Lights[0].Clear(); + s_lights.Sun = false; + } + } // end SCM::UpdatePoolMembership + + // ---- Temporal budget: decide which lights redraw this frame ---- + double budget = s_settings.RedrawBudgetMs; + { + // Frame-time EMA + budget formula evaluation. Scoped separately + // from ScheduleLoop so the once-per-frame budget cost is visible + // distinct from the per-light scheduling cost. + { + ZoneNamedN(zoneCompBud, "SCM::ComputeBudget", true); + // Update frame-time EMA and ring buffer (always, for formula params and UI). + const float dtMs = *globals::game::deltaTime * 1000.0f; + s_ftRing[s_ftHead] = dtMs; + s_ftHead = (s_ftHead + 1) % kFrameWindow; + if (s_ftCount < kFrameWindow) + ++s_ftCount; + s_ftEMA = (s_ftCount == 1) ? dtMs : 0.1f * dtMs + 0.9f * s_ftEMA; + + const float target_ms = ComputeFrameTimePercentile90(); + if (s_ftEMA < target_ms) + s_stableFrames = std::min(s_stableFrames + 1, 45); + else + s_stableFrames = 0; + + FormulaHelper::SetParam(kFormulaParam_FrameTime, static_cast(s_ftEMA)); + FormulaHelper::SetParam(kFormulaParam_FrameTarget, static_cast(target_ms)); + FormulaHelper::SetParam(kFormulaParam_StableFrames, static_cast(s_stableFrames)); + + // Evaluate the budget for the whole frame. + // Manual: fixed slider value (RedrawBudgetMs). + // Formula: user-editable exprtk expression. + if (s_settings.BudgetMode == BudgetModeEnum::Formula && s_formulaRedrawBudget) { + budget = s_formulaRedrawBudget->Calculate(); + } + s_autoBudgetMs = static_cast(budget); + } // end SCM::ComputeBudget + + s_redrawnLightsThisFrame = 0; + s_totalShadowLightsThisFrame = s_settings.ShadowLightCount; + + ZoneScopedN("SCM::ScheduleLoop"); + int maxRedraw = std::min(s_settings.MaxRedrawPerFrame, s_lights.Size); + int32_t budgetRemain = static_cast(budget * 1000.0); + bool isFirst = true; + int32_t now = *globals::game::frameCounter; + + // Clear RedrawFrame on slots OUTSIDE the point-light range (converted / + // otherwise-allocated). Note PointLightEnd accounts for the sun + // bookkeeping slot when Sun=true, so a converted-slot light at + // pool[ShadowLightCount + 1] correctly gets cleared. + for (int i = s_lights.PointLightEnd(s_settings.ShadowLightCount); i < s_lights.Size; i++) + s_lights.Lights[i].RedrawFrame = false; + + // First pass: sun only. Point-light slots fall through to the + // importance-scored pending loop below so new lights compete + // fairly with existing redraws (sorted by importance, not pool + // order). AllowDrawNewLight is honoured by the pending loop's + // filter. + for (int i = 0; i < s_lights.Size; i++) { + auto& e = s_lights.Lights[i]; + if (!e.Light) { + e.RedrawFrame = false; + continue; + } + e.RedrawFrame = (i == 0 && s_lights.Sun); + if (e.RedrawFrame) { + e.LastDrawnFrame = now; + isFirst = false; + maxRedraw--; + // Sun's budget cost is bookkept at 0 (different texture + // pipeline -- it has its own cascade buffer), so no + // budgetRemain decrement. + } + } + + if (maxRedraw > 0 && budgetRemain > 0) { + std::vector pending; + for (int i = 0; i < s_lights.Size; i++) { + auto& e = s_lights.Lights[i]; + if (!e.Light || e.RedrawFrame) + continue; + // Honour AllowDrawNewLight: when disabled, brand-new + // entries (LastDrawnFrame < 0) wait until the next frame + // rather than competing for this frame's budget. Existing + // lights re-entering view still schedule normally. + if (!s_settings.AllowDrawNewLight && e.LastDrawnFrame < 0) + continue; + pending.push_back(&e); + } + + for (auto* e : pending) { + double interval = 0.0; + if (s_formulaRedrawInterval) { + SetupLightFormula(e->Light, camera, 0); + // e->Index is the pool index. Beyond PointLightEnd are converted slots. + if (e->Index >= s_lights.PointLightEnd(s_settings.ShadowLightCount)) + FormulaHelper::SetParam(kFormulaParam_LightConverted, 1.0); + + // Compute how far the light has moved since its last shadow map render. + // Exposed as `lightdisplacement` so the formula can prioritise fast-moving + // lights (e.g. player torches) without relying on distance-to-camera alone. + if (auto* nilight = e->Light->light.get()) { + auto& curr = nilight->world.translate; + float dx = curr.x - e->lastRenderedPos.x; + float dy = curr.y - e->lastRenderedPos.y; + float dz = curr.z - e->lastRenderedPos.z; + FormulaHelper::SetParam(kFormulaParam_LightDisplacement, + static_cast(sqrtf(dx * dx + dy * dy + dz * dz))); + } + + interval = s_formulaRedrawInterval->Calculate(); + } + interval += 1.0; + + // Contribution-weighted redraw interval: + // importance = luminance(diffuse × fade) × max(att_cam, att_plr) + // att(pos) = max(1 - (dist/radius)^2, 0)^2 (Skyrim falloff) + // interval *= 2.0 * (0.025/2.0)^importance + // importance=0 -> x2.0 (deprioritise), 0.5 -> ~x0.32, 1.0 -> ~x0.05. + // Refs: Wimmer & Scherzer 2006 "Instant Shadow Maps" sec. 3; + // Valient 2014 "Practical Shadow Maps". + + float importance = 0.0f; + + if (auto* ni = e->Light->light.get()) { + auto& rtd = ni->GetLightRuntimeData(); + float lightRadius = rtd.radius.x; + auto lp = ni->world.translate; + + // Perceptual luminance (Rec.709) × engine fade factor. + float lum = 0.2126f * rtd.diffuse.red + + 0.7152f * rtd.diffuse.green + + 0.0722f * rtd.diffuse.blue; + float effectiveLum = lum * rtd.fade; + + // Primary: screen-space projected solid angle. + // "How much of the view does this light's influence + // sphere occupy?" Industry standard for many-light + // shadow prioritisation -- see Olsson & Assarsson 2012, + // "Clustered Deferred and Forward Shading"; Wronski + // 2014, "Sample Distribution Shadow Maps"; CryEngine + // shadow LOD docs. Approximates angular radius from + // camera as radius/viewZ; solid angle ~ angularRadius^2. + // Constants (screenH / 2*tan(fovY/2))^2 drop out -- they're + // the same across all lights and don't affect ranking. + // + // Edge cases: + // viewZ < -radius : light fully behind camera, coverage=0 + // |viewZ| < radius : light intersects camera plane; + // clamp effectiveZ to avoid blow-up. + float coverage = 0.0f; + if (camera) { + auto cp = camera->world.translate; + RE::NiPoint3 fwd = camera->world.rotate.GetVectorY(); + float rx = lp.x - cp.x, ry = lp.y - cp.y, rz = lp.z - cp.z; + float viewZ = fwd.x * rx + fwd.y * ry + fwd.z * rz; + if (viewZ > -lightRadius) { + float effectiveZ = std::max(viewZ, lightRadius * 0.5f); + float angularRadius = lightRadius / effectiveZ; + coverage = angularRadius * angularRadius; + } + } + + // Fallback: Skyrim-style quadratic distance falloff + // from camera/player. Covers two cases where coverage + // alone returns 0 but the user still sees shadows: + // 1. Light just outside the frustum (around a corner) + // illuminating a visible wall. + // 2. Player-held torch behind the camera lighting + // geometry ahead. + // Weighted at 0.3 -- coverage dominates when the light + // is in view, but out-of-view lights still get a floor + // proportional to their illumination at the viewer. + auto computeAtt = [&](const RE::NiPoint3& pos) -> float { + float dx = pos.x - lp.x, dy = pos.y - lp.y, dz = pos.z - lp.z; + float dist2 = dx * dx + dy * dy + dz * dz; + float r2 = lightRadius * lightRadius; + if (dist2 >= r2) + return 0.0f; + float t = dist2 / r2; + float a = 1.0f - t; + return a * a; // matches Skyrim (1-(d/r)^2)^2 falloff + }; + auto* plr = RE::PlayerCharacter::GetSingleton(); + float attCam = camera ? computeAtt(camera->world.translate) : 0.0f; + float attPlr = plr ? computeAtt(plr->GetPosition()) : attCam; + float distanceFallback = std::max(attCam, attPlr) * 0.3f; + + importance = effectiveLum * std::max(coverage, distanceFallback); + } + + // Exponential interval scaling: maxScale*(minScale/maxScale)^clamp(importance,0,1) + float kMaxMult = s_settings.ImportanceMaxScale; + float kMinMult = std::min(s_settings.ImportanceMinScale, kMaxMult); + float clampedImp = std::min(importance, 1.0f); + interval *= static_cast(kMaxMult * powf(kMinMult / kMaxMult, clampedImp)); + + FormulaHelper::SetParam(kFormulaParam_LightImportance, static_cast(importance)); + e->RedrawScore = e->LastDrawnFrame + interval; + e->lastImportance = importance; + + // Cached shadow maps: if the geometry hash matches what we + // rendered last time, the shadow map currently in the slot + // is byte-identical to what a fresh re-render would produce. + // No need to redraw -- push the score sky-high so this entry + // loses every budget contest unless literally nothing else + // needs redrawing (defensive: still allow eventual refresh + // against any hashing bugs). + // + // Industry-standard pattern: UE5 "Cached Shadow Maps", + // Frostbite movable-light caching. The hash captures + // (1) light's own pose + radius and (2) each caster's + // worldBound + identity -- both rigid motion and engine- + // updated bounds (BSDynamicTriShape vertex changes update + // worldBound). + e->pendingGeomHash = ComputeShadowGeomHash(e->Light); + if (e->LastDrawnFrame >= 0 && e->lastGeomHash != 0 && + e->pendingGeomHash == e->lastGeomHash) { + e->RedrawScore += 1e15; + } + } + + // Count lights meaningfully illuminating the viewer area. + s_highImportanceLightCount = static_cast( + std::count_if(pending.begin(), pending.end(), + [](const LightEntry* e) { return e->lastImportance > 0.1f; })); + + std::sort(pending.begin(), pending.end(), + [](const LightEntry* a, const LightEntry* b) { return a->RedrawScore < b->RedrawScore; }); + + for (auto* e : pending) { + if (maxRedraw <= 0) + break; + if (budgetRemain <= 0) + break; + int32_t budgetEstimate = s_budget.GetCost(e->Light); + if (isFirst) { + if (!s_lights.Sun || e->Index > 0) + budgetRemain -= budgetEstimate; + maxRedraw--; + e->RedrawFrame = true; + e->LastDrawnFrame = now; + e->lastGeomHash = e->pendingGeomHash; + isFirst = false; + continue; + } + if (budgetEstimate <= budgetRemain) { + budgetRemain -= budgetEstimate; + maxRedraw--; + e->RedrawFrame = true; + e->LastDrawnFrame = now; + e->lastGeomHash = e->pendingGeomHash; + continue; + } + } + } + } + + // Count how many shadow lights are scheduled to redraw this frame. + // Iterate the point-light range (sun-aware: skips pool[0] when Sun=true). + s_redrawnLightsThisFrame = 0; + for (int j = s_lights.PointLightFirst(); j < s_lights.PointLightEnd(s_settings.ShadowLightCount); j++) { + if (s_lights.Lights[j].RedrawFrame) + ++s_redrawnLightsThisFrame; + } + + // EWMA so the UI counter doesn't flicker frame-to-frame. + s_redrawnLightsSmoothed = 0.8f * s_redrawnLightsSmoothed + 0.2f * s_redrawnLightsThisFrame; + + // Atomic per-candidate loop: process each score-sorted candidate to + // completion before moving on. Branch dispatch: + // chosen + RedrawFrame + slot in budget: EnableLight + render + // chosen otherwise: DisableLight (re-added below + // via GameSetShadowCasterSlot) + // excess + ConvertExcessToNormal: ConvertLight + // excess otherwise / invalid: DisableLight + // + // Ordering matters: chosen (rank < ShadowLightCount) runs before any + // excess. ConvertLight's ReturnShadowmaps can mutate activeShadowLights + // and free other BSShadowLights, but by then chosen entries have + // already completed EnableLight + budget pairing in-iteration -- no + // later phase walks those pointers. + // + // isUsableLight() per-iteration guard catches dangling pointers if an + // earlier EnableLight invalidated a later candidate via scene mutation. + + auto* shadowSceneNodeRT = &ssn->GetRuntimeData(); + + // Two-stage validity check used before any virtual dispatch on a + // BSShadowLight from s_lights[] or candidates[]: + // (1) Is the pointer still in the scene's activeShadowLights? + // (catches "removed since last frame") + // (2) Is the vtable non-zero? + // (catches "freed and zeroed by tbbmalloc / EngineFixes via a path + // that bypassed BSSmartPointer ref-counting" — the pointer is + // still in activeShadowLights but the object is dead) + // Either failure → caller must skip the light. + auto isAliveNow = [shadowSceneNodeRT, sunLight](RE::BSShadowLight* l) -> bool { + if (!l) + return false; + if (l == sunLight) + return true; + for (auto& sp : shadowSceneNodeRT->activeShadowLights) + if (sp.get() == l) + return true; + return false; + }; + auto isVtableValid = [](RE::BSShadowLight* l) -> bool { + return l && *reinterpret_cast(l) != 0; + }; + auto isUsableLight = [&](RE::BSShadowLight* l) -> bool { + return isAliveNow(l) && isVtableValid(l); + }; + + auto findSlotForLight = [](RE::BSShadowLight* l) -> int { + for (int i = 0; i < s_lights.Size; i++) + if (s_lights.Lights[i].Light == l) + return i; + return -1; + }; + + // Single decision point for "this light won't shadow this frame -- + // Convert (keeps diffuse via cluster pipeline) or Disable (light + // vanishes)?". Used by both the c.invalid and c.excess branches. + // + // Spots always Disable: the engine has no NiSpotLight equivalent, so + // ConvertLight on a BSShadowFrustumLight would make the cone-shaped + // illumination spherical and bleed through walls behind the cone. + // Omnis/hemis Convert when ConvertExcessToNormal is on or a debug + // pin-convert is set on this light. The pin override applies even + // when the user disabled ConvertExcessToNormal globally. + // + // allowConvert is a callsite veto -- the c.invalid path passes it + // false for invalidPortal (cluster has no portal-graph awareness, + // converting would leak light across cells) so portal-occluded + // lights always Disable. + // + // Returns true on Convert, false on Disable, so callers can apply + // path-specific follow-ups (e.g. lodDimmer=1 reset on the invalidLod + // path so the converted light still contributes to clusters). + auto convertOrDisable = [&](RE::BSShadowLight* light, bool allowConvert) -> bool { + const bool isSpot = light->GetIsFrustumLight(); + const bool forceConvert = s_pinConvert.count(reinterpret_cast(light)) > 0; + if (allowConvert && (s_settings.ConvertExcessToNormal || forceConvert) && !isSpot) { + ConvertLight(light, ssn, false); + return true; + } + DisableLight(light); + return false; + }; + + // Sun slot (slot 0) is processed inline below — sun setup happened at the + // top of the function; we only need to mark its mask index here. + if (s_lights.Sun && s_lights.Lights[0].Light && s_lights.Lights[0].RedrawFrame) { + ShadowField(s_lights.Lights[0].Light, maskIndex) = 0; + doneLightCount++; + } + + // Per-candidate Begin/EnableLight/End mutation loop. EnableLight may + // trigger synchronous shadow render dispatches in the engine, so this + // zone captures both our scheduler work and any engine-side rendering + // it pulls in for chosen lights. + { + ZoneNamedN(zoneAtomic, "SCM::AtomicMutationLoop", true); + for (auto& c : candidates) { + if (c.invalid) { + // isUsableLight (membership + vtable) is the same gate the + // excess branch uses. Both ConvertLight and DisableLight + // fan into virtually-dispatched callees (ReturnShadowmaps), + // so a freed-but-canonical pointer must be skipped for + // either path. + if (!isUsableLight(c.light)) + continue; + + // All c.invalid cases route through convertOrDisable. Per the + // Ghidra-verified UpdateCamera analysis above, frustrumCull + // is set both by the genuine sphere-vs-frustum cull AND by + // the shadow-distance LOD cull; treating them uniformly lets + // distant lights past shadow distance still reach the + // cluster pipeline. allowConvert=c.invalidCamera so portal- + // occluded omnis fall to Disable (cluster lighting has no + // portal-graph awareness and would leak across cells). + ZoneNamedN(zCvt, "SCM::Engine::convertOrDisable(invalid)", true); + if (convertOrDisable(c.light, /*allowConvert=*/c.invalidCamera)) { + s_schedDiag.converted_invalid++; + // UpdateCamera zeros lodDimmer alongside frustrumCull + // when its shadow-distance LOD cull fires. The + // cluster lighting builder multiplies light.fade by + // lodDimmer and drops the light if the product falls + // below 1e-4. Restore only when fully zeroed -- any + // smooth fade value the engine set is preserved so + // the cluster contribution fades gradually rather + // than snapping to full intensity. Matches the + // per-frame restore in LightLimitFix::UpdateLights + // for already-converted lights. + if (c.light->lodDimmer == 0.0f) + c.light->lodDimmer = 1.0f; + } else { + s_schedDiag.disabled_invalid++; + } + continue; + } + + if (c.chosen) { + int slot = findSlotForLight(c.light); + if (slot < 0) + continue; // matches old behaviour: chosen-but-no-slot is a no-op + if (slot == 0 && s_lights.Sun) + continue; // sun handled above + + auto& e = s_lights.Lights[slot]; + + // Render-this-frame path is reserved for chosen point-light slots + // (excludes converted slots which start at PointLightEnd). Use + // the sun-aware bound so pool[ShadowLightCount] (the highest + // point-light slot when Sun=true) is included. + if (e.RedrawFrame && slot < s_lights.PointLightEnd(s_settings.ShadowLightCount)) { + // Render-this-frame path. A previous iteration's EnableLight + // may have transitively freed this light via game-side scene + // mutations (membership change OR tbbmalloc-zeroed memory), + // so re-validate before any virtual dispatch. + if (!isUsableLight(e.Light)) { + e.Light = nullptr; + continue; + } + + auto* lightSnapshot = e.Light; // value snapshot for budget pairing + + e.Light->UpdateCamera(camera); + s_budget.BeginLight(lightSnapshot, 0); + { + ZoneNamedN(zEnable, "SCM::Engine::EnableLight", true); + EnableLight(e.Light, camera, ssn, slot); + } + + // EnableLight callbacks can null e.Light (re-entrant scheduling + // / scene mutation), AND the engine can free the BSShadowLight + // during the call without nulling our pointer -- a third-party + // VR crash report (CommunityShaders.dll v1.5.1, file path + // D:\a\skyrim-community-shaders\... at EnableLight's + // `*GetAccumLightSlot() += light->shadowMapCount`) showed the + // engine reading shadowMapCount from a freed BSShadowLight, + // corrupting the global accumLightSlot counter, then a + // downstream `[base + corrupted*8]` AV. Bare null check passes + // for the freed-but-non-null case; isUsableLight rejects it + // via the activeShadowLights-membership and vtable checks. + if (!e.Light || !isUsableLight(e.Light)) + continue; + s_budget.EndLight(lightSnapshot, 0); + + if (auto* nilight = e.Light->light.get()) + e.lastRenderedPos = nilight->world.translate; + + ShadowField(e.Light, maskIndex) = static_cast(slot); + doneLightCount++; + } + // Cached-shadow path (chosen + !RedrawFrame, or i >= ShadowLightCount): + // do nothing here. The non-redrawn light keeps its stale shadow map and + // is re-inserted by the GameSetShadowCasterSlot loop below at endIdx. + // Calling DisableLight here would invoke ReturnShadowmaps, releasing the + // cached shadow data for one frame and producing visible flicker that + // worsens as the budget gets more constrained. + continue; + } + + if (c.excess) { + if (!isUsableLight(c.light)) + continue; + + // Atomic ordering: by the time we reach excess (rank + // >= ShadowLightCount), all chosen lights have completed + // their Begin/EnableLight/End sequence. ConvertLight's + // ReturnShadowmaps side effect can only invalidate + // pointers we are no longer walking. LightLimitFix:: + // UpdateLights then iterates activeShadowLights to pick + // up converted lights for the cluster pipeline. + // + // Rank-drift suppression (a torch's importance score + // bobbing across the chosen/excess boundary frame-to- + // frame) lives in the score formula via the + // lightframessincerender decay term, not here. + ZoneNamedN(zCvt, "SCM::Engine::convertOrDisable(excess)", true); + if (convertOrDisable(c.light, /*allowConvert=*/true)) + s_schedDiag.converted_excess++; + else + s_schedDiag.disabled_excess++; + continue; + } + } + } // end SCM::AtomicMutationLoop + + // Non-redrawn chosen lights: insert at end of shadow caster array without rendering. + // GetAccumLightSlot() already advanced past all EnableLight()-rendered slots. + // + // Re-rebuild the alive set: the atomic loop above may have invalidated + // pointers (e.g. ConvertLight on excess removes from activeShadowLights). + // Skip s_lights entries whose pointer is no longer in the scene to avoid + // dereferencing freed BSShadowLight memory below. + { + ZoneNamedN(zonePostAtomic, "SCM::PostAtomicRevalidate", true); + std::unordered_set aliveAfterAtomic; + { + auto& alive = ssn->GetRuntimeData().activeShadowLights; + aliveAfterAtomic.reserve(alive.size() + 1); + if (sunLight) + aliveAfterAtomic.insert(sunLight); + for (auto& sp : alive) + if (auto* l = sp.get()) + aliveAfterAtomic.insert(l); + } + + int endIdx = (int)*GetAccumLightSlot(); + + for (int i = 0; i < s_lights.Size; i++) { + auto& e = s_lights.Lights[i]; + // Re-insert (without rendering) every chosen+!RedrawFrame light + // AND every converted-slot light (i >= PointLightEnd). The + // PointLightEnd bound is sun-aware so converted slots correctly + // start one slot later when Sun=true. + if (e.Light && (!e.RedrawFrame || i >= s_lights.PointLightEnd(s_settings.ShadowLightCount))) { + // Membership check uses the snapshot built above (a + // game-mutation in the atomic loop may have invalidated + // pointers; aliveAfterAtomic captures the current scene + // state in O(N) for O(1) membership queries here). + if (aliveAfterAtomic.find(e.Light) == aliveAfterAtomic.end()) { + s_schedDiag.reconciliation_clears++; + e.Clear(); + continue; + } + // First-render gate: a chosen light whose slot has never + // been rendered for IT (LastDrawnFrame < 0) has no valid + // shadow content in its kSHADOWMAPS slice -- the depth + // content is either cleared or carries the evicted + // previous occupant's shadow. Inserting the light as a + // shadow caster would make the cluster shader sample stale + // depth and project a wrong shadow shape through the new + // light. Skip insertion this frame; the light still + // illuminates via the cluster pipeline as a non-shadow + // light, with no false shadow. Once it wins a redraw turn + // LastDrawnFrame goes >= 0 and it joins the shadow set + // normally. + // + // Converted-slot range (i >= PointLightEnd) is unaffected: + // converted lights don't sample kSHADOWMAPS via this slot + // path; they participate via the s_normalConvert non-shadow + // pipeline. + if (i < s_lights.PointLightEnd(s_settings.ShadowLightCount) && + e.LastDrawnFrame < 0 && + !(s_lights.Sun && i == 0)) { + s_schedDiag.first_render_skips++; + continue; + } + + // Cached-shadow reuse (the UE5 / CryEngine / Frostbite + // pattern). We unconditionally sample the cached + // kSHADOWMAPS slice even when the geometry hash mismatches + // (light or caster moved since the cached render). For + // small motion the staleness is sub-pixel and invisible; + // for large motion the shadow visibly lags the light by + // 1-2 frames, which is much less objectionable than the + // full-frame on/off flicker that hash-gated suppression + // produces on every animated torch. The hash-mismatch + // priority hint above keeps stale entries at the front of + // the redraw queue, so the lag self-corrects within budget + // cycles. + // + // The first_render_skips gate above is the only safety + // gate that DOES suppress insertion: a slot with no + // rendered content for its current owner (LastDrawnFrame + // < 0) has no valid cached shadow to fall back on; the + // GPU slice is either cleared or contains an evicted + // previous occupant. Hash mismatch on an existing slice + // is at worst a small visual lag. + // GameSetShadowCasterSlot calls Accumulate virtually; reuse + // isUsableLight's vtable guard to catch tbbmalloc-zeroed + // objects that are still in activeShadowLights but freed. + if (!isVtableValid(e.Light)) { + e.Light = nullptr; + continue; + } + GameSetShadowCasterSlot(ssn, e.Light, endIdx, 1); + // Same hazard as the post-EnableLight site: the engine can + // free the light during this call. Use isUsableLight, not + // just null check. + if (!e.Light || !isUsableLight(e.Light)) + continue; + endIdx += e.Light->shadowMapCount; + ShadowField(e.Light, maskIndex) = static_cast(i); + + // GameSetShadowCasterSlot (via Accumulate) overwrites shadowmapIndex + // with the sequential endIdx counter, diverging from the stable + // container-slot index that CopyShadowLightData and Prepass expect. + // All shadow-slot light types are affected: + // Spot (!IsParabolicLight): 1 descriptor, 1 atlas slice. + // Hemi (IsParabolicLight && !IsOmniLight): 1 descriptor, 1 atlas slice. + // Omni (IsParabolicLight && IsOmniLight): both paraboloids packed into + // a single atlas slice via UV splitting in GetOmnidirectionalShadow, + // so all descriptors should also point to i. + // Restore shadowmapIndex = i for every non-redrawn shadow-slot light. + // Only restore shadowmapIndex for point-light slots (skip converted). + // PointLightEnd accounts for sun bookkeeping so the highest point-light + // slot (Sun=true: pool[ShadowLightCount]) is included. + if (s_settings.ShadowLightCount > 4 && i < s_lights.PointLightEnd(s_settings.ShadowLightCount)) { + // Restore descriptor.shadowmapIndex for cached (non-redrawn) + // chosen lights so RenderCascade samples their preserved + // depth slice. Sun (pool[0] when Sun=true) is skipped — + // it renders via the directional cascade path, not + // kSHADOWMAPS, so its descriptor.shadowmapIndex is unused. + if (s_lights.Sun && i == 0) + continue; + if (REL::Module::IsVR()) { + for (auto& desc : e.Light->GetVRRuntimeData().shadowmapDescriptors) + desc.shadowmapIndex = static_cast(i); + } else { + for (auto& desc : e.Light->GetRuntimeData().shadowmapDescriptors) + desc.shadowmapIndex = static_cast(i); + } + } + } + } + } + // Update rolling redraw and budget statistics. + { + int redrawing = 0; + int32_t consumed = 0; + for (int i = 0; i < s_lights.Size; i++) { + auto& e = s_lights.Lights[i]; + if (e.Light && e.RedrawFrame) { + if (i != 0 || !s_lights.Sun) + consumed += s_budget.GetCost(e.Light); + redrawing++; + } + } + s_redrawSum -= s_redrawHistory[s_redrawHistoryPos]; + s_redrawHistory[s_redrawHistoryPos] = redrawing; + s_redrawSum += redrawing; + s_redrawHistoryPos = (s_redrawHistoryPos + 1) % kRedrawHistorySize; + + s_budgetSum -= s_budgetHistory[s_budgetHistoryPos]; + s_budgetHistory[s_budgetHistoryPos] = consumed; + s_budgetSum += consumed; + s_budgetHistoryPos = (s_budgetHistoryPos + 1) % kRedrawHistorySize; + } + + ssn->GetRuntimeData().firstPersonShadowMask = *GetShadowMask(); + *GetFrameLightCount() = static_cast(doneLightCount); + + // ===================================================================== + // Tracy per-frame plots: scheduler diagnostic counters + live config. + // Emitting both in the same frame lets a capture be queried for A/B + // behaviour without re-running the game: the cfg_* plots are the + // independent variables, the scm.* plots are the dependent outcomes. + // ===================================================================== + { + // Sample slot occupancy at frame end (post-reconciliation). + for (int i = 0; i < s_lights.Size; i++) + if (s_lights.Lights[i].Light) + s_schedDiag.slots_in_use++; + + TracyPlot("scm.candidates.total", (int64_t)s_schedDiag.candidates_total); + TracyPlot("scm.candidates.chosen", (int64_t)s_schedDiag.candidates_chosen); + TracyPlot("scm.candidates.excess", (int64_t)s_schedDiag.candidates_excess); + TracyPlot("scm.candidates.invalid_camera", (int64_t)s_schedDiag.candidates_invalid_camera); + TracyPlot("scm.candidates.invalid_portal", (int64_t)s_schedDiag.candidates_invalid_portal); + TracyPlot("scm.candidates.invalid_frustum", (int64_t)s_schedDiag.candidates_invalid_frustum); + TracyPlot("scm.candidates.invalid_lod", (int64_t)s_schedDiag.candidates_invalid_lod); + TracyPlot("scm.candidates.invalid_other", (int64_t)s_schedDiag.candidates_invalid_other); + TracyPlot("scm.converted.invalid", (int64_t)s_schedDiag.converted_invalid); + TracyPlot("scm.converted.excess", (int64_t)s_schedDiag.converted_excess); + TracyPlot("scm.disabled.invalid", (int64_t)s_schedDiag.disabled_invalid); + TracyPlot("scm.disabled.excess", (int64_t)s_schedDiag.disabled_excess); + TracyPlot("scm.reconciliation.clears", (int64_t)s_schedDiag.reconciliation_clears); + TracyPlot("scm.slots.in_use", (int64_t)s_schedDiag.slots_in_use); + TracyPlot("scm.first_render_skips", (int64_t)s_schedDiag.first_render_skips); + + // Live config plots — record the *current* settings on each frame so + // a single capture spanning a settings change captures both sides. + TracyPlot("cfg.ShadowLightCount", (int64_t)s_settings.ShadowLightCount); + TracyPlot("cfg.MaxRedrawPerFrame", (int64_t)s_settings.MaxRedrawPerFrame); + TracyPlot("cfg.ConvertExcessToNormal", (int64_t)(s_settings.ConvertExcessToNormal ? 1 : 0)); + TracyPlot("cfg.Enabled", (int64_t)(s_settings.Enabled ? 1 : 0)); + TracyPlot("cfg.RedrawBudgetMs", (double)s_settings.RedrawBudgetMs); + } + } + + // ========================================================================= + // Render hook: replaces RenderActiveShadowCasterLights + // Iterates s_lights and calls Render() on lights flagged RedrawFrame. + // Uses install_context_hook at a specific call site in the render loop (see Install()). + // ========================================================================= + + static void RenderScheduledShadowLights() + { + // VR: RenderActiveShadowCasterLights normally saves+clears g_drawStereo before + // iterating shadow casters, then restores it. Without this, each hemisphere + // render is doubled for both eyes -> 4-quadrant shadow map texture. + bool savedStereo = false; + if (REL::Module::IsVR()) { + savedStereo = *globals::game::drawStereo; + *globals::game::drawStereo = false; + } + + ZoneScopedN("SCM::RenderScheduledShadowLights"); + auto* state = globals::state; + state->BeginPerfEvent("SCM::RenderScheduledShadowLights"); +#ifdef TRACY_ENABLE + TracyD3D11Zone(state->tracyCtx, "SCM::RenderScheduledShadowLights"); +#endif + + s_budget.Begin(1); + + uint32_t tmp = 0; + // Sun first: BSShadowDirectionalLight::Render emits the "Directional + // Light Shadowmaps" marker and writes the cascade depth maps to + // kSHADOWMAPS_ESRAM. The engine's vanilla RenderActiveShadowCasterLights + // dispatches this via the same vtable walk it uses for point lights; + // we replaced that walk with this loop, so we need to call sun.Render + // explicitly. Without this, the directional cascade pass is skipped + // and exterior scenes render with no sun shadow. + if (s_lights.Sun && s_lights.Lights[0].Light) { + ZoneNamedN(zSun, "SCM::Render::Sun", true); +#ifdef TRACY_ENABLE + TracyD3D11Zone(state->tracyCtx, "SCM::Render::Sun"); +#endif + s_budget.BeginLight(s_lights.Lights[0].Light, 1); + s_lights.Lights[0].Light->Render(tmp); + s_budget.EndLight(s_lights.Lights[0].Light, 1); + } + + // Point lights from PointLightFirst onwards. PointLightFirst skips + // slot 0 (handled above when Sun=true). PointLightEnd includes the + // highest point-light slot when Sun=true. + { + ZoneNamedN(zPoint, "SCM::Render::PointLights", true); +#ifdef TRACY_ENABLE + TracyD3D11Zone(state->tracyCtx, "SCM::Render::PointLights"); +#endif + for (int i = s_lights.PointLightFirst(); i < s_lights.PointLightEnd(s_settings.ShadowLightCount); i++) { + auto& e = s_lights.Lights[i]; + if (!e.Light || !e.RedrawFrame) + continue; + s_budget.BeginLight(e.Light, 1); + e.Light->Render(tmp); + s_budget.EndLight(e.Light, 1); + } + } + + state->EndPerfEvent(); + + if (REL::Module::IsVR()) + *globals::game::drawStereo = savedStereo; + } + + // Replaces the call to RenderActiveShadowCasterLights. + // install_context_hook (RtlRestoreContext) is required so all volatile registers (r8, etc.) + // are restored before the game continues past the patched call site. + // + // Non-VR (SE/AE): set ctx.Rax = 0 so the conditional between 107133+0x192 and + // +0x1AE skips "call [r8+0x50]" -- r8 is loaded from rax there; if rax != 0, + // r8 gets a stale pointer whose [+0x50] slot is null -> crash at execute 0x0. + static void Hook_RenderShadowLights(CONTEXT& ctx) + { + if (!REL::Module::IsVR()) + ctx.Rax = 0; + RenderScheduledShadowLights(); + }; + + // Hook struct for stl::detour_thunk. + // + // `s_settings.Enabled` is now a BOOT-TIME flag only -- toggling at + // runtime has no effect on this thunk, the same way ShadowLightCount + // and atlas texture sizes are restart-gated. See Init() at the + // settings.Enabled early-return for the boot-time gate. + // + // Rationale (Ghidra-verified by crash 2026-05-17 20:31:12): the AV + // at BSBatchRenderer::sub_SE100843_AE107633 +0x54 + // (`mov rax, [r14+0x48]`, r14=1 = vfunc bool returned as pointer) + // is reached via: + // NiCamera::CalculateAndDrawShadowCasterLights + // -> CalculateActiveShadowCasterLights (the engine's vanilla + // scheduler -- what we'd + // route to on disable) + // -> BSShadowDirectionalLight::sub_SE100818_AE107602 (sun + // shadow) + // -> FUN_1414bf320 (BSCullingProcess inner) + // -> BSCullingProcess::sub + // -> FUN_1414f50d0 + // -> BSBatchRenderer::sub_SE100843_AE107633 (AV) + // + // The crash is in the vanilla scheduler itself. SCM's boot-time + // modifications (kSHADOWMAPS texture sized to ShadowLightCount, + // depth-buffer creation loop redirected via Hook_CreateNormalDepthBuffer + // and Hook_CreateReadOnlyDepthBuffer, screen-space mask pass wrapped + // by Hook_RenderShadowLightsWithUtilityShader) make the engine state + // incompatible with + // the vanilla traversal even when our runtime tracking is left + // untouched (soft-disable still crashed). The deep engine hooking + // is not safely reversible at runtime; restart is the only safe + // way to revert to vanilla. + struct Hook_CalculateActiveShadowCasters + { + static void thunk() + { + ScheduleShadowCasters(); + } + static inline REL::Relocation func; + }; + + // ========================================================================= + // Surface lights hook + // Replaces CalculateActiveNonShadowCasterLights (ID 100997/107784). + // Uses install_context_hook because the function has 10 args (11 in VR) + // with VR-specific stack layout -- CONTEXT is the simplest cross-runtime approach. + // ========================================================================= + + static void Hook_CalculateActiveLightsForSurface(CONTEXT& ctx) + { + // Args from registers/stack (x64 fastcall, shadow space at RSP+0x00..0x20): + auto* lightData = reinterpret_cast(ctx.Rcx); // a1 + auto** lights = reinterpret_cast(ctx.Rdx); // a2 + int maxCount = static_cast(ctx.R8); // a3 + int* shadowCount = reinterpret_cast(ctx.R9); // a4 + auto* ssn = *reinterpret_cast(ctx.Rsp + 0x28); // a5 + auto* shaderProp = *reinterpret_cast(ctx.Rsp + 0x30); // a6 + bool addShadow = *reinterpret_cast(ctx.Rsp + 0x38); // a7 + bool* useShadowSun = *reinterpret_cast(ctx.Rsp + 0x40); // a8 + bool firstPerson = *reinterpret_cast(ctx.Rsp + 0x48); // a9 + uint32_t fpMask = *reinterpret_cast(ctx.Rsp + 0x50); // a10 + + // VR passes an 11th arg: if non-zero, skip accumulation (vanilla early-out). + if (REL::Module::IsVR() && *reinterpret_cast(ctx.Rsp + 0x58) != 0) { + ctx.Rax = 1; // addedLightCount = sun only + return; + } + + // Determine the sun light for this surface. + RE::BSLight* sunLight; + if (*useShadowSun) + sunLight = ssn->GetRuntimeData().sunShadowDirLight; + else + sunLight = ssn->GetRuntimeData().sunLight; + if (shaderProp->flags.any(RE::BSShaderProperty::EShaderPropertyFlag::kCloudLOD)) + sunLight = ssn->GetRuntimeData().cloudLight; + + lights[0] = sunLight; + *shadowCount = 0; + int added = 1; + + if (addShadow) { + auto& casters = ssn->GetRuntimeData().shadowLightsAccum; + + // Step 1: vanilla shadow lights gated by activeLightMask / first-person mask. + for (uint32_t slot = 0; slot < casters.size() && added < maxCount; slot++) { + uint32_t bit = 1u << slot; + if (!((firstPerson && (fpMask & bit)) || (lightData->activeLightMask & bit))) + continue; + auto* sl = reinterpret_cast(casters[slot]); + if (!sl || sl == sunLight) + continue; + if (GameIsLightAffectingSurface(shaderProp, sl)) { + lights[added++] = sl; + (*shadowCount)++; + } + } + + // Step 2: extended pool lights not covered by the vanilla mask. + // Only inject lights that are present in this scene's caster array + // (prevents world lights leaking into menu / special scenes). + // Iterate the point-light range (sun-aware via PointLightFirst / + // PointLightEnd; pre-helper loops missed pool[ShadowLightCount] + // when Sun=true, dropping one shadow caster from per-surface lists). + for (int i = s_lights.PointLightFirst(); i < s_lights.PointLightEnd(s_settings.ShadowLightCount) && added < maxCount; i++) { + auto& e = s_lights.Lights[i]; + if (!e.Light || reinterpret_cast(e.Light) == sunLight) + continue; + + bool inScene = false; + for (uint32_t s = 0; s < casters.size() && !inScene; s++) + if (reinterpret_cast(casters[s]) == reinterpret_cast(e.Light)) + inScene = true; + if (!inScene) + continue; + + bool alreadyAdded = false; + for (int j = 1; j < added && !alreadyAdded; j++) + if (lights[j] == reinterpret_cast(e.Light)) + alreadyAdded = true; + if (alreadyAdded) + continue; + + if (GameIsLightAffectingSurface(shaderProp, reinterpret_cast(e.Light))) { + lights[added++] = reinterpret_cast(e.Light); + (*shadowCount)++; + } + } + } + + // Step 3: non-shadow lights from the per-surface accumulation list. + // Skip parabolic shadow-casters (frustrumCull == 0xFF) and hidden NiLights. + for (uint32_t i = 0; i < lightData->lights.size() && added < maxCount; i++) { + auto* l = lightData->lights[i]; + if (!l || l == sunLight) + continue; + auto* ni = l->light.get(); + if (ni && (l->frustrumCull == 0xFFu || ni->GetFlags().any(RE::NiAVObject::Flag::kHidden))) + continue; + lights[added++] = l; + } + + // Step 4: Inject converted shadow lights (s_normalConvert, issue #2121 #3) + // into the per-surface lights array. These lights have frustrumCull == 0xFF + // (parabolic shadow-caster marker) and are skipped by Step 3, while Steps + // 1/2 don't include them either (ReturnShadowmaps cleared shadowLightsAccum). + // + // The cluster pipeline picks them up separately via LightLimitFix::UpdateLights' + // activeShadowLights iteration; this Step 4 ensures the engine's vanilla + // strict-light loop (which consumes lights[] passed to this function) also + // sees them so non-LLF code paths and shaders without LIGHT_LIMIT_FIX still + // receive the diffuse contribution. + for (auto& c : s_normalConvert) { + if (added >= maxCount) + break; + auto* l = reinterpret_cast(c.light); + if (!l || l == sunLight) + continue; + auto* ni = l->light.get(); + if (!ni || ni->GetFlags().any(RE::NiAVObject::Flag::kHidden)) + continue; + + // Skip if already added in any prior step. + bool alreadyAdded = false; + for (int j = 1; j < added && !alreadyAdded; j++) + if (lights[j] == l) + alreadyAdded = true; + if (alreadyAdded) + continue; + + if (GameIsLightAffectingSurface(shaderProp, l)) + lights[added++] = l; + // Note: do NOT increment *shadowCount; this is a non-shadow contribution. + } + + ctx.Rax = static_cast(added); + } + + // ========================================================================= + // Light conversion hooks + // + // BSShadowLight::IsShadowLight (VFT slot 3): returns false for lights in + // s_normalConvert so the engine treats them as normal (non-shadow) lights + // during the geometry-shader/stencil shadow-masking pass. + // + // RemoveLight / AddLight / SetLight hooks maintain s_normalConvert and + // s_shadowConvert so the lists stay consistent with scene changes. + // ========================================================================= + + static bool Hook_IsShadowLight(RE::BSShadowLight* light) + { + for (auto& c : s_normalConvert) + if (c.light == light) + return false; + return true; + } + + // Fires at start of ShadowSceneNode::RemoveLight (ID 99697/106331). + static void Hook_ConvertLights_Remove(CONTEXT& ctx) + { + auto* ssn = reinterpret_cast(ctx.Rcx); + auto* light = reinterpret_cast(ctx.Rdx); + if (ssn != GetShadowSceneNode()) + return; + for (auto it = s_normalConvert.begin(); it != s_normalConvert.end(); ++it) { + auto* nl = it->light->light.get(); + if (nl && nl == light) { + GameClearGeometryList(it->light); + s_normalConvert.erase(it); + break; + } + } + if (light) + s_shadowConvert.erase(light); + } + + // Fires at start of ShadowSceneNode::AddLight (ID 99692/106326). + // Optionally promotes normal light to shadow light; always forces portal-strict. + static void Hook_ConvertLights_Add(CONTEXT& ctx) + { + auto* ssn = reinterpret_cast(ctx.Rcx); + auto* light = reinterpret_cast(ctx.Rdx); + auto* p = reinterpret_cast(ctx.R8); + if (ssn != GetShadowSceneNode() || !light || !p) + return; + + if (s_settings.PromoteNormalToShadow && !p->shadowLight) { + p->shadowLight = true; + p->fov = 6.2831855f; + p->dynamic = true; + p->restrictedNode = nullptr; + p->falloff = 1.0f; + p->depthBias = 1.0f; + p->nearDistance = (light->GetLightRuntimeData().radius.x / 512.0f) * 219.6356f; + s_shadowConvert.insert(light); + } + // Portal-strict policy by shadow type. The engine picks the concrete + // shadow class (BSShadowParabolicLight / BSShadowHemisphereLight / + // BSShadowFrustumLight) based on the FOV in LIGHT_CREATE_PARAMS: + // fov >= ~2pi -> dual-paraboloid omni + // fov >= ~pi -> hemisphere + // fov < ~pi -> perspective spot/frustum + // Tightening portal-strict on omnis/hemis usefully exercises the + // portal-graph visibility test; doing it on spots drops culled-but- + // visible spots entirely (the cone test rejects spots whose origin + // sits behind a portal even when the cone sweeps into a visible + // room). Honour the per-type toggle so users can A/B easily. + constexpr float kFovHemiThreshold = 3.0f; // ~pi + constexpr float kFovOmniThreshold = 6.0f; // ~2pi + bool enforce = false; + if (p->fov >= kFovOmniThreshold) + enforce = s_settings.ForceEnablePortalStrictOmni; + else if (p->fov >= kFovHemiThreshold) + enforce = s_settings.ForceEnablePortalStrictHemi; + else + enforce = s_settings.ForceEnablePortalStrictSpot; + if (enforce) + p->portalStrict = true; + } + + // Fires at start of BSLight::SetLight (ID 101302/108289). + // Tracks NiLight pointer reassignments in s_shadowConvert. + static void Hook_ConvertLights_SetLight(CONTEXT& ctx) + { + auto* bslight = reinterpret_cast(ctx.Rcx); + auto* nilight = reinterpret_cast(ctx.Rdx); + if (!bslight) + return; + auto* oldlight = bslight->light.get(); + if (oldlight && oldlight != nilight) { + bool did = s_shadowConvert.erase(oldlight) != 0; + if (nilight && did) + s_shadowConvert.insert(nilight); + } + } + + // ========================================================================= + // Stealth detection fix + // + // GetLightLevel (AIProcess::CalculateLightValue, ID 38900/39946) uses the + // engine shadow-light iteration internally. When we replace shadow caster + // selection, the vanilla per-light affect-player loop no longer sees our + // chosen lights correctly. We replace it with our own pass that iterates + // activeShadowLights and calls IsLightAffectingActor() directly. + // ========================================================================= + + // Temporary set of lights that affect the player -- populated each frame + // in Hook_UpdateLightLevelPlayer, consumed in Hook_CheckLightLevelPlayer. + static std::set s_stealthDetectionTmp; + + static void* GetUnkDetectionGlobal() + { + // SE: 142F6DB98 -- a ~80-byte detection struct; GetSingleton equivalent + static REL::RelocationID uid(518074, 404596); + return *reinterpret_cast(uid.address()); + } + + static bool IsLightAffectingActor(RE::BSShadowLight* light, RE::Actor* actor, RE::NiPoint3* pos) + { + // SE: 14071A380 (ID 41661) + using F = bool (*)(void*, RE::BSShadowLight*, RE::Actor*, RE::NiPoint3*); + static REL::Relocation func{ REL::RelocationID(41661, 42744) }; + return func(GetUnkDetectionGlobal(), light, actor, pos); + } + + // Replaces the vanilla shadow-light-affect-player loop. + // RBP-33 holds the player's position (NiPoint3*). + static void Hook_UpdateLightLevelPlayer(CONTEXT& ctx) + { + auto* pos = reinterpret_cast(ctx.Rbp - 33); + auto* player = RE::PlayerCharacter::GetSingleton(); + + s_stealthDetectionTmp.clear(); + auto* ssn = GetShadowSceneNode(); + if (!ssn) + return; + + for (auto& sp : ssn->GetRuntimeData().activeShadowLights) { + auto* l = sp.get(); + if (!l) + continue; + auto* ni = l->light.get(); + if (!ni || ni->GetFlags().any(RE::NiAVObject::Flag::kHidden)) + continue; + if (IsLightAffectingActor(l, player, pos)) + s_stealthDetectionTmp.insert(reinterpret_cast(l)); + } + } + + // Per-light check inside the vanilla affect-player path. + // If the light is not in our set, skip the branch (ctx.Rip += 0x16). + // Note: Execute() sets ctx.Rip = resumeAddr BEFORE calling this, so + // ctx.Rip += 0x16 skips 0x16 bytes past the hook site -- correct. + static void Hook_CheckLightLevelPlayer(CONTEXT& ctx) + { + auto* light = reinterpret_cast(ctx.Rcx); + if (s_stealthDetectionTmp.find(reinterpret_cast(light)) == s_stealthDetectionTmp.end()) + ctx.Rip += 0x16; + } + + // ========================================================================= + // Public API + // ========================================================================= + + void Init(const Settings& settings) + { + s_settings = settings; + + // Check for external shadow management plugins that conflict with our hooks. + if (GetModuleHandleW(L"intellightent-ng.dll")) { + s_externalConflict = true; + s_conflictMessage = + "Disabled: intellightent-ng.dll detected. Both mods manage shadow caster " + "selection and cannot run simultaneously. Remove one to use the other."; + logger::warn("[SCM] {}", s_conflictMessage); + return; + } + + int total = LightContainerSize(settings); + s_lights.Size = total; + s_lights.Sun = false; + s_lights.Lights = new LightEntry[total](); + for (int i = 0; i < total; i++) + s_lights.Lights[i].Index = i; + + // Seed auto-budget ring buffer to 60 fps so the first few frames have sane values. + std::fill(std::begin(s_ftRing), std::end(s_ftRing), 16.67f); + s_ftEMA = 16.67f; + + // Parse formula strings + if (!settings.ScoreFormula.empty()) { + s_formulaScore = std::make_unique(); + if (!s_formulaScore->Parse(settings.ScoreFormula)) + logger::error("[SCM] Failed to parse ScoreFormula"); + } + if (!settings.RedrawIntervalFormula.empty()) { + s_formulaRedrawInterval = std::make_unique(); + if (!s_formulaRedrawInterval->Parse(settings.RedrawIntervalFormula)) + logger::error("[SCM] Failed to parse RedrawIntervalFormula"); + } + if (!settings.RedrawBudgetFormula.empty()) { + s_formulaRedrawBudget = std::make_unique(); + if (!s_formulaRedrawBudget->Parse(settings.RedrawBudgetFormula)) + logger::error("[SCM] Failed to parse RedrawBudgetFormula"); + } + } + + // Set by the resolution combo when the user picks a new tier. Gates the + // SaveINISettings write so we only touch SkyrimPrefs.ini when there's an + // actual change to persist -- without this, every Save Settings click + // would rewrite the user's prefs file even if shadow res wasn't edited. + static bool s_shadowResolutionDirty = false; + + void LoadINISettings() + { + // No-op: the engine already loaded SkyrimPrefs.ini at startup, so the + // live RE::Setting reflects the user's saved value. Future overrides + // that need to land before SCM::Install hook here. + } + + void SaveINISettings() + { + if (!s_shadowResolutionDirty) + return; + auto* prefColl = RE::INIPrefSettingCollection::GetSingleton(); + if (!prefColl) + return; + auto* setting = prefColl->GetSetting("iShadowMapResolution:Display"); + if (!setting) + return; + + // The engine's INIPrefSettingCollection::WriteSetting requires + // OpenHandle to have been called first (it writes via the cached + // `handle` member, which is null between RefreshINI calls). Calling + // it directly returns true but silently no-ops -- verified by the + // fact that the live RE::Setting updates but SkyrimPrefs.ini's + // timestamp doesn't change after Save Settings. + // + // Sidestep the engine path entirely with WritePrivateProfileStringA. + // CommonLib stores the full path of SkyrimPrefs.ini in subKey at + // startup (see InitializeSkyrimINIPrefSettingCollection caller at + // SE 1406489e6 / AE 140648990 / VR equivalent -- it concatenates the + // Documents path with "SkyrimPrefs.ini"). The setting name encodes + // ":
    " -- "iShadowMapResolution:Display" means + // [Display]\niShadowMapResolution=N. + const char* fullName = setting->GetName(); + const char* colon = std::strchr(fullName, ':'); + if (!colon) { + logger::warn("[SCM] Setting name '{}' has no section -- cannot write to INI", fullName); + s_shadowResolutionDirty = false; + return; + } + const std::string key(fullName, colon - fullName); + const std::string section(colon + 1); + const std::string value = std::to_string(setting->GetInteger()); + + // subKey holds the full path to SkyrimPrefs.ini. + const char* iniPath = prefColl->subKey; + if (!iniPath || !iniPath[0]) { + logger::warn("[SCM] INIPrefSettingCollection subKey is empty -- cannot write to INI"); + s_shadowResolutionDirty = false; + return; + } + + if (::WritePrivateProfileStringA(section.c_str(), key.c_str(), value.c_str(), iniPath)) { + // Windows caches INI writes in-process; the file on disk doesn't + // update until the cache is flushed. Calling WritePrivateProfile + // with three NULL parameters forces the flush. Without this the + // write succeeds (returns non-zero, no error) but the file's + // timestamp and contents stay stale until the process exits. + // See KB Q104112 / MSDN remarks for WritePrivateProfileString. + ::WritePrivateProfileStringA(nullptr, nullptr, nullptr, iniPath); + logger::info("[SCM] Persisted [{}]{}={} to {}", section, key, value, iniPath); + } else { + const DWORD err = ::GetLastError(); + logger::warn("[SCM] WritePrivateProfileStringA failed (err={}) writing [{}]{}={} to {}", + err, section, key, value, iniPath); + } + s_shadowResolutionDirty = false; + } + + // Boot-time value of settings.Enabled, captured once in Install() and + // never modified afterwards. The ImGui "Restart required" label + // compares the user's current setting against this rather than the + // (mutable) s_settings.Enabled, so the label persists across Save + // Settings clicks until the user actually restarts. Without this the + // label vanished the moment they saved -- s_settings would catch up + // to the new value and the !=-against-staged condition cleared. + static bool s_bootEnabled = false; + static bool s_bootEnabledCaptured = false; + + void Install(const Settings& settings) + { + s_settings = settings; + s_installedShadowLightCount = settings.ShadowLightCount; + // kSHADOWMAPS is point/spot only -- the sun renders to a separate + // kSHADOWMAPS_ESRAM texture (cascade descriptors live there, not + // here). So the engine allocates exactly ShadowLightCount slices + // in kSHADOWMAPS; no +1 for the sun. + s_requestedSlotCount = static_cast(settings.ShadowLightCount); + + // One-shot capture of the boot Enabled value. Install() is called + // once at startup, but guard anyway in case it's ever re-invoked. + if (!s_bootEnabledCaptured) { + s_bootEnabled = settings.Enabled; + s_bootEnabledCaptured = true; + } + + if (s_externalConflict) + return; + + if (!settings.Enabled) { + logger::info("[SCM] Shadow caster manager disabled -- skipping hook installation."); + return; + } + + bool extended = settings.ShadowLightCount > 4; + bool needExtraBuffers = settings.ShadowLightCount > 8; + + // ---- Extended depth buffer infrastructure ------------------------- + + if (needExtraBuffers) { + globals::features::llf::normalDepthBuffer = new void*[settings.ShadowLightCount + 1](); + globals::features::llf::readOnlyDepthBuffer = new void*[settings.ShadowLightCount + 1](); + + // Patch the creation-loop count from 8 to ShadowLightCount. + // SE/VR: pattern "C7 44 24 68 08 00 00 00" (+4 = the imm32 0x00000008) + // AE: same pattern at different offset + // + // The instruction encodes a 32-bit immediate; we overwrite all four + // bytes so values >255 don't silently truncate (a single-byte write + // to the low byte would leave higher bytes stale, capping us at 255 + // while making the cap silent). + { + static REL::RelocationID uid(100458, 107175); + uintptr_t addr = uid.address() + REL::Relocate(0xD326 - 0xC940, 0xBF6 - 0x210, 0xc91); + int immOff = REL::Relocate(4, 4, 3); + uint32_t newCount = static_cast(settings.ShadowLightCount); + REL::safe_write(addr + immOff, &newCount, sizeof(newCount)); + } + + // Redirect depth-buffer pointer storage in the creation loop. + { + // Normal DSV creation: SE 140D6AB52 / VR 140DBCA00 + static REL::RelocationID uid(75469, 77255); + uintptr_t base = uid.address(); + uintptr_t off = REL::Relocate(0xB52 - 0x9E0, 0x2EB - 0x180, 0x1a0); + int sz = REL::Relocate(7, 7, 8); + if (!SKSE::stl::install_context_hook(base + off, sz, Hook_CreateNormalDepthBuffer, sz)) + logger::error("[SCM] Failed to install Hook_CreateNormalDepthBuffer"); + } + { + // ReadOnly DSV creation: SE 140D6AB71 / VR 140DBCA24 + static REL::RelocationID uid(75469, 77255); + uintptr_t base = uid.address(); + uintptr_t off = REL::Relocate(0xB71 - 0x9E0, 0x2FC - 0x180, 0x1c4); + int sz = REL::Relocate(8, 7, 7); + if (!SKSE::stl::install_context_hook(base + off, sz, Hook_CreateReadOnlyDepthBuffer, sz)) + logger::error("[SCM] Failed to install Hook_CreateReadOnlyDepthBuffer"); + } + + // Sync the first 8 slots into the game's own DepthStencilData array. + { + // SE 140D6AC00 / VR 140DBCAB0 + static REL::RelocationID uid(75469, 77255); + uintptr_t base = uid.address(); + uintptr_t off = REL::Relocate(0xC00 - 0x9E0, 0x384 - 0x180, 0x250); + if (!SKSE::stl::install_context_hook(base + off, 8, Hook_SetupGameArray, 8)) + logger::error("[SCM] Failed to install Hook_SetupGameArray"); + } + + // Depth-buffer selection at draw time. + { + // SE 140D70444 + static REL::RelocationID uid(75580, 77386); + uintptr_t base = uid.address(); + uintptr_t off = REL::Relocate(0x444 - 0x2F0, 0x704 - 0x5B0, 0x1c3); + if (!SKSE::stl::install_context_hook(base + off, 21, Hook_SelectDepthBuffer1)) + logger::error("[SCM] Failed to install Hook_SelectDepthBuffer1"); + } + { + // SE 140D6A1A5 / VR 140DBBFFC + static REL::RelocationID uid(75462, 77247); + uintptr_t base = uid.address(); + uintptr_t off = REL::Relocate(0x1A5 - 0x070, 0x985 - 0x850, 0x19c); + int sz = REL::Relocate(10, 10, 0x2e); + if (!SKSE::stl::install_context_hook(base + off, sz, Hook_SelectDepthBuffer2)) + logger::error("[SCM] Failed to install Hook_SelectDepthBuffer2"); + } + + // Release extended buffers at renderer shutdown. + // SE: ZeroDepthStencilData; AE/VR: Renderer::Shutdown and related dtor paths. + if (REL::Module::GetRuntime() != REL::Module::Runtime::AE) { + // SE + VR share the same pattern. + static REL::RelocationID uid(75628, 0 /*AE unused*/); + uintptr_t addr = uid.address() + (0xE27 - 0xDD0); + if (!SKSE::stl::install_context_hook(addr, 9, Hook_DeleteDepthBuffers_SE, -9)) + logger::error("[SCM] Failed to install Hook_DeleteDepthBuffers_SE"); + } else { + // AE has three separate shutdown paths. + static REL::RelocationID uid1(0, 77228); + if (!SKSE::stl::install_context_hook(uid1.address() + (0x3195 - 0x2E10), 7, Hook_DeleteDepthBuffers_AE, 7)) + logger::error("[SCM] Failed to install Hook_DeleteDepthBuffers_AE (path 1)"); + + static REL::RelocationID uid2(0, 77237); + if (!SKSE::stl::install_context_hook(uid2.address() + (0x3B8C - 0x34A0), 7, Hook_DeleteDepthBuffers_AE, 7)) + logger::error("[SCM] Failed to install Hook_DeleteDepthBuffers_AE (path 2)"); + + static REL::RelocationID uid3(0, 77238); + if (!SKSE::stl::install_context_hook(uid3.address() + (0x3E79 - 0x3BC0), 6, Hook_DeleteDepthBuffers_AE, -6)) + logger::error("[SCM] Failed to install Hook_DeleteDepthBuffers_AE (path 3)"); + } + } + + // Expanded accumulated-lights array (needed when ShadowLightCount > 4). + if (extended) { + // SE: BSShadowFrustumLight accumulation setup + static REL::RelocationID uid(99686, 106320); + uintptr_t base = uid.address(); + uintptr_t off = REL::Relocate(0xFCA4 - 0xF950, 0xF05 - 0xBB0, 0x387); + if (!SKSE::stl::install_context_hook(base + off, 5, Hook_AccumulatedLightsArray, 5)) + logger::error("[SCM] Failed to install Hook_AccumulatedLightsArray"); + } + + // Force per-light shadow map slot assignment. + // Required whenever our temporal scheduler is active (ShadowLightCount >= 4): + // RenderCascade recalculates the slot from a global counter each call; without + // this hook, a light not redrawn this frame gets a different slot than last + // frame and corrupts another light's shadow map. + { + // SE: RenderCascade+0xBE; VR: RenderCascade+0xE0 + static REL::RelocationID uid(100820, 107604); + uintptr_t base = uid.address(); + uintptr_t off = REL::Relocate(0xA9E - 0x9E0, 0xDB0 - 0xCF0, 0xe0); + if (!SKSE::stl::install_context_hook(base + off, 0x25, Hook_OverwriteShadowMapIndex)) + logger::error("[SCM] Failed to install Hook_OverwriteShadowMapIndex"); + } + + // Suppress the engine's focus shadow path in extended mode (matches + // Intellightent's mitigation). In extended mode parabolic lights + // occupy kSHADOWMAPS slots [4..7] -- the same range g_focusShadow- + // BaseSlotIndex (=4) reserves for focus rendering. If the engine + // enters BSShadowParabolicLight::Render's focus loop on a parabolic + // light in those slots it CTDs without a crashlog. Two layers of + // defense: these byte patches zero the engine's global gate, and + // ScheduleShadowCasters scrubs drawFocusShadows on every light + // per-frame to clear stale flags. The per-frame scrub alone would + // suffice; the patches make the suppression robust against any + // engine path that bypasses the per-light flag. + if (extended) { + const uint8_t xorRax[6] = { 0x48, 0x31, 0xC0, 0x90, 0x90, 0x90 }; + + static REL::RelocationID uid1(10209, 10247); + REL::safe_write(uid1.address(), xorRax, 6); + + static REL::RelocationID uid2(10207, 10245); + REL::safe_write(uid2.address(), xorRax, 6); + + static REL::RelocationID uid3(513201, 390932); + const uint8_t zero = 0; + REL::safe_write(uid3.address(), &zero, 1); + } + + // ---- Screen-space shadow-mask pass: clamp to vanilla 4 slices --------- + // Detour RenderShadowLightsWithUtilityShader (100423/107141). The wrapper + // runs vanilla but writes a null sentinel into shadowLightsAccum at the + // 4-slice cutoff so the engine's loop never OOB-reads the 4-entry + // per-slot blend-mode table for any extended-mode slot. The mask's R + // channel (sun cascades) is the only channel LIGHT_LIMIT_FIX consumes; + // extended shadow casters are served by LLF's cluster pipeline reading + // kSHADOWMAPS directly. See the Hook_RenderShadowLightsWithUtilityShader + // definition above for the full rationale, including the previous + // Hook_DisableColorMask's misread (it patched out the inner call, not a + // color-mask call -- verified via Ghidra). + stl::detour_thunk( + REL::RelocationID(100423, 107141)); + + // ---- Shadow caster selection ----------------------------------------- + + // Replace CalculateActiveShadowCasterLights entirely (ID 100419/107137). + // VR confirmed: 0x1413226e0 + stl::detour_thunk(REL::RelocationID(100419, 107137)); + + // Replace the CALL to RenderActiveShadowCasterLights inside the render loop. + // ID 100415/107133; VR confirmed: 0x141322130 + // Offsets: SE = 0xF76-0xE30 (0x146), AE = 0xC17D-0xBFF0 (0x18D), VR = 0x1CA + // Must use install_context_hook (not write_thunk_call) so RtlRestoreContext restores + // volatile registers (r8, etc.) before the game continues past the call site. + { + static REL::RelocationID uid(100415, 107133); + uintptr_t addr = uid.address() + REL::Relocate(0xF76 - 0xE30, 0xC17D - 0xBFF0, 0x1CA); + if (!SKSE::stl::install_context_hook(addr, 5, Hook_RenderShadowLights)) + logger::error("[SCM] Failed to install Hook_RenderShadowLights"); + } + + // Replace CalculateActiveNonShadowCasterLights (surface light injection). + // ID 100997/107784; VR confirmed: 0x141354d20 + // Uses install_context_hook because the function has 10 args (11 in VR) with + // platform-specific stack layout. We write a RET at func+5 so + // RtlRestoreContext lands on ret and the function returns cleanly. + { + static REL::RelocationID uid(100997, 107784); + if (!SKSE::stl::install_context_hook(uid.address(), 5, Hook_CalculateActiveLightsForSurface)) + logger::error("[SCM] Failed to install Hook_CalculateActiveLightsForSurface"); + const uint8_t ret = 0xC3; + REL::safe_write(uid.address() + 5, &ret, 1); + } + + // ---- Stealth detection fix ------------------------------------------- + // GetLightLevel (ID 38900/39946) iterates shadow lights to check which + // affect the player. We replace that iteration with our own. + // VR: 38900 confirmed (0x1406892e0); offsets assumed same as SE for VR. + { + static REL::RelocationID uid(38900, 39946); + + // Hook at the start of the affect-player loop. + // Original bytes: "41 83 CE FF 33 C0" (6 bytes) -- keep them running first. + uintptr_t off1 = REL::Relocate(0x185 - 0x050, 0x847 - 0x710, 0x185 - 0x050); + if (!SKSE::stl::install_context_hook(uid.address() + off1, 6, Hook_UpdateLightLevelPlayer, 6)) + logger::error("[SCM] Failed to install Hook_UpdateLightLevelPlayer"); + + // Byte patch: change JA (0x73) to JMP (0xEB) to skip the vanilla iteration. + uintptr_t off2 = REL::Relocate(0x194 - 0x050, 0x856 - 0x710, 0x194 - 0x050); + const uint8_t jmp = 0xEB; + REL::safe_write(uid.address() + off2, &jmp, 1); + } + // Per-light check (ID 99725/106362): not yet confirmed in VR address library, + // so guard VR until addresses are found. + if (!REL::Module::IsVR()) { + static REL::RelocationID uid(99725, 106362); + uintptr_t off = REL::Relocate(0x648 - 0x560, 0xB49 - 0xA60, 0x648 - 0x560); + if (!SKSE::stl::install_context_hook(uid.address() + off, 5, Hook_CheckLightLevelPlayer)) + logger::error("[SCM] Failed to install Hook_CheckLightLevelPlayer"); + } + + // ---- Light conversion ------------------------------------------------ + // All conversion hooks install unconditionally; runtime behaviour is + // gated by s_settings.ConvertExcessToNormal / PromoteNormalToShadow + // and container membership. When both flags are false the hooks fire + // but are no-ops -- required so toggling either flag on at runtime + // takes effect without a restart. + + { + // BSShadowLight vtable slot 3 = IsShadowLight; replace on all 4 shadow light types. + // Reads s_normalConvert membership -- empty when ConvertExcessToNormal + // off, so the hook returns vanilla truth for every light. + REL::Relocation vtbl1{ RE::BSShadowLight::VTABLE[0] }; + vtbl1.write_vfunc(3, Hook_IsShadowLight); + REL::Relocation vtbl2{ RE::BSShadowDirectionalLight::VTABLE[0] }; + vtbl2.write_vfunc(3, Hook_IsShadowLight); + REL::Relocation vtbl3{ RE::BSShadowFrustumLight::VTABLE[0] }; + vtbl3.write_vfunc(3, Hook_IsShadowLight); + REL::Relocation vtbl4{ RE::BSShadowParabolicLight::VTABLE[0] }; + vtbl4.write_vfunc(3, Hook_IsShadowLight); + } + + { + // ShadowSceneNode::RemoveLight -- fires at +0x9 (SE: 6 bytes, AE: 5 bytes). + // Drains s_normalConvert / s_shadowConvert entries for the removed light. + // No-op when both containers are empty. + static REL::RelocationID uid(99697, 106331); + int sz = REL::Relocate(6, 5, 6); + if (!SKSE::stl::install_context_hook(uid.address() + REL::Relocate(0x9, 0x9, 0x9), sz, Hook_ConvertLights_Remove, sz)) + logger::error("[SCM] Failed to install Hook_ConvertLights_Remove"); + } + + { + // ShadowSceneNode::AddLight -- at function start (5 bytes). + // Applies portal-strict per type (always) and PromoteNormalToShadow + // flag mutation (when enabled). + static REL::RelocationID uid(99692, 106326); + if (!SKSE::stl::install_context_hook(uid.address(), 5, Hook_ConvertLights_Add, 5)) + logger::error("[SCM] Failed to install Hook_ConvertLights_Add"); + } + + { + // BSLight::SetLight -- at function start (5 bytes). + // Tracks NiLight* reassignments for s_shadowConvert. No-op when + // PromoteNormalToShadow is off (s_shadowConvert is empty). + static REL::RelocationID uid(101302, 108289); + if (!SKSE::stl::install_context_hook(uid.address(), 5, Hook_ConvertLights_SetLight, 5)) + logger::error("[SCM] Failed to install Hook_ConvertLights_SetLight"); + } + + logger::info("[SCM] Hooks installed (ShadowLightCount={})", settings.ShadowLightCount); + + // Wholesale reset on LoadingMenu open so transient session state + // (s_normalConvert, s_lights pool, debug pins) drops the previous + // cell's pointers before the engine tears them down. Mirrors the + // pattern in DynamicCubemaps for similar reset-on-scene-transition + // behaviour. + RegisterSceneTransitionEvents(); + + // DXGI budget snapshot at install. Per-slice geometry follows once + // Update() sees a non-null kSHADOWMAPS SRV. + if (auto* menu = Menu::GetSingleton()) { + if (auto adapter3 = menu->GetDXGIAdapter3()) { + DXGI_QUERY_VIDEO_MEMORY_INFO vmem{}; + if (SUCCEEDED(adapter3->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &vmem)) && vmem.Budget > 0) { + const float budgetMB = static_cast(vmem.Budget) / (1024.f * 1024.f); + const float usageMB = static_cast(vmem.CurrentUsage) / (1024.f * 1024.f); + logger::info("[SCM] VRAM at install: {:.1f}/{:.1f} MB used", usageMB, budgetMB); + } + } + } + } + + void Update(const Settings& settings, RE::ShadowSceneNode* /*shadowSceneNode*/, + RE::NiCamera* /*worldCamera*/) + { + ZoneScopedN("SCM::Update"); + if (s_externalConflict) + return; + + // Lazy verification of the kSHADOWMAPS allocation. Self-healing: + // retries until kSHADOWMAPS exists, then early-returns. Cheap. + // This must run BEFORE the clamp below so a VRAM-exhaustion + // fallback gets reflected in the same frame the verification + // succeeds. + RefreshInstalledSlotCount(); + + Settings capped = settings; + if (s_installedShadowLightCount > 0) + capped.ShadowLightCount = std::min(settings.ShadowLightCount, s_installedShadowLightCount); + + int newTotal = LightContainerSize(capped); + if (newTotal != s_lights.Size) { + auto* newLights = new LightEntry[newTotal](); + int copyCount = std::min(s_lights.Size, newTotal); + for (int i = 0; i < copyCount; i++) + newLights[i] = s_lights.Lights[i]; + for (int i = copyCount; i < newTotal; i++) + newLights[i].Index = i; + delete[] s_lights.Lights; + s_lights.Lights = newLights; + s_lights.Size = newTotal; + } + + // Apply settings as a pure flag flip. Conversion-related state + // (s_normalConvert, s_shadowConvert, s_lights pool) is NOT + // drained on toggles -- it ages out at the next LoadingMenu + // via the natural ResetSession() in SceneTransitionEventHandler. + // See Hook_CalculateActiveShadowCasters::thunk for the rationale: + // wholesale clearing mid-session caused engine accumulate-shadow + // crashes (2026-05-17 crash logs) because the engine still had + // our converted/promoted lights in activeShadowLights with + // half-populated shadowmapDescriptors, and tearing our tracking left + // the engine walking that half-state. + // + // Each setting's gating still takes effect immediately via the + // runtime checks in the relevant hook / scheduler branches: + // - Enabled: per-frame thunk routes to vanilla; OverwriteShadowMapIndex no-ops. + // - ConvertExcessToNormal: convertOrDisable routes excess omnis to DisableLight. + // - PromoteNormalToShadow: Hook_ConvertLights_Add stops promoting. + // "Off = stop converting" is the documented semantic; existing + // converted/promoted lights persist in their current form until + // the engine itself drops them at cell change. + s_settings = capped; + } + + void ResetSession() + { + // Wholesale drop of pointers the engine is about to free during + // a scene transition. Called by RegisterSceneTransitionEvents + // when the LoadingMenu opens. The per-frame reconciliation in + // ScheduleShadowCasters keeps these caches honest during normal + // play; this is the explicit "scene is gone" signal so the UI + // counter, debug pins, and tracking sets read empty during the + // loading screen rather than displaying stale entries from the + // previous cell. + // + // Stale BSRenderPass.sceneLights[] captures that would otherwise AV + // in BSEffectShader::SetupGeometry are handled by the defensive + // guard there (clamps numLights past the first stale entry), not by + // trying to drive engine-side cleanup from here. An earlier version + // tried calling ShadowSceneNode::RemoveLight to undo our + // ConvertLight -> GameEnableLight pinning, but the engine function + // takes NiLight* (not BSLight* as the wrapper assumed); the call + // was a silent no-op on every runtime and accomplished nothing. + s_normalConvert.clear(); + s_shadowConvert.clear(); + s_pinShadow.clear(); + s_pinConvert.clear(); + s_soloLight = 0; + s_suppressedLights.clear(); + // Clear pool entries but keep the array allocation; size is set by + // Install/Update based on the configured ShadowLightCount. + if (s_lights.Lights) { + for (int i = 0; i < s_lights.Size; ++i) + s_lights.Lights[i].Clear(); + s_lights.Sun = false; + } + } + + class SceneTransitionEventHandler : public RE::BSTEventSink + { + public: + RE::BSEventNotifyControl ProcessEvent(const RE::MenuOpenCloseEvent* a_event, + RE::BSTEventSource*) override + { + if (a_event && a_event->menuName == RE::LoadingMenu::MENU_NAME && a_event->opening) + ResetSession(); + return RE::BSEventNotifyControl::kContinue; + } + static SceneTransitionEventHandler* GetSingleton() + { + static SceneTransitionEventHandler singleton; + return &singleton; + } + }; + + void RegisterSceneTransitionEvents() + { + auto* ui = globals::game::ui; + if (!ui) { + logger::error("[SCM] No UI singleton; cannot register LoadingMenu handler"); + return; + } + ui->AddEventSink(SceneTransitionEventHandler::GetSingleton()); + logger::info("[SCM] LoadingMenu event handler registered"); + } + + const LightContainer& GetLights() + { + return s_lights; + } + + int32_t GetShadowSlot(RE::BSShadowLight* light) + { + // Returns the kSHADOWMAPS texture-array slot for `light`, or -1 if the + // light has no kSHADOWMAPS slice. Pool index == texture slot for point + // lights (1:1). Sun's pool slot returns -1 since the sun renders to + // kSHADOWMAPS_ESRAM (a separate texture) — callers in ShadowRenderer + // upload and LightLimitFix cluster builder must skip it. + const int32_t poolIdx = s_lights.FindLight(light, s_settings.ShadowLightCount); + if (poolIdx < 0) + return -1; + if (s_lights.Sun && poolIdx == 0) + return -1; // sun + return poolIdx; + } + + void ForEachConvertedLight(const std::function& visitor) + { + for (auto& c : s_normalConvert) { + if (!c.light) + continue; + // Defensive vtable check: catches lights freed and zeroed by + // tbbmalloc / EngineFixes between our per-frame reconciliation + // in ScheduleShadowCasters and the cluster builder running. The + // reconciliation prunes stale pointers up-front, but a bulk + // engine teardown could still happen mid-frame. + if (*reinterpret_cast(c.light) == 0) + continue; + visitor(c.light); + } + } + + // ========================================================================= + // Per-slot visualization state (owned by ShadowCasterManager) + // ========================================================================= + + static constexpr const char* kShadowTypeNames[] = { "Spot", "Hemisphere", "Omni" }; + + static std::vector s_shadowSlotInfos; + static uint32_t s_shadowSlotUsage = 0; + // Persists last-seen ShadowSlotInfo for every light ever recorded this session, + // so suppressed lights that leave the active slots still have metadata for the settings table. + static std::unordered_map s_knownLights; + + /// Computes the golden-ratio hue color for a shadow-map slot (matches mode-8 shader). + ImVec4 ShadowSlotHueColor(uint32_t slotIdx) + { + auto chan = [](float h, float shift) { + float v = fmodf(h + shift, 1.0f); + if (v < 0.0f) + v += 1.0f; + return std::clamp(fabsf(v * 6.0f - 3.0f) - 1.0f, 0.0f, 1.0f); + }; + float hue = fmodf(float(slotIdx) * 0.618033988f, 1.0f); + return ImVec4(chan(hue, 0.0f), chan(hue, 2.0f / 3.0f), chan(hue, 1.0f / 3.0f), 1.0f); + } + + // ========================================================================= + // Slot frame API implementations + // ========================================================================= + + void BeginSlotFrame(uint32_t slotCount) + { + s_shadowSlotInfos.assign(slotCount, ShadowSlotInfo{}); + s_shadowSlotUsage = 0; + } + + void RecordSlot(uint32_t depthSlot, const ShadowSlotInfo& info) + { + if (depthSlot < static_cast(s_shadowSlotInfos.size())) + s_shadowSlotInfos[depthSlot] = info; + // Omni lights (type 2) occupy 2 depth-texture slices; all others use 1. + s_shadowSlotUsage += (info.type == 2) ? 2 : 1; + s_knownLights[info.lightKey] = info; + } + + bool IsSuppressed(uintptr_t lightKey) + { + if (s_suppressedLights.count(lightKey)) + return true; + // Solo: every key except the soloed one is implicitly suppressed. + if (s_soloLight != 0 && s_soloLight != lightKey) + return true; + return false; + } + + bool IsPinnedShadow(uintptr_t lightKey) { return s_pinShadow.count(lightKey) > 0; } + bool IsPinnedConvert(uintptr_t lightKey) { return s_pinConvert.count(lightKey) > 0; } + + void SetPinnedShadow(uintptr_t lightKey, bool pinned) + { + if (pinned) { + s_pinShadow.insert(lightKey); + s_pinConvert.erase(lightKey); // mutually exclusive + s_suppressedLights.erase(lightKey); + } else { + s_pinShadow.erase(lightKey); + } + } + + void SetPinnedConvert(uintptr_t lightKey, bool pinned) + { + if (pinned) { + s_pinConvert.insert(lightKey); + s_pinShadow.erase(lightKey); + s_suppressedLights.erase(lightKey); + } else { + s_pinConvert.erase(lightKey); + } + } + + uintptr_t GetSoloLight() { return s_soloLight; } + void SetSoloLight(uintptr_t lightKey) { s_soloLight = lightKey; } + + uintptr_t GetHoveredLight() { return s_hoverLightKey; } + void SetHoveredLight(uintptr_t lightKey) { s_hoverLightKey = lightKey; } + + void ClearAllOverrides() + { + s_suppressedLights.clear(); + s_pinShadow.clear(); + s_pinConvert.clear(); + s_soloLight = 0; + // Hover key is transient (per-draw); not part of "overrides". + } + + bool HasAnyOverrides() + { + return !s_suppressedLights.empty() || !s_pinShadow.empty() || + !s_pinConvert.empty() || s_soloLight != 0; + } + + bool HasSuppressedLights() + { + return !s_suppressedLights.empty(); + } + + uint32_t GetSlotUsage() + { + return s_shadowSlotUsage; + } + + uint32_t GetHighImportanceCount() + { + return s_highImportanceLightCount; + } + + const std::vector& GetSlotInfos() + { + return s_shadowSlotInfos; + } + + const char* GetShadowTypeName(uint32_t type) + { + return kShadowTypeNames[std::min(type, 2u)]; + } + + // ========================================================================= + // DrawShadowLightTable + // ========================================================================= + // Interactive shadow caster table: suppress/re-enable per light or by type, + // filter by type name/range/address, sort by any column. + // Rows are keyed by lightKey (light object pointer) so suppression persists + // across slot reassignments as the player moves around. + // + // compact=true -> auto-sizes height (up to 15 rows visible) + // compact=false -> fills available window height (resizable overlay window) + // showColor -> adds a golden-ratio hue swatch column (visualization mode 8) + // ========================================================================= + + void DrawShadowLightTable(bool compact, bool showColor, bool sceneOnly, bool readOnly) + { + // Hover key is set per-row here and consumed (cleared) once per frame + // by UpdateLights. Do NOT clear it at function entry -- if both the + // settings-menu table and the overlay table render in the same frame, + // a top-of-function clear would let the second table clobber the + // hover set by the first. Only one row hovers at a time, so the two + // callsites can't fight. + + struct SlotRow + { + uint32_t idx; // shadow slot index; only meaningful when inScene=true + bool inScene; // currently occupies a shadow slot this frame + bool converted; // demoted to non-shadow rendering via ConvertExcessToNormal + bool isFocus{ false }; // engine-owned focus shadow slot (read-only row) + ShadowSlotInfo info; + float importance{ 0.0f }; // contribution-weighted importance (luminance × fade × attenuation²) + bool highImp{ false }; // importance > 0.1 — light meaningfully illuminates the viewer area + }; + + // Build index of lights currently in scene (slot -> info). + // Static containers avoid per-frame heap allocation. + static std::unordered_map sceneSlot; + sceneSlot.clear(); + for (uint32_t i = 0; i < static_cast(s_shadowSlotInfos.size()); ++i) + if (s_shadowSlotInfos[i].valid) + sceneSlot[s_shadowSlotInfos[i].lightKey] = i; + + // Build lightKey -> LightEntry* lookup for debug columns. + static std::unordered_map lightEntryByKey; + lightEntryByKey.clear(); + for (int li = 0; li < s_lights.Size; ++li) { + const auto& e = s_lights.Lights[li]; + if (e.Light) + lightEntryByKey[reinterpret_cast(e.Light)] = &e; + } + + auto applyEntryDebug = [&](SlotRow& row) { + auto it = lightEntryByKey.find(row.info.lightKey); + if (it != lightEntryByKey.end()) { + row.importance = it->second->lastImportance; + row.highImp = row.importance > 0.1f; + } + }; + + // Build set of converted-light keys (shadow lights demoted to non-shadow + // rendering via ConvertExcessToNormal). These don't occupy a shadow slot + // this frame but are still active in the scene as normal lights — we want + // them visible in the table with a "Conv" indicator and the same suppress + // toggle so users can hide them like any other shadow caster. + static std::unordered_set convertedKeys; + convertedKeys.clear(); + ForEachConvertedLight([&](RE::BSShadowLight* light) { + convertedKeys.insert(reinterpret_cast(light)); + }); + + // Build row list. + static std::vector rows; + rows.clear(); + auto addConvertedRows = [&]() { + for (uintptr_t key : convertedKeys) { + if (sceneSlot.count(key)) + continue; // simultaneously a shadow caster this frame + SlotRow r{ 0, false, true, false, {} }; + auto it = s_knownLights.find(key); + if (it != s_knownLights.end()) { + r.info = it->second; + r.info.valid = false; // no shadow slot this frame + } else { + // First-frame convert: no cached metadata yet. Surface a minimal + // row so the user can still toggle suppression by address. + r.info.lightKey = key; + } + applyEntryDebug(r); + rows.push_back(r); + } + }; + if (sceneOnly) { + rows.reserve(sceneSlot.size() + convertedKeys.size()); + for (auto& [key, idx] : sceneSlot) { + SlotRow r{ idx, true, false, false, s_shadowSlotInfos[idx] }; + applyEntryDebug(r); + rows.push_back(r); + } + addConvertedRows(); + } else { + // All scene lights first, then converted lights, then suppressed lights + // not currently in scene at all. + rows.reserve(sceneSlot.size() + convertedKeys.size() + s_suppressedLights.size()); + for (auto& [key, idx] : sceneSlot) { + SlotRow r{ idx, true, false, false, s_shadowSlotInfos[idx] }; + applyEntryDebug(r); + rows.push_back(r); + } + addConvertedRows(); + for (uintptr_t key : s_suppressedLights) { + if (sceneSlot.count(key) || convertedKeys.count(key)) + continue; + auto it = s_knownLights.find(key); + if (it != s_knownLights.end()) { + SlotRow r{ 0, false, false, false, it->second }; + applyEntryDebug(r); + rows.push_back(r); + } + } + } + + // Engine-owned focus shadow rows. One per active focus actor at the + // matching kSHADOWMAPS slot. Synthetic lightKey encodes the slot index + // so each row is unique without colliding with real BSShadowLight + // pointers (top-half-set is impossible for user-mode allocations). + for (int32_t i = 0; i < s_focusShadowSlots; ++i) { + SlotRow r{}; + r.idx = static_cast(kFocusShadowBaseSlotIndex + i); + r.inScene = true; + r.isFocus = true; + r.info.valid = true; + r.info.lightKey = 0xFEFE'0000ULL | static_cast(r.idx); + r.info.type = 0; // surfaced as "Focus" in the type column override below + rows.push_back(r); + } + + if (rows.empty()) { + ImGui::TextDisabled("No shadow slots this frame."); + return; + } + + // -- Header: active count + suppression badge ---------------------- + ImGui::Text("Shadow slots: %u active", s_shadowSlotUsage); + if (!s_suppressedLights.empty()) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0.6f, 0.2f, 1), " %zu suppressed", s_suppressedLights.size()); + } + + // -- Group toggle buttons ------------------------------------------ + // green = at least one unsuppressed; grey = all suppressed; click flips. + // Predicate-based so we can mix type filters (Spot/Hemi/Omni) with state + // filters (Conv = converted-to-normal lights). + { + using RowPred = std::function; + auto allSuppressedMatching = [&](const RowPred& pred) { + bool sawAny = false; + for (auto& r : rows) { + if (!pred(r)) + continue; + sawAny = true; + if (!s_suppressedLights.count(r.info.lightKey)) + return false; + } + // If nothing matches, treat as "all suppressed" so the button shows + // grey/disabled (clicking a no-op button does nothing). + return sawAny; + }; + auto toggleMatching = [&](const RowPred& pred) { + if (allSuppressedMatching(pred)) { + for (auto& r : rows) + if (pred(r)) + s_suppressedLights.erase(r.info.lightKey); + } else { + for (auto& r : rows) + if (pred(r)) + s_suppressedLights.insert(r.info.lightKey); + } + }; + auto groupButton = [&](const char* label, const RowPred& pred, const char* tooltip) { + bool allOff = allSuppressedMatching(pred); + ImGui::PushStyleColor(ImGuiCol_Button, + allOff ? ImVec4(0.35f, 0.35f, 0.35f, 1) : ImVec4(0.15f, 0.5f, 0.15f, 1)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, + allOff ? ImVec4(0.5f, 0.5f, 0.5f, 1) : ImVec4(0.2f, 0.7f, 0.2f, 1)); + if (ImGui::SmallButton(label)) + toggleMatching(pred); + ImGui::PopStyleColor(2); + if (tooltip && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", tooltip); + }; + auto typePred = [](uint32_t type) { + return [type](const SlotRow& r) { return r.info.type == type; }; + }; + groupButton( + "All", [](const SlotRow&) { return true; }, nullptr); + ImGui::SameLine(); + groupButton("Spot", typePred(0), "Toggle all spot/frustum shadow lights"); + ImGui::SameLine(); + groupButton("Hemi", typePred(1), "Toggle all hemisphere shadow lights"); + ImGui::SameLine(); + groupButton("Omni", typePred(2), "Toggle all omni (paraboloid) shadow lights"); + ImGui::SameLine(); + groupButton( + "Conv", [](const SlotRow& r) { return r.converted; }, + "Toggle all lights currently demoted from shadow to normal\n" + "(ConvertExcessToNormal). Hides their cluster-light contribution."); + + // "Clear All": resets every debug override (suppress / pin shadow / + // pin convert / solo) so the table returns to scheduler-auto. Only + // shown when overrides are active so it doesn't take up space when + // there's nothing to reset. + if (HasAnyOverrides()) { + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.25f, 0.25f, 1)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.35f, 0.35f, 1)); + if (ImGui::SmallButton("Clear All")) + ClearAllOverrides(); + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Reset every debug override:\n" + " - clear suppression\n" + " - clear shadow / convert pins\n" + " - clear solo\n" + "Returns the table to scheduler-auto behaviour."); + } + + // Help marker: explains the per-row debug controls so users aren't + // surprised by states / pulses they didn't know they could trigger. + ImGui::SameLine(); + Util::HelpMarker( + "Per-row controls:\n" + " * Cycle button (col 1): click to rotate this light through\n" + " Auto -> Shadow pin (S) -> Convert pin (C) -> Suppress (X) -> Auto.\n" + " * Solo button (col 2): isolate this light against a black scene.\n" + " Click again to clear; only one light may be soloed at a time.\n" + " * Hold Shift while hovering a row to highlight that light in the\n" + " world with a pulsing magenta tint. Release Shift or move the\n" + " cursor away to stop. Useful when you can't tell which entry\n" + " corresponds to which physical light. Does not affect rendering\n" + " when Shift is not held.\n\n" + "Group buttons toggle suppression for every matching row at once.\n" + "Clear All appears when any override is active and resets everything."); + } + + // -- Filter input -------------------------------------------------- + static std::string s_filterText; + { + char buf[128] = {}; + strncpy_s(buf, s_filterText.c_str(), sizeof(buf) - 1); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputText("##slotfilter", buf, sizeof(buf))) + s_filterText = buf; + ImGui::SameLine(); + ImGui::TextDisabled(sceneOnly ? "filter (yes/conv/type/range/addr)" : "filter (yes/conv/no/type/range/addr)"); + } + + // Apply filter. + static std::vector filteredRows; + filteredRows.clear(); + if (s_filterText.empty()) { + filteredRows = rows; + } else { + std::string lower = s_filterText; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + char addrBuf[16]; + for (auto& r : rows) { + std::string typeName = kShadowTypeNames[std::min(r.info.type, 2u)]; + std::transform(typeName.begin(), typeName.end(), typeName.begin(), ::tolower); + // Range filter matches both raw units and rounded meters. + char rangeBuf[32]; + snprintf(rangeBuf, sizeof(rangeBuf), "%.0f %.0f", + r.info.range, Util::Units::GameUnitsToMeters(r.info.range)); + snprintf(addrBuf, sizeof(addrBuf), "%08x", static_cast(r.info.lightKey & 0xFFFFFFFF)); + const char* statusStr = r.inScene ? "yes" : (r.converted ? "conv" : "no"); + if (typeName.find(lower) != std::string::npos || + std::string(rangeBuf).find(lower) != std::string::npos || + std::string(addrBuf).find(lower) != std::string::npos || + lower == statusStr) + filteredRows.push_back(r); + } + } + + // -- Column layout ------------------------------------------------- + // Interactive (settings menu, or overlay with menu open): + // [Mode] [Solo] [Status] [Address] [Color?] [Type] [Range] [Imp] + // Read-only (overlay with menu closed -- buttons would be dead pixels): + // [Status] [Address] [Color?] [Type] [Range] [Imp] + // + // Status merges the old "In Scene" + "Slot" columns into one cell + // showing one of: "Slot N" / "Conv" / "Out" / "Suppr". The old "Hi" + // boolean column is gone -- highImp now tints the row instead, which + // is what the column was being used for visually. + const bool showButtons = !readOnly; + const int modeColIdx = showButtons ? 0 : -1; + const int soloColIdx = showButtons ? 1 : -1; + const int statusColIdx = showButtons ? 2 : 0; + const int addrColIdx = statusColIdx + 1; + const int typeColIdx = addrColIdx + (showColor ? 2 : 1); + const int radColIdx = typeColIdx + 1; + const int centrColIdx = radColIdx + 1; + + std::vector headers; + if (showButtons) { + headers.push_back("Mode"); // cycle: Auto / Pin-S / Pin-C / Suppress + headers.push_back("Solo"); + } + headers.push_back("Status"); + headers.push_back("Address"); + if (showColor) + headers.push_back("Color"); + headers.push_back("Type"); + headers.push_back("Range"); + headers.push_back("Imp"); + + using SortFn = std::function; + std::vector sorts(headers.size(), nullptr); + // Status sort: in-scene shadow casters → converted → out-of-scene. + // Suppressed lights sort to the end (treated as worst rank). + sorts[statusColIdx] = [](const SlotRow& a, const SlotRow& b, bool asc) { + auto rank = [](const SlotRow& r) -> int { + bool sup = s_suppressedLights.count(r.info.lightKey) > 0; + if (sup) + return 3; + return r.inScene ? 0 : (r.converted ? 1 : 2); + }; + int ra = rank(a), rb = rank(b); + if (ra != rb) + return asc ? ra < rb : ra > rb; + return asc ? a.idx < b.idx : a.idx > b.idx; + }; + sorts[addrColIdx] = [](const SlotRow& a, const SlotRow& b, bool asc) { + return asc ? a.info.lightKey < b.info.lightKey : a.info.lightKey > b.info.lightKey; + }; + sorts[typeColIdx] = [](const SlotRow& a, const SlotRow& b, bool asc) { + return asc ? a.info.type < b.info.type : a.info.type > b.info.type; + }; + sorts[radColIdx] = [](const SlotRow& a, const SlotRow& b, bool asc) { + return asc ? a.info.range < b.info.range : a.info.range > b.info.range; + }; + sorts[centrColIdx] = [](const SlotRow& a, const SlotRow& b, bool asc) { + return asc ? a.importance < b.importance : a.importance > b.importance; + }; + + // outerSize logic: + // * compact auto-size up to 15 rows (handled by + // ShowSortedStringTableCustom when y==0). Used in + // the menu's Active Casters block where the table + // is one of several elements in a long settings + // list and shouldn't grab unbounded vertical space. + // * non-compact fill remaining vertical space. The table itself + // scrolls internally (ScrollY flag in the shared + // helper) so summary stats above stay visible + // regardless of how many lights exist or how the + // user has sized the host window. + ImVec2 outerSize = compact ? ImVec2(0, 0) : ImVec2(0, ImGui::GetContentRegionAvail().y); + + Util::ShowSortedStringTableCustom( + "##ShadowLightTbl", + headers, + filteredRows, + static_cast(statusColIdx), // default sort: Status + true, // ascending + sorts, + [&](int /*rowIdx*/, int col, const SlotRow& row) { + const uintptr_t key = row.info.lightKey; + const bool suppressed = s_suppressedLights.count(key) > 0; + const bool pinShadow = s_pinShadow.count(key) > 0; + const bool pinConvert = s_pinConvert.count(key) > 0; + const bool isSolo = (s_soloLight == key && key != 0); + + // Helper: shift-gated debug pulse. Setting s_hoverLightKey makes + // the cluster light builder replace this light's colour with a + // 1Hz magenta pulse — useful for finding which light a row + // corresponds to in 3D, but visually startling if it triggered + // every time the cursor crossed a cell. Requiring Shift+hover + // means a user clicking through the cycle/solo buttons doesn't + // see lights randomly turn purple, while debugging is one + // modifier away. + auto noteHover = [&]() { + if (ImGui::IsItemHovered() && ImGui::GetIO().KeyShift) + s_hoverLightKey = key; + }; + + // Row tint: highImp lights get a subtle yellow background so + // the eye can pick out the lights actually contributing to the + // frame at a glance. Replaces the dropped "Hi" column. Set on + // col 0 so it applies to the whole row. + if (col == 0 && row.highImp) { + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, + ImGui::GetColorU32(ImVec4(0.30f, 0.30f, 0.10f, 0.35f))); + } + + // === Mode column: state cycle button ======================= + // Cycle: Auto (·) -> PinShadow (S) -> PinConvert (C) -> Suppress (X) -> Auto + // Mutually exclusive (SetPinned* / suppressed.erase enforce that). + // Hidden in readOnly mode (overlay with menu closed). + // Focus rows skip Mode/Solo entirely -- engine owns the slot. + if (row.isFocus && col == modeColIdx) { + ImGui::TextDisabled("eng"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Engine-controlled focus shadow; not pinnable/suppressible."); + return; + } + if (row.isFocus && col == soloColIdx) { + ImGui::TextDisabled("--"); + return; + } + if (showButtons && col == modeColIdx) { + ImGui::PushID(static_cast(key & 0xFFFFFFFF)); + const char* label = "·"; + ImVec4 col4 = ImVec4(0.15f, 0.6f, 0.15f, 1); // green = auto/active + ImVec4 colH = ImVec4(0.2f, 0.75f, 0.2f, 1); + const char* tip = "Auto (scheduler decides)\nClick: pin as shadow caster"; + if (pinShadow) { + label = "S"; + col4 = ImVec4(0.20f, 0.40f, 0.85f, 1); // blue + colH = ImVec4(0.30f, 0.55f, 1.0f, 1); + tip = "Pinned: forced shadow caster\nClick: pin as converted (non-shadow)"; + } else if (pinConvert) { + label = "C"; + col4 = ImVec4(0.85f, 0.55f, 0.15f, 1); // amber + colH = ImVec4(1.0f, 0.7f, 0.25f, 1); + tip = "Pinned: forced converted (non-shadow)\nClick: suppress entirely"; + } else if (suppressed) { + label = "X"; + col4 = ImVec4(0.45f, 0.25f, 0.25f, 1); // dim red + colH = ImVec4(0.6f, 0.35f, 0.35f, 1); + tip = "Suppressed (hidden)\nClick: return to auto"; + } + ImGui::PushStyleColor(ImGuiCol_Button, col4); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colH); + if (ImGui::SmallButton(label)) { + // Cycle to next state. + if (pinShadow) { + SetPinnedShadow(key, false); + SetPinnedConvert(key, true); + } else if (pinConvert) { + SetPinnedConvert(key, false); + s_suppressedLights.insert(key); + } else if (suppressed) { + s_suppressedLights.erase(key); + } else { + SetPinnedShadow(key, true); + } + } + ImGui::PopStyleColor(2); + noteHover(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", tip); + ImGui::PopID(); + return; + } + + // === Solo column ========================================== + // Hidden in readOnly mode. + if (showButtons && col == soloColIdx) { + ImGui::PushID(static_cast((key & 0xFFFFFFFF) ^ 0xA1)); + ImVec4 col4 = isSolo ? + ImVec4(0.85f, 0.7f, 0.15f, 1) : // bright yellow when active + ImVec4(0.30f, 0.30f, 0.30f, 1); + ImVec4 colH = isSolo ? ImVec4(1.0f, 0.85f, 0.25f, 1) : ImVec4(0.45f, 0.45f, 0.45f, 1); + ImGui::PushStyleColor(ImGuiCol_Button, col4); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colH); + if (ImGui::SmallButton(isSolo ? "!" : "·")) + SetSoloLight(isSolo ? 0 : key); + ImGui::PopStyleColor(2); + noteHover(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", + isSolo ? + "Solo: this light is shown alone\nClick: clear solo" : + "Solo this light\n(suppresses every other light\nuntil cleared)"); + ImGui::PopID(); + return; + } + + if (suppressed || (s_soloLight != 0 && !isSolo)) + ImGui::BeginDisabled(); + bool dimmed = suppressed || (s_soloLight != 0 && !isSolo); + if (col == statusColIdx) { + // Merged "In Scene" + "Slot" column. Four mutually-exclusive + // states; suppressed wins because the user explicitly hid it. + if (suppressed) { + ImGui::TextColored(ImVec4(0.85f, 0.35f, 0.35f, 1), "Suppr"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Suppressed by debug override.\nClick the Mode button to clear."); + } else if (row.inScene) { + ImGui::Text("Slot %u", row.idx); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Casting shadows this frame in slot %u.", row.idx); + } else if (row.converted) { + ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1), "Conv"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Demoted to a normal (non-shadow) light this frame.\n" + "Cluster lighting still illuminates it; no shadow-map cost."); + } else { + ImGui::TextDisabled("Out"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Out of range / not active in the current frame."); + } + } else if (col == addrColIdx) { + if (row.isFocus) { + ImGui::TextDisabled("focus[%u]", row.idx - static_cast(kFocusShadowBaseSlotIndex)); + } else { + char addrFull[20]; + snprintf(addrFull, sizeof(addrFull), "0x%016llX", static_cast(row.info.lightKey)); + ImGui::Selectable(addrFull + 10, false, ImGuiSelectableFlags_None); + if (ImGui::IsItemClicked()) + ImGui::SetClipboardText(addrFull); + noteHover(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Click to copy: %s", addrFull); + } + } else if (showColor && col == addrColIdx + 1) { + ImVec4 c = ShadowSlotHueColor(row.idx); + auto ri = static_cast(c.x * 255.0f); + auto gi = static_cast(c.y * 255.0f); + auto bi = static_cast(c.z * 255.0f); + ImGui::ColorButton("##col", c, + ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoBorder, ImVec2(22, 16)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("#%02X%02X%02X", ri, gi, bi); + } else if (col == typeColIdx) { + if (row.isFocus) { + ImGui::TextColored(ImVec4(0.55f, 0.75f, 1.0f, 1.0f), "Focus"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Engine-owned focus shadow slot.\n" + "FocusShadowActors[%u] = high-res shadow for a tracked\n" + "actor (player + dialog/combat NPCs). SCM reserves\n" + "this slot so the engine's focus render isn't trampled\n" + "by point/spot lights.", + row.idx - static_cast(kFocusShadowBaseSlotIndex)); + } else { + ImGui::TextUnformatted(kShadowTypeNames[std::min(row.info.type, 2u)]); + noteHover(); + } + } else if (col == radColIdx) { + if (row.isFocus) { + ImGui::TextDisabled("--"); + } else { + ImGui::Text("%.0f u", row.info.range); + noteHover(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", Util::Units::FormatDistance(row.info.range).c_str()); + } + } else if (col == centrColIdx) { + // Importance score: luminance × fade × attenuation² at viewer. + // White (0) → bright green (1+) as contribution increases. + float imp = row.importance; + float t = std::min(imp, 1.0f); + ImVec4 colour = ImVec4(1.0f - t * 0.7f, 1.0f, 1.0f - t * 0.7f, 1.0f); // white → green + ImGui::TextColored(colour, "%.2f", imp); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Contribution importance score:\n" + " luminance(diffuse * fade)\n" + " * max(att_camera, att_player)\n" + " where att = (1 - (dist/radius)^2)^2\n\n" + "Higher = light strongly illuminates the viewer area.\n" + "Drives interval multiplier (configurable in Advanced settings).\n" + "Default: 0 => x2.0, 0.5 => x0.32, 1 => x0.05\n\n" + "Rows tinted yellow are high-importance (>0.1)\n" + "-- they deliver meaningful illumination near the camera\n" + "or player and receive accelerated shadow redraw scheduling."); + } + // Hi column dropped -- highImp now tints the row background + // (see TableSetBgColor at the top of this lambda) so the visual + // signal is preserved without consuming a column. + if (dimmed) + ImGui::EndDisabled(); + }, + {}, + outerSize); + } + + void DrawShadowSummary(uint32_t clusterCount, uint32_t clusterMax, uint32_t shadowUnshadowedLightCount) + { + // Canonical "where are we vs the limits" panel. Used by both the menu's + // Active Casters block and the overlay header so testers see the same + // numbers in the same format regardless of which view they're in. + const uint32_t slotUsage = s_shadowSlotUsage; + const uint32_t slots = GetInstalledSlotCount(); + // "Wanted" = total shadow-eligible demand this frame (active + dropped). + // We don't track demand separately, but slotUsage + dropped is the + // observable proxy that matches the user-visible "X dropped" signal. + const uint32_t requested = slotUsage + shadowUnshadowedLightCount; + + if (clusterCount >= clusterMax) + ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Cluster lights : %u / %u (overflow)", clusterCount, clusterMax); + else + ImGui::Text("Cluster lights : %u / %u", clusterCount, clusterMax); + + // "lights" rather than "slots" matches the Shadow Light Count + // setting name -- users think in lights, the engine thinks in + // texture slots, so we use the user's word. + if (shadowUnshadowedLightCount > 0) + ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), + "Shadow lights : %u / %u (%u wanted, %u dropped, %zu converted)", + slotUsage, slots, requested, shadowUnshadowedLightCount, s_normalConvert.size()); + else + ImGui::Text("Shadow lights : %u / %u (%u wanted, 0 dropped, %zu converted)", + slotUsage, slots, requested, s_normalConvert.size()); + + if (s_highImportanceLightCount > 0 && ImGui::IsItemHovered()) + ImGui::SetTooltip("%u high-importance (near camera/player).", + s_highImportanceLightCount); + } + + void DrawShadowSchedulerStats() + { + // Avg redraws/frame: rolling average of how many shadow casters per frame + // the scheduler decided to (re)render. Bounded by MaxRedrawPerFrame. + float avgRedraws = static_cast(s_redrawSum) / static_cast(kRedrawHistorySize); + ImGui::Text("Avg redraws/frame : %.1f (cap: %d)", avgRedraws, s_settings.MaxRedrawPerFrame); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Rolling average over the last %d frames.", kRedrawHistorySize); + + // Avg per-light cost: budget tracker's measured GPU cost per shadow caster. + // Used by the formula budget mode to decide how many casters fit in the + // per-frame time budget. + int32_t avgCost = s_budget.GetAverageCostUs(); + if (avgCost > 0) + ImGui::Text("Avg light cost : %.2f ms", avgCost / 1000.0f); + + // ---- Budget verdict --------------------------------------------- + // Cross-checks measured shadow cost against the user-chosen budget + // to surface "is your setup actually working?" without making the + // user math it out themselves. We compare measured shadow time to + // the user's chosen shadow budget -- not to total frame time -- so + // this is "are we honouring your settings?" not "are your settings + // right for your hardware?". The latter genuinely needs data we + // don't own (frame target, GPU headroom, async overlap). + const float budgetMs = s_autoBudgetMs; // active budget (Manual = slider, Formula = computed) + const float costMs = avgCost / 1000.0f; + const float usedMs = avgRedraws * costMs; + const int32_t cap = s_settings.MaxRedrawPerFrame; + const bool capLimited = avgCost > 0 && avgRedraws >= static_cast(cap) * 0.95f; + const bool slotLimited = (s_shadowSlotUsage + 0u) >= GetInstalledSlotCount(); + const bool overBudget = avgCost > 0 && budgetMs > 0.0f && usedMs > budgetMs * 1.0f; + const bool headroom = avgCost > 0 && budgetMs > 0.0f && usedMs < budgetMs * 0.5f && !capLimited; + + if (avgCost <= 0 || budgetMs <= 0.0f) { + ImGui::TextDisabled("Budget usage : (warming up)"); + return; + } + + // Verdicts named after the user-visible settings, not internal + // engineering terms. Tooltips kept to one short line each so the + // hover doesn't grow into a wall of text. + ImVec4 col; + const char* verdict; + const char* tip; + if (overBudget) { + col = ImVec4(0.95f, 0.35f, 0.35f, 1); + verdict = "OVER BUDGET"; + tip = "Shadow time exceeds Redraw Budget. Lower Max Redraws or raise Redraw Budget."; + } else if (capLimited && slotLimited) { + col = ImVec4(0.95f, 0.65f, 0.25f, 1); + verdict = "AT LIMITS"; + tip = "Both Max Redraws and Shadow Light Count are full. Enable Convert to Normal or raise Shadow Light Count."; + } else if (slotLimited) { + col = ImVec4(0.95f, 0.65f, 0.25f, 1); + verdict = "LIGHT LIMITED"; + tip = "Shadow Light Count is full. Enable Convert to Normal or raise Shadow Light Count."; + } else if (capLimited) { + col = ImVec4(0.95f, 0.85f, 0.25f, 1); + verdict = "REDRAW LIMITED"; + tip = "Hitting Max Redraws Per Frame. Raise it to spend the unused Redraw Budget."; + } else if (headroom) { + col = ImVec4(0.55f, 0.85f, 0.55f, 1); + verdict = "HEADROOM"; + tip = "Under half the Redraw Budget is being used. Raise Max Redraws or accept the slack."; + } else { + col = ImVec4(0.55f, 0.85f, 0.55f, 1); + verdict = "OK"; + tip = "Within Redraw Budget; no limits hit."; + } + // Budget gauge: progress bar tinted by the verdict colour so the + // state is readable at a glance, with the numeric reading and + // verdict label inside the bar. One widget replaces the old + // separate progress bar (in SCM settings) + verdict text line. + const float fraction = std::min(usedMs / budgetMs, 1.0f); + char overlay[80]; + snprintf(overlay, sizeof(overlay), "%.2f / %.2f ms - %s", usedMs, budgetMs, verdict); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, col); + ImGui::Text("Budget usage :"); + ImGui::SameLine(); + ImGui::ProgressBar(fraction, ImVec2(-1.0f, 0.0f), overlay); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", tip); + + // ---- Shadow VRAM progress bar ---- + // Bar fills `currentUsage / budget` (process headroom); overlay text + // shows the kSHADOWMAPS array's share of that. Same DXGI data source + // as PerformanceOverlay. + auto vinfo = GetVRAMInfo(); + if (vinfo.valid && vinfo.budgetBytes > 0) { + const std::uint64_t freeBytes = vinfo.budgetBytes > vinfo.currentUsageBytes ? vinfo.budgetBytes - vinfo.currentUsageBytes : 0; + const float arrayMB = static_cast(vinfo.shadowArrayBytes) / (1024.f * 1024.f); + const float freeMB = static_cast(freeBytes) / (1024.f * 1024.f); + const float usageMB = static_cast(vinfo.currentUsageBytes) / (1024.f * 1024.f); + const float budgetMBf = static_cast(vinfo.budgetBytes) / (1024.f * 1024.f); + const float perSliceMB = static_cast(vinfo.bytesPerSlice) / (1024.f * 1024.f); + // Disambiguated from the budget-verdict string above. + const VRAMVerdict vramVerdict = EvaluateVRAMVerdict(vinfo.shadowArrayBytes, freeBytes, vinfo.budgetBytes); + const float fillFraction = std::min(1.0f, + static_cast(vinfo.currentUsageBytes) / static_cast(vinfo.budgetBytes)); + char overlayText[96]; + snprintf(overlayText, sizeof(overlayText), + "%.0f / %.0f MB - shadows %.0f MB (%u slices)", + usageMB, budgetMBf, arrayMB, vinfo.shadowSlices); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, vramVerdict.colour); + ImGui::Text("Shadow VRAM :"); + ImGui::SameLine(); + ImGui::ProgressBar(fillFraction, ImVec2(-1.0f, 0.0f), overlayText); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Bar fill = process VRAM usage / DXGI budget (same data the\n" + "performance overlay reports). Overlay text shows the shadow\n" + "array's contribution to that usage.\n" + "\n" + "Slices : %u (sun lives in its own kSHADOWMAPS_ESRAM texture)\n" + "Per slice : %.2f MB (%u x %u @ %u B/pixel)\n" + "Shadow array : %.1f MB\n" + "Free in budget : %.1f MB\n" + "\n" + "Green when free VRAM and shadow share are comfortable.\n" + "Yellow when free < 512 MB or shadow array > 25%% of budget.\n" + "Red when free < 128 MB or shadow array > 50%% of budget --\n" + "lower Shadow Light Count or iShadowMapResolution.", + vinfo.shadowSlices, perSliceMB, + vinfo.shadowWidth, vinfo.shadowHeight, + vinfo.shadowWidth && vinfo.shadowHeight ? vinfo.bytesPerSlice / (vinfo.shadowWidth * vinfo.shadowHeight) : 0u, + arrayMB, freeMB); + } + } + } + + void DrawOverlayShadowModeInfo(uint32_t mode, uint32_t /*shadowUnshadowedLightCount*/, uint32_t /*totalLightCount*/) + { + // Cluster light count, slot usage, requested/dropped/converted are all + // covered by DrawShadowSummary above this in the overlay header. This + // function now carries only mode-specific information that wouldn't be + // meaningful elsewhere -- channel meanings, heatmap legends, etc. + if (mode == 3) { + ImGui::Text("R channel = directional soft shadow"); + ImGui::Text("G channel = directional detailed shadow"); + ImGui::TextDisabled("(B = unused)"); + } else if (mode == 4) { + ImGui::TextDisabled("Pixel heatmap: 0=blue 8+=red"); + } else if (mode == 5) { + ImGui::TextDisabled("White = fully lit, black = fully in shadow"); + } else if (mode == 6) { + ImGui::TextDisabled("Pixel heatmap: 0=blue 8+=red (lights without shadow maps)"); + } else if (mode == 7) { + ImGui::TextDisabled("Cool Turbo[0.0-0.3] = 1-4 shadows"); + ImGui::TextDisabled("Warm Turbo[0.3-0.8] = 5-%u shadows", GetInstalledSlotCount()); + ImGui::TextDisabled("Red = overflow"); + } else if (mode == 9) { + uint32_t spotC = 0, hemiC = 0, omniC = 0; + for (const auto& info : GetSlotInfos()) { + if (!info.valid) + continue; + if (info.type == 0) + spotC++; + else if (info.type == 1) + hemiC++; + else + omniC++; + } + ImGui::Text("R Spot (frustum) : %u", spotC); + ImGui::Text("G Hemisphere : %u", hemiC); + ImGui::Text("B Omni (paraboloid): %u", omniC); + } + } + + void DrawVisualisationTooltipShadowModes() + { + ImGui::Text( + "\n" + "Shadow Mask: R=directional soft shadow, G=directional detailed shadow.\n" + "\n" + "Shadow Light Count: Heatmap of shadow-casting point/spot lights per pixel (blue=0, red=8+).\n" + "Use to gauge shadow density; high counts indicate expensive shadow sampling.\n" + "\n" + "Point Light Shadow Factor: Brightness shows the darkest shadow value from any point/spot\n" + "light. White=fully lit, black=fully shadowed. Shows where PCF/PCSS filtering is active.\n" + "\n" + "Unshadowed Point Lights: Heatmap of point/spot lights without shadow maps (blue=0, red=8+).\n" + "High values where lights are bright indicate where the shadow slot limit is costing quality.\n" + "\n" + "Shadow Caster Density: Custom Turbo ranges show how heavily shadow slots are used.\n" + " Cool (Turbo 0.0-0.3): 1-4 shadow lights per pixel.\n" + " Warm (Turbo 0.3-0.8): 5 to ShadowMapSlots lights (dynamic range).\n" + " Bright red: overflow - a light wanted a shadow slot but none was available.\n" + "\n" + "Shadow Slot Index Color: Assigns each shadow-map slot a unique high-contrast hue\n" + "(golden-ratio sequence) so you can identify which slot is casting the primary shadow.\n" + "First valid shadow light index per pixel is shown. Bright red = slot overflow.\n" + "\n" + "Light Type Visualization: RGB channels encode shadow light types per pixel.\n" + " R = spot/frustum lights (ShadowParam.x == 0).\n" + " G = hemisphere/paraboloid lights (ShadowParam.x == 1).\n" + " B = omnidirectional/full-paraboloid lights (ShadowParam.x == 2).\n" + " Dark grey = unshadowed lights only (no shadow maps assigned).\n" + " Bright red = overflow (slot capacity exceeded).\n" + "Intensity scales with count (up to 4); channels blend for mixed-type pixels."); + } + + void DrawSettings(Settings& settings) + { + ImGui::SeparatorText("Shadow Limit Fix"); + + // ---- External conflict banner -------------------------------------- + if (s_externalConflict) { + const auto& theme = Menu::GetSingleton()->GetTheme(); + ImGui::TextColored(theme.StatusPalette.Error, "%s", s_conflictMessage.c_str()); + ImGui::BeginDisabled(); + } + + // ---- Enable toggle (requires restart) ------------------------------ + ImGui::Checkbox("Enable Shadow Limit Fix", &settings.Enabled); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Extends Skyrim's hard limit of 4 simultaneous shadow-casting lights.\n" + "Intelligently selects which lights cast shadows each frame based on\n" + "distance, intensity, and a configurable priority formula.\n\n" + "Based on Intellightent by meh321.\n" + "https://www.nexusmods.com/skyrimspecialedition/mods/172423\n\n" + "Restart required to take effect in either direction. The boot-time\n" + "patches (extended atlas slices, depth buffer creation loop, color-mask\n" + "pass replacement) cannot be safely reversed at runtime -- vanilla\n" + "shadow scheduling crashes when run on top of them. Toggle and restart."); + // Either direction requires restart -- the boot-time patches modify + // the engine's shadow texture array, depth buffer creation, and + // color-mask pass. Vanilla scheduling cannot run on top of those + // (verified by AV in BSShadowDirectionalLight processing during a + // runtime-disable test, 2026-05-17 crash logs). + // + // Compare the user's current value against the BOOT value, not + // against s_settings -- s_settings updates when the user saves, + // so a stale comparison against s_settings would hide the label + // the instant the user clicked Save Settings, leaving them with + // no indication that their change won't apply until restart. + if (s_bootEnabledCaptured && settings.Enabled != s_bootEnabled) { + const auto& theme = Menu::GetSingleton()->GetTheme(); + ImGui::TextColored(theme.StatusPalette.RestartNeeded, + "Restart required -- this session is %s.", s_bootEnabled ? "enabled" : "disabled"); + } + + if (!settings.Enabled) + ImGui::BeginDisabled(); + + // ---- Shadow Light Count (requires restart) ------------------------- + // Upper bound of 127: the engine refuses to render any shadow caster + // when ShadowLightCount >= 128 even though kSHADOWMAPS allocates + // successfully -- some internal limit (likely an 8-bit shadow index + // somewhere we haven't patched) silently disables shadow rendering. + // 127 is the highest value that actually works. + ImGui::SliderInt("Shadow Light Count", &settings.ShadowLightCount, 0, 127); + // Compute projected VRAM for the slider's current value so the user + // can see the cost of a higher count *before* committing the restart. + // kSHADOWMAPS holds exactly ShadowLightCount slices -- the sun lives + // in its own kSHADOWMAPS_ESRAM texture, so there's no +1. + auto sliderVram = GetVRAMInfo(); + std::uint64_t projectedBytes = 0; + std::uint64_t projectedFreeBytes = 0; + bool projectionValid = sliderVram.valid; + if (projectionValid) { + projectedBytes = ProjectShadowArrayBytes(static_cast(settings.ShadowLightCount)); + std::int64_t projectedUsage = static_cast(sliderVram.currentUsageBytes) - + static_cast(sliderVram.shadowArrayBytes) + + static_cast(projectedBytes); + if (projectedUsage < 0) + projectedUsage = 0; + projectedFreeBytes = (static_cast(sliderVram.budgetBytes) > projectedUsage) ? static_cast(sliderVram.budgetBytes - projectedUsage) : 0; + } + if (ImGui::IsItemHovered()) { + constexpr const char* kSliderBase = + "Maximum simultaneous shadow-casting point/spot lights (directional sun not counted).\n" + " 0 = scheduler runs but selects no point lights (sun/directional unaffected).\n" + " 4 = vanilla point light count with intelligent selection.\n" + " >4 = extended mode; depth buffer expanded when >8. Max 127\n" + " (VRAM is the practical limit -- watch the projected-VRAM bar).\n" + "Requires a game restart to take effect."; + if (projectionValid) { + ImGui::SetTooltip( + "%s\n" + "\n" + "Projected kSHADOWMAPS array at %d slots: %.1f MB\n" + "Per-slice cost: %.2f MB (%u x %u, %u B/pixel)\n" + "Projected free VRAM after restart: %.1f MB", + kSliderBase, + settings.ShadowLightCount, + static_cast(projectedBytes) / (1024.f * 1024.f), + static_cast(sliderVram.bytesPerSlice) / (1024.f * 1024.f), + sliderVram.shadowWidth, sliderVram.shadowHeight, + sliderVram.shadowWidth && sliderVram.shadowHeight ? + sliderVram.bytesPerSlice / (sliderVram.shadowWidth * sliderVram.shadowHeight) : + 0u, + static_cast(projectedFreeBytes) / (1024.f * 1024.f)); + } else { + ImGui::SetTooltip("%s", kSliderBase); + } + } + // Custom-drawn stacked bar against DXGI budget showing non-shadow / + // current-shadow / projected-shadow segments. ImGui::ProgressBar + // can't multi-segment. + if (projectionValid && sliderVram.budgetBytes > 0) { + const VRAMVerdict verdict = EvaluateVRAMVerdict(projectedBytes, projectedFreeBytes, sliderVram.budgetBytes); + const float budgetMBf = static_cast(sliderVram.budgetBytes) / (1024.f * 1024.f); + const float nonShadowMB = std::max(0.0f, + (static_cast(sliderVram.currentUsageBytes) - static_cast(sliderVram.shadowArrayBytes)) / (1024.f * 1024.f)); + const float currentShadowMB = static_cast(sliderVram.shadowArrayBytes) / (1024.f * 1024.f); + const float projectedShadowMB = static_cast(projectedBytes) / (1024.f * 1024.f); + + ImGui::Text("Projected shadow VRAM :"); + ImGui::SameLine(); + const ImVec2 cursor = ImGui::GetCursorScreenPos(); + const float fullWidth = ImGui::GetContentRegionAvail().x; + const float barHeight = ImGui::GetFrameHeight(); + const float scale = fullWidth / budgetMBf; + auto* draw = ImGui::GetWindowDrawList(); + // Background frame, then non-shadow / current / projected segments. + draw->AddRectFilled(cursor, ImVec2(cursor.x + fullWidth, cursor.y + barHeight), + ImGui::GetColorU32(ImGuiCol_FrameBg)); + const float nonShadowEndX = cursor.x + nonShadowMB * scale; + draw->AddRectFilled(cursor, ImVec2(nonShadowEndX, cursor.y + barHeight), + IM_COL32(120, 120, 120, 200)); + const float currentEndX = std::min(cursor.x + fullWidth, nonShadowEndX + currentShadowMB * scale); + draw->AddRectFilled(ImVec2(nonShadowEndX, cursor.y), + ImVec2(currentEndX, cursor.y + barHeight), + IM_COL32(80, 130, 200, 220)); + // Projection outline anchored at the same start as current, so + // the visual delta IS the difference. Solid fill for grow, dark + // stripe for shrink. + const float projectedEndX = std::min(cursor.x + fullWidth, nonShadowEndX + projectedShadowMB * scale); + const ImU32 verdictColU32 = ImGui::GetColorU32(verdict.colour); + draw->AddRect(ImVec2(nonShadowEndX, cursor.y), ImVec2(projectedEndX, cursor.y + barHeight), + verdictColU32, 0.0f, 0, 2.0f); + if (projectedShadowMB > currentShadowMB) { + draw->AddRectFilled(ImVec2(currentEndX, cursor.y), ImVec2(projectedEndX, cursor.y + barHeight), + (verdictColU32 & 0x00FFFFFFu) | 0xA0000000u); + } else if (projectedShadowMB < currentShadowMB) { + draw->AddRectFilled(ImVec2(projectedEndX, cursor.y), ImVec2(currentEndX, cursor.y + barHeight), + IM_COL32(80, 80, 80, 120)); + } + + char overlay[128]; + snprintf(overlay, sizeof(overlay), + "shadows %.0f -> %.0f MB (%d slots, %.0f MB free after restart)", + currentShadowMB, projectedShadowMB, + settings.ShadowLightCount, + static_cast(projectedFreeBytes) / (1024.f * 1024.f)); + const ImVec2 textSize = ImGui::CalcTextSize(overlay); + const ImVec2 textPos(cursor.x + (fullWidth - textSize.x) * 0.5f, + cursor.y + (barHeight - textSize.y) * 0.5f); + draw->AddText(textPos, IM_COL32(240, 240, 240, 255), overlay); + ImGui::Dummy(ImVec2(fullWidth, barHeight)); // reserve layout space + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Stacked VRAM bar against DXGI budget.\n" + " Grey block : process VRAM not counted as shadow array\n" + " Blue block : current kSHADOWMAPS allocation this session\n" + " Outlined block: what the slider's value would allocate\n" + " after restart (colour reflects verdict)\n" + "\n" + "Solid colour past the blue: shadow array would GROW by that\n" + "amount. Dark stripe inside the blue: shadow array would\n" + "SHRINK by that amount.\n" + "\n" + "Slots requested : %d (sun lives in kSHADOWMAPS_ESRAM)\n" + "Per-slice cost : %.2f MB (%u x %u @ %u B/pixel)\n" + "Current array : %.1f MB\n" + "Projected array : %.1f MB\n" + "Free after restart : %.1f MB / %.0f MB budget\n" + "%s", + settings.ShadowLightCount, + static_cast(sliderVram.bytesPerSlice) / (1024.f * 1024.f), + sliderVram.shadowWidth, sliderVram.shadowHeight, + sliderVram.shadowWidth && sliderVram.shadowHeight ? + sliderVram.bytesPerSlice / (sliderVram.shadowWidth * sliderVram.shadowHeight) : + 0u, + currentShadowMB, + projectedShadowMB, + static_cast(projectedFreeBytes) / (1024.f * 1024.f), + budgetMBf, + verdict.over ? + "\nRED: this projection won't fit in the current VRAM budget.\n" + "The driver will page or refuse the allocation, leaving the\n" + "shadow array smaller than requested -- shadows will silently\n" + "break. Lower the slot count or reduce iShadowMapResolution." : + verdict.tight ? + "\nYELLOW: tight headroom. A driver or OS spike could push\n" + "shadow allocation into paging. Safe for testing, risky for\n" + "long sessions or heavily-modded scenes." : + ""); + } + } + + // ---- Allocation mismatch banner ---- + // Surface kSHADOWMAPS truncation visibly so users hit by a silent + // "shadows don't work at high slot counts" failure can see why. + // Reads the verified count directly (not the GetInstalledSlotCount + // accessor, which falls back to the requested value). + { + uint32_t installed = s_installedSlotCount; + uint32_t requested = s_requestedSlotCount; + if (installed > 0 && requested > 0 && installed < requested) { + ImGui::TextColored(ImVec4(0.95f, 0.35f, 0.35f, 1), + "VRAM exhausted: requested %u slots, GPU allocated %u.", + requested, installed); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "The engine tried to create kSHADOWMAPS with %u slices but\n" + "the GPU / driver returned a smaller array (likely out of\n" + "VRAM at the configured iShadowMapResolution). The scheduler\n" + "has clamped itself to the actual count so the existing %u\n" + "slices work correctly, but to reach the requested %u you'll\n" + "need to free VRAM (lower resolution, other features, etc).", + requested, installed, requested); + } else if (installed == 0 && s_settings.Enabled && !s_externalConflict) { + ImGui::TextColored(ImVec4(0.95f, 0.85f, 0.25f, 1), + "Shadow array not yet verified -- load a save to confirm allocation."); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "kSHADOWMAPS isn't readable yet (main menu / loading screen).\n" + "Once you reach gameplay the scheduler verifies the actual\n" + "slice count against your requested value. If they disagree\n" + "this banner turns red."); + } + } + + if (settings.ShadowLightCount != s_installedShadowLightCount) { + const auto& theme = Menu::GetSingleton()->GetTheme(); + ImGui::TextColored(theme.StatusPalette.RestartNeeded, + "Restart required -- current session uses %d lights.", s_installedShadowLightCount); + } + + // ---- Shadow Map Resolution (requires restart) --------------------- + // Mirrors the launcher's resolution tiers (the four power-of-two values + // Skyrim itself offers). Mutates the live iShadowMapResolution:Display + // RE::Setting immediately; persistence to SkyrimPrefs.ini happens in + // SCM::SaveINISettings (called from LightLimitFix::SaveSettings). + if (auto* prefColl = RE::INIPrefSettingCollection::GetSingleton()) { + if (auto* setting = prefColl->GetSetting("iShadowMapResolution:Display")) { + static constexpr struct + { + const char* label; + std::int32_t value; + } kResTiers[] = { + { "Low (1024)", 1024 }, + { "Medium (2048)", 2048 }, + { "High (4096)", 4096 }, + { "Ultra (8192)", 8192 }, + }; + constexpr int kTierCount = static_cast(sizeof(kResTiers) / sizeof(kResTiers[0])); + + const std::int32_t currentRes = setting->GetInteger(); + int tierIdx = -1; + for (int i = 0; i < kTierCount; ++i) { + if (kResTiers[i].value == currentRes) { + tierIdx = i; + break; + } + } + // Non-tier values (manual INI edits / third-party tools) + // surface as "Custom (N)" so the user sees what the engine is + // actually using, but we don't offer it as a selectable tier. + char previewBuf[32]; + const char* preview; + if (tierIdx >= 0) { + preview = kResTiers[tierIdx].label; + } else { + snprintf(previewBuf, sizeof(previewBuf), "Custom (%d)", currentRes); + preview = previewBuf; + } + + if (ImGui::BeginCombo("Shadow Map Resolution", preview)) { + for (int i = 0; i < kTierCount; ++i) { + const bool selected = (i == tierIdx); + if (ImGui::Selectable(kResTiers[i].label, selected) && + kResTiers[i].value != currentRes) { + setting->SetInteger(kResTiers[i].value); + s_shadowResolutionDirty = true; + } + if (selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Drives iShadowMapResolution:Display in SkyrimPrefs.ini.\n" + "Affects both omni/spot shadow slices and the sun cascade\n" + "texture; per-slice VRAM scales as resolution^2 * 4 bytes\n" + "(4 / 16 / 64 / 256 MB at 1024 / 2048 / 4096 / 8192).\n" + "Requires a game restart to take effect."); + } + + if (s_initialShadowMapResolution > 0 && currentRes != s_initialShadowMapResolution) { + const auto& theme = Menu::GetSingleton()->GetTheme(); + ImGui::TextColored(theme.StatusPalette.RestartNeeded, + "Restart required -- current session uses %d px shadow maps.", + s_initialShadowMapResolution); + } + } + } + + // ---- Temporal budget (dynamic) ------------------------------------ + + // Migrate legacy Auto saves silently. Manual is now the default and the + // closest match in spirit to what most users actually wanted from Auto: + // a predictable budget that doesn't ping-pong. Power users can switch + // back to Formula manually if they want the adaptive default expression. + if (settings.BudgetMode == BudgetModeEnum::Auto) + settings.BudgetMode = BudgetModeEnum::Manual; + + // Budget mode selector — Manual or Formula. Auto was removed: it was an + // opaque DRS controller that confused users when the budget moved without + // a visible cause. The default Formula expresses the same behaviour + // transparently and stays editable. + static const char* budgetModeNames[] = { "Manual", "Formula" }; + int budgetModeIdx = (settings.BudgetMode == BudgetModeEnum::Manual) ? 0 : 1; + if (ImGui::Combo("Budget Mode", &budgetModeIdx, budgetModeNames, 2)) + settings.BudgetMode = (budgetModeIdx == 0) ? BudgetModeEnum::Manual : BudgetModeEnum::Formula; + if (ImGui::IsItemHovered()) { + if (budgetModeIdx == 0) + ImGui::SetTooltip( + "Manual (default): fixed per-frame GPU time budget for shadow re-renders.\n" + "Predictable; doesn't oscillate. Adjust the slider to trade FPS for shadow quality."); + else + ImGui::SetTooltip( + "Formula: user-editable exprtk expression for per-frame budget.\n" + "Default expression matches Intellightent's original behaviour\n" + "(1 ms outdoors, 2 ms indoors). Edit the expression in the\n" + "Advanced section below.\n" + "\n" + "Caveat: adaptive expressions referencing `frametime` tend to\n" + "ping-pong because rendering shadows raises frametime, removing\n" + "the headroom that allowed the budget. Stick to static or\n" + "slowly-varying inputs (`isinterior`, `frametarget`)."); + } + + // Per-mode controls. + if (budgetModeIdx == 0) { + ImGui::SliderFloat("Redraw Budget (ms)", &settings.RedrawBudgetMs, 0.1f, 32.0f, "%.2f ms"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Per-frame GPU time budget for shadow re-renders (milliseconds).\n" + "Lights whose estimated render cost exceeds the remaining budget are deferred.\n" + "The first eligible light always renders regardless of budget (starvation prevention).\n" + "\n" + "Reference points:\n" + " 1-2 ms: Intellightent's original (1 outdoors, 2 indoors)\n" + " 5 ms : default — comfortable for typical scenes (~5-8 lights at ~1 ms each)\n" + " 16 ms: full 60 fps frame; shadows can saturate the frame here\n" + " 32 ms: extreme — only useful for very high light counts on fast GPUs\n" + "\n" + "Higher = more shadow lights redraw per frame, fewer stale shadow maps,\n" + "at the cost of frametime. The Budget verdict in the Active Casters\n" + "section shows whether the current setting has headroom to spare."); + } else { + ImGui::Text("Budget from formula: %.2f ms", s_autoBudgetMs); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Edit the Redraw Budget formula in the Advanced section below."); + } + + // Budget consumption visualisation lives in the Active Casters block + // (DrawShadowSchedulerStats) alongside the verdict, so the bar, the + // numeric reading and the actionable state appear in one place + // instead of being split between two sections. + + // ---- Frame-target diagnostic (Formula mode only) ------------------ + // `frametarget` is an exprtk variable available to the Redraw Budget + // formula -- in Formula mode the user needs to see what it evaluates + // to in order to write/debug expressions that reference it. In Manual + // mode the user's chosen RedrawBudgetMs has nothing to do with frame + // timing, so this block would just be noise -- the new Budget verdict + // (in the Active Casters block) covers the "headroom / saturated" + // signal more actionably for both modes, and DrawShadowSummary covers + // the rendered/dropped lights count without duplication. + if (settings.BudgetMode == BudgetModeEnum::Formula) { + const float currentFrameMs = *globals::game::deltaTime * 1000.0f; + const float currentFPS = 1000.0f / std::max(currentFrameMs, 1.0f); + const float targetMs = ComputeFrameTimePercentile90(); + const float targetFPS = targetMs > 0.0f ? 1000.0f / targetMs : 0.0f; + const float rawHeadroom = targetMs - s_ftEMA; + const float headroomMs = rawHeadroom - kFrameHeadroomSafetyMs; + + const char* state = "steady"; + if (rawHeadroom > kFrameHeadroomSafetyMs + kFrameHeadroomDeadZoneMs) + state = "growing"; + else if (rawHeadroom < -kFrameHeadroomDeadZoneMs) + state = "throttling"; + + ImGui::Text("Frame: %.1f FPS (%.1f ms) | frametarget: %.0f FPS (%.1f ms) | headroom: %+.1f ms | %s", + currentFPS, currentFrameMs, targetFPS, targetMs, headroomMs, state); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Live values of the exprtk variables exposed to the Redraw\n" + "Budget formula. `frametarget` is the rolling 90th-percentile\n" + "frame time, used as a self-measured ceiling -- not a vsync\n" + "target. State indicator:\n" + " steady -- within +/-%.1f ms of target\n" + " growing -- frametime well below target; headroom available\n" + " throttling -- frametime over target; expressions returning\n" + " nonzero values here will keep frametime high", + kFrameHeadroomDeadZoneMs); + } + { + // Use ShadowLightCount as the slider upper bound when the scheduler hasn't + // run yet (s_totalShadowLightsThisFrame == 0 on the first menu open). + // Never clamp the stored setting here — the scheduling code already applies + // the live cap. Clamping here caused MaxRedrawPerFrame to be permanently + // written to 1 on the first DrawSettings call before the hook fired. + // Track active shadow lights this frame, falling back to the + // configured ShadowLightCount when the scheduler hasn't run yet. + // No artificial 64 cap -- if the user dialled in 128 lights, the + // redraw cap should be allowed to follow. + int maxRedraws = s_totalShadowLightsThisFrame > 0 ? s_totalShadowLightsThisFrame : settings.ShadowLightCount; + maxRedraws = std::max(maxRedraws, Settings::kMinMaxRedrawPerFrame); + ImGui::SliderInt("Max Redraws Per Frame", &settings.MaxRedrawPerFrame, + Settings::kMinMaxRedrawPerFrame, maxRedraws); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Hard cap on how many shadow lights may re-render their shadow maps in one frame.\n" + "Acts as a safety valve regardless of budget -- the budget controls time spent,\n" + "this controls count. The sun directional light always counts as one redraw.\n" + "Minimum is %d (lower values cause shadow flicker as redraw rotation outpaces TAA).\n" + "Upper bound tracks the number of active shadow lights this frame (%d).", + Settings::kMinMaxRedrawPerFrame, maxRedraws); + } + + // ---- Light conversion (requires restart for hooks) ----------------- + if (ImGui::TreeNode("Light Conversion##LightConv")) { + ImGui::Checkbox("Convert Excess Lights to Normal", &settings.ConvertExcessToNormal); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Shadow lights that exceed the active shadow caster limit are demoted to\n" + "normal (unshadowed) lights so they still contribute diffuse and specular\n" + "lighting at no shadow-map cost. Lights that fail culling are dropped entirely.\n" + "Requires a game restart to change."); + + // No texture-array cost -- converted lights flow through the cluster + // pipeline as ordinary non-shadow lights. Match the ShadowLightCount + // max so users can pair a large shadow pool with a matching converted + // pool without the slider lying about the upper bound. + ImGui::SliderInt("Converted Shadow Slots", &settings.ConvertedShadowSlots, 0, 127); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Extra pool slots for lights converted to normal (unshadowed) mode.\n" + "Increase if Convert Excess Lights drops lights you expect to see."); + + ImGui::Checkbox("Promote Normal Lights to Shadow Casters", &settings.PromoteNormalToShadow); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Experimental: elevate high-scoring unshadowed lights to shadow casters\n" + "when shadow slots are available.\n" + "Requires a game restart to change."); + + ImGui::SeparatorText("Portal-Strict Enforcement"); + // Three-way toggle plus master row. SCM forces the engine's + // portal-strict flag on shadow casters at creation time, gated + // per shadow type (FOV-derived). Defaults enforce on omni and + // hemisphere, leave spotlights alone -- portal-strict on spots + // drops culled-but-visible spots entirely (cone test rejects + // spots whose origin is behind a portal even when the beam + // sweeps into a visible room). + { + const bool allOn = settings.ForceEnablePortalStrictOmni && + settings.ForceEnablePortalStrictHemi && + settings.ForceEnablePortalStrictSpot; + const bool allOff = !settings.ForceEnablePortalStrictOmni && + !settings.ForceEnablePortalStrictHemi && + !settings.ForceEnablePortalStrictSpot; + bool master = allOn; + bool indeterminate = !allOn && !allOff; + if (indeterminate) { + // Render the master checkbox as visually mixed via a + // muted alpha so the row still functions as a "set all" + // control without misrepresenting state. + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.6f); + } + if (ImGui::Checkbox("Force Enable Portal Strict (All)", &master)) { + settings.ForceEnablePortalStrictOmni = master; + settings.ForceEnablePortalStrictHemi = master; + settings.ForceEnablePortalStrictSpot = master; + } + if (indeterminate) + ImGui::PopStyleVar(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Master toggle for the three per-type rows below.\n" + "Checked when all three are enforced, unchecked when none are,\n" + "and rendered translucent when mixed.\n" + "Requires a game restart to change."); + } + + ImGui::Indent(); + ImGui::Checkbox("Force Portal Strict on Omni Lights", &settings.ForceEnablePortalStrictOmni); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Force-enable portal-strict on dual-paraboloid (omnidirectional)\n" + "shadow casters. Recommended on -- tightens portal-graph visibility\n" + "culling for full-sphere shadow lights without side effects.\n" + "Requires a game restart to change."); + ImGui::Checkbox("Force Portal Strict on Hemisphere Lights", &settings.ForceEnablePortalStrictHemi); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Force-enable portal-strict on single-paraboloid (hemisphere)\n" + "shadow casters. Recommended on -- behaves like the omni case\n" + "under portal culling.\n" + "Requires a game restart to change."); + ImGui::Checkbox("Force Portal Strict on Spot Lights", &settings.ForceEnablePortalStrictSpot); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Force-enable portal-strict on perspective (frustum/spot) shadow\n" + "casters. Off by default: the cone test rejects spots whose\n" + "origin sits behind a portal even when their beam sweeps into a\n" + "visible room, which drops culled-but-visible spots entirely.\n" + "Enable only for debugging.\n" + "Requires a game restart to change."); + ImGui::Unindent(); + + ImGui::TreePop(); + } + + // ---- Advanced (dynamic) ------------------------------------------- + if (ImGui::TreeNode("Advanced##ShadowScheduling")) { + ImGui::Checkbox("Allow Immediate Draw for New Lights", &settings.AllowDrawNewLight); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Allow a light just added to the active pool to render its shadow map this frame.\n" + "Prevents a one-frame shadow-map gap when new lights enter view."); + + // ---- Importance scheduling curve ------------------------------ + ImGui::SeparatorText("Importance Scheduling"); + ImGui::SliderFloat("Max Interval Scale", &settings.ImportanceMaxScale, 0.5f, 5.0f, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Interval multiplier applied to unimportant lights (importance = 0).\n" + "Higher values defer dim or distant lights more aggressively.\n" + "Default: 2.0"); + settings.ImportanceMaxScale = std::max(settings.ImportanceMaxScale, settings.ImportanceMinScale); + + ImGui::SliderFloat("Min Interval Scale", &settings.ImportanceMinScale, 0.01f, 1.0f, "%.3f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Interval multiplier applied to high-importance lights (importance >= 1).\n" + "Lower values make bright/close lights update shadows more frequently.\n" + "The ratio Max/Min defines the scheduling dynamic range.\n" + "Default: 0.05 (40x range at default Max=2.0)"); + settings.ImportanceMinScale = std::min(settings.ImportanceMinScale, settings.ImportanceMaxScale); + + { + float ratio = settings.ImportanceMaxScale / std::max(settings.ImportanceMinScale, 0.001f); + ImGui::Text("Dynamic range: %.0fx (unimportant lights wait %.0fx longer)", ratio, ratio); + } + + if (ImGui::Button("Reset Importance Defaults")) { + settings.ImportanceMinScale = 0.05f; + settings.ImportanceMaxScale = 2.0f; + } + + // ---- Formula editor ------------------------------------------ + if (ImGui::TreeNode("Formula Editor##Formulas")) { + // Build variable reference from the DRY table. + if (ImGui::TreeNode("Available Variables##FormulaVars")) { + if (ImGui::BeginTable("##FormulaVarTable", 2, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY, + ImVec2(0, std::min(static_cast(IM_ARRAYSIZE(kFormulaVars)) * 20.0f + 28.0f, 320.0f)))) { + ImGui::TableSetupColumn("Variable"); + ImGui::TableSetupColumn("Description"); + ImGui::TableHeadersRow(); + for (const auto& v : kFormulaVars) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(v.name); + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(v.description); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + static char scoreBuf[512]; + static char scoreErr[256] = {}; + static char redrawIntervalBuf[512]; + static char redrawIntervalErr[256] = {}; + static char redrawBudgetBuf[512]; + static char redrawBudgetErr[256] = {}; + static bool formulaBufsInited = false; + if (!formulaBufsInited) { + snprintf(scoreBuf, sizeof(scoreBuf), "%s", settings.ScoreFormula.c_str()); + snprintf(redrawIntervalBuf, sizeof(redrawIntervalBuf), "%s", settings.RedrawIntervalFormula.c_str()); + snprintf(redrawBudgetBuf, sizeof(redrawBudgetBuf), "%s", settings.RedrawBudgetFormula.c_str()); + formulaBufsInited = true; + } + + // Helper lambda: validate, apply live, revert buffer on error. + auto applyFormula = [](const char* label, char* buf, size_t bufSize, + std::string& settingStr, char* errBuf, size_t errBufSize, + std::unique_ptr& helper) { + ImGui::InputText(label, buf, bufSize); + if (ImGui::IsItemDeactivatedAfterEdit()) { + std::string err; + if (FormulaHelper::Validate(buf, err)) { + settingStr = buf; + errBuf[0] = '\0'; + if (helper) + helper->Reparse(settingStr); + else { + helper = std::make_unique(); + helper->Parse(settingStr); + } + } else { + snprintf(errBuf, errBufSize, "Parse error: %s", err.c_str()); + snprintf(buf, bufSize, "%s", settingStr.c_str()); + } + } + if (errBuf[0]) + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", errBuf); + }; + + applyFormula("Score", scoreBuf, sizeof(scoreBuf), + settings.ScoreFormula, scoreErr, sizeof(scoreErr), s_formulaScore); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Light priority scoring formula. Higher score = more likely to get a shadow slot."); + + applyFormula("Redraw Interval", redrawIntervalBuf, sizeof(redrawIntervalBuf), + settings.RedrawIntervalFormula, redrawIntervalErr, sizeof(redrawIntervalErr), s_formulaRedrawInterval); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Per-light redraw interval formula. Higher = less frequent shadow map updates."); + applyFormula("Redraw Budget", redrawBudgetBuf, sizeof(redrawBudgetBuf), + settings.RedrawBudgetFormula, redrawBudgetErr, sizeof(redrawBudgetErr), s_formulaRedrawBudget); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Per-frame redraw budget formula (ms). Empty = use the Redraw Budget (ms) slider value."); + + ImGui::TreePop(); + } + + ImGui::TreePop(); + } + + // Active casters table + scheduler stats are rendered by LightLimitFix + // alongside its own quick-stats line, so the table area has full + // testing context (cluster light count, shadow slot usage, etc.) in + // one place. See LightLimitFix::DrawSettings. + + if (!settings.Enabled) + ImGui::EndDisabled(); + + if (s_externalConflict) + ImGui::EndDisabled(); + } +} diff --git a/src/Features/LightLimitFix/ShadowCasterManager.h b/src/Features/LightLimitFix/ShadowCasterManager.h new file mode 100644 index 0000000000..6230b1c870 --- /dev/null +++ b/src/Features/LightLimitFix/ShadowCasterManager.h @@ -0,0 +1,666 @@ +// ShadowCasterManager.h +// Shadow caster scheduling for LightLimitFix. +// +// Based on Intellightent by meh321 +// https://www.nexusmods.com/skyrimspecialedition/mods/172423 +// +// Ported and adapted for Community Shaders by the Community Shaders team with permission. +// +// The original plugin managed shadow caster selection, temporal shadow +// update scheduling, and depth buffer extension entirely outside Community +// Shaders. This file houses the CPU-side shadow scheduling subsystem so it +// can live alongside (and share settings with) LightLimitFix's GPU-side +// clustered light culling without coupling the two concerns inside a single +// translation unit. + +#pragma once + +#include +#include + +#include "RE/B/BSShadowLight.h" +#include "RE/S/ShadowSceneNode.h" + +struct ImVec4; + +namespace ShadowCasterManager +{ + // Type-based shadow-caster check that bypasses the IsShadowLight vtable + // hook (which flips to false for ConvertExcessToNormal-demoted lights). + // Use this when callers need the intrinsic type, not the current shadow + // state -- e.g. InverseSquareLighting's cutoff selection, where the + // radius shouldn't oscillate as a light flips in and out of conversion. + // Cost: one vtable-pointer compare. + inline bool IsShadowLightType(RE::BSLight* bsLight) + { + return skyrim_cast(bsLight) != nullptr; + } + + // shadowLightsAccum iterator. shadowLightsAccum is a flat slot array + // where a dual-paraboloid light occupies shadowMapCount==2 consecutive + // physical slots (second is null). ForEachShadowLight advances by + // shadowMapCount so each logical light is visited once. + // + // WARNING: _size is never updated -- do not push_back or use range-for / + // BSTArray iterators on this directly. + /// Conservative upper bound on shadowLightsAccum iteration index, derived + /// from the active scheduler settings (ShadowLightCount + sun cascades). + /// Used by ForEachShadowLight as a setting-aware safety cap so corrupt + /// / non-null-terminated arrays can't loop forever, while still allowing + /// iteration past BSTArray's static _capacity. + std::uint32_t MaxShadowAccumIterationBound(); + + /// kSHADOWMAPS texture-array slot count the engine actually allocated. + /// 0 until the SRV becomes readable; the read is lazy and self-healing + /// across frames so callers can use it without timing constraints. + /// Consumers in the cluster pipeline, scheduler, and UI should call + /// this rather than reaching into Deferred / the renderer directly. + std::uint32_t GetInstalledSlotCount(); + + /// Live VRAM telemetry used for shadow-array sizing decisions and stats. + /// All values in bytes; populated from IDXGIAdapter3::QueryVideoMemoryInfo + /// + the kSHADOWMAPS texture's actual geometry. valid=false when the + /// adapter/texture aren't ready yet (e.g. before SetupResources). + struct VRAMInfo + { + std::uint64_t currentUsageBytes = 0; ///< VRAM currently allocated to this process (local heap) + std::uint64_t budgetBytes = 0; ///< Driver-suggested budget for this process + std::uint64_t shadowArrayBytes = 0; ///< Bytes currently used by the kSHADOWMAPS texture array + std::uint32_t shadowWidth = 0; ///< Per-slice width + std::uint32_t shadowHeight = 0; ///< Per-slice height + std::uint32_t shadowSlices = 0; ///< Current kSHADOWMAPS ArraySize + std::uint32_t bytesPerSlice = 0; ///< Per-slice byte cost (width*height*format size) + bool valid = false; + }; + VRAMInfo GetVRAMInfo(); + + /// Predict the kSHADOWMAPS texture-array byte size for a given slice count + /// using the current per-slice geometry. Returns 0 if VRAMInfo isn't valid yet. + std::uint64_t ProjectShadowArrayBytes(std::uint32_t sliceCount); + + template + inline void ForEachShadowLight(const RE::BSTArray& accum, Fn&& fn) + { + // Engine writes via SetShadowCasterLightArrayEntry which bypasses + // BSTArray::push_back, so _capacity stays at the initial preallocation + // -- using capacity as the bound silently caps SLF at vanilla shadow + // counts. Use the null sentinel instead, with a setting-derived + // safety cap (ShadowLightCount + sun cascades, with a small margin). + // Per-pointer plausibility (alignment + user-mode range) handles + // non-null garbage between our prepass and this read. + const std::uint32_t maxIdx = MaxShadowAccumIterationBound(); + std::uint32_t idx = 0; + while (idx < maxIdx) { + RE::BSShadowLight* light = accum[idx]; + if (!light) + break; + const auto raw = reinterpret_cast(light); + if (raw >= 0x0000800000000000ull || (raw & 0x7) != 0) + break; + fn(light); + const std::uint32_t step = light->shadowMapCount; + if (step == 0) + break; + const std::uint64_t next = static_cast(idx) + step; + if (next >= maxIdx) + break; + idx = static_cast(next); + } + } + + // ------------------------------------------------------------------------- + // Formula parameter indices + // ------------------------------------------------------------------------- + enum FormulaParams + { + kFormulaParam_LightIndex, + kFormulaParam_LightIntensity, + kFormulaParam_LightDistance, + kFormulaParam_LightRadius, + kFormulaParam_LightX, + kFormulaParam_LightY, + kFormulaParam_LightZ, + kFormulaParam_LightR, + kFormulaParam_LightG, + kFormulaParam_LightB, + kFormulaParam_LightAmbientR, + kFormulaParam_LightAmbientG, + kFormulaParam_LightAmbientB, + kFormulaParam_LightChosenLastFrame, + kFormulaParam_LightFramesSinceRender, ///< frames since this light's slot was last rendered; large sentinel if never rendered or no slot + kFormulaParam_LightNeverFades, + kFormulaParam_LightPortalStrict, + kFormulaParam_LightNS, + kFormulaParam_LightConverted, + kFormulaParam_LightDisplacement, ///< distance moved since last shadow map render (game units) + kFormulaParam_PlayerLightDistance, ///< distance from the player character to the light (game units) + kFormulaParam_LightImportance, ///< contribution importance: lum(diffuse*fade) * max(att_cam, att_plr); set in interval loop only + kFormulaParam_LightIsSpot, ///< 1 if light is a spot (BSShadowFrustumLight), 0 otherwise + kFormulaParam_LightSpotVisible, ///< 1 if a spot's cone is plausibly visible to the camera (cone-aimed-at-frustum). Always 1 for non-spots so omni-only formulas aren't affected. + + kFormulaParam_CameraX, + kFormulaParam_CameraY, + kFormulaParam_CameraZ, + kFormulaParam_IsInterior, + kFormulaParam_TimeOfDay, + + kFormulaParam_FrameTime, ///< EMA-smoothed frame time (ms) + kFormulaParam_FrameTarget, ///< 90th-percentile frame time (ms) — target budget ceiling + kFormulaParam_StableFrames, ///< consecutive frames the EMA has been below FrameTarget + + kFormulaParam_Max + }; + + // ------------------------------------------------------------------------- + // Expression-based formula evaluator (wraps exprtk) + // ------------------------------------------------------------------------- + struct FormulaHelper + { + FormulaHelper(); + ~FormulaHelper(); + + FormulaHelper(const FormulaHelper&) = delete; + FormulaHelper& operator=(const FormulaHelper&) = delete; + FormulaHelper(FormulaHelper&&) = delete; + FormulaHelper& operator=(FormulaHelper&&) = delete; + + bool Parse(const std::string& input); + double Calculate(); + + /// Re-parse with a new expression, replacing any previously compiled formula. + /// Returns true on success. On failure the old formula remains active. + bool Reparse(const std::string& input); + + /// Compile `input` into a temporary expression and return true if it succeeds. + /// On failure, `errorOut` receives the first parser error message. + /// Does NOT affect the active formula. + static bool Validate(const std::string& input, std::string& errorOut); + + static void SetParam(int32_t index, double value); + static double GetParam(int32_t index); + + private: + void* _ptr; + }; + // ------------------------------------------------------------------------- + // ------------------------------------------------------------------------- + // Budget mode enum + // ------------------------------------------------------------------------- + enum class BudgetModeEnum : int32_t + { + Auto = 0, ///< DEPRECATED: kept only for save-file backward compat. Migrated to Formula at load. + Manual = 1, ///< Fixed slider value + Formula = 2, ///< User-editable exprtk expression (default) + }; + + // ------------------------------------------------------------------------- + // Settings + // All shadow-scheduling knobs. Held inside LightLimitFix::Settings and + // serialised as part of that JSON blob. Pass a const-ref to Init(). + // ------------------------------------------------------------------------- + struct Settings + { + /// Enable the shadow caster scheduler entirely. Requires a game restart to take effect. + bool Enabled = true; + + /// Number of simultaneous shadow-casting point/spot lights (NOT counting the directional sun). + /// 0 = scheduler active but selects no point lights (sun/directional unaffected). + /// 4 = vanilla point light count with intelligent selection replacing the game's default. + /// 5-127 = extended mode; depth buffer array is expanded beyond game's 8-slot limit + /// when this exceeds 8. The practical ceiling is VRAM (per-slice cost + /// of the kSHADOWMAPS texture array) -- the in-game settings panel + /// shows a live projection so users can see when a value won't fit. + /// Higher values allow more lights to hold stale shadow maps between redraws at + /// the cost of startup memory. The redraw budget and interval formula control + /// per-frame GPU cost independently. + int32_t ShadowLightCount = 16; + + /// Number of additional converted-light slots (lights treated as normal lights + /// for geometry but tracked alongside shadow casters when ConvertExcessToNormal is enabled). + int32_t ConvertedShadowSlots = 32; + + /// Allow a newly-chosen light to draw even if it was not chosen last frame. + bool AllowDrawNewLight = true; + + /// Hard cap on how many lights may re-render their shadow maps in one + /// frame. Floored at kMinMaxRedrawPerFrame. + int32_t MaxRedrawPerFrame = 16; + + /// Lower bound for MaxRedrawPerFrame. Below this the per-slot redraw + /// rotation is slow enough that camera-relative jitter on the cluster + /// shadow lookup crosses occluder silhouettes between TAA frames, + /// producing visible shadow flicker on the nearest light's + /// contribution. Enforced in the ImGui slider and on JSON load. + static constexpr int32_t kMinMaxRedrawPerFrame = 4; + + /// How the per-frame shadow redraw budget is determined. + /// Manual is the default — predictable, doesn't ping-pong, and matches + /// the spirit of Intellightent's original behaviour. Formula is available + /// for power users who want adaptive logic, with the caveat that any + /// formula referencing `frametime` will tend to oscillate (rendering + /// shadows raises frametime, which removes the headroom that allowed + /// the budget — classic feedback loop without hysteresis). + BudgetModeEnum BudgetMode = BudgetModeEnum::Manual; + + /// Per-frame time budget for shadow re-renders (milliseconds). + /// Used in Manual mode. Lights whose estimated GPU cost would exceed this + /// are deferred to a later frame. + float RedrawBudgetMs = 5.0f; + + /// Demote shadow lights that exceed the active caster limit to normal (non-shadow) lights + /// so they still contribute diffuse lighting without a shadow-map cost. + bool ConvertExcessToNormal = true; + + /// Promote normal (non-shadow) lights to shadow casters when there is budget. + /// Disabled by default; experimental. + bool PromoteNormalToShadow = false; + + /// Force-enable portal-strict on shadow casters as they're added by + /// the engine. Per-type because portal-strict on spotlights drops + /// culled-but-visible spots entirely, while on omnis/hemispheres it + /// usefully tightens the engine's portal-graph visibility test. + /// + /// Defaults: omni + hemi enforced, spotlights left to their + /// engine-authored portal-strict flag. + bool ForceEnablePortalStrictOmni = true; + bool ForceEnablePortalStrictHemi = true; + bool ForceEnablePortalStrictSpot = false; + + // --- Formula strings (exprtk expressions) --- + + /// Light priority scoring formula (exprtk). Variables: + /// lightindex, lightintensity, lightdistance, playerlightdistance, + /// lightradius, lightx/y/z, lightr/g/b, lightambientr/g/b, + /// lightchosenlastframe, lightframessincerender, lightneverfades, + /// lightportalstrict, lightns, lightconverted, camerax/y/z, + /// isinterior, timeofday, lightisspot, lightspotvisible + /// Default-formula notes: + /// (1 + lightisspot * lightspotvisible) gives visible spots 2x, + /// omnis 1x. lightspotvisible=0 for spots pointing away from camera. + /// max(0, 1 - lightframessincerender / 8) * 0.4 is smooth temporal + /// stickiness; recently-rendered lights resist demotion across small + /// score perturbations, decaying to 0 over 8 frames since last redraw. + std::string ScoreFormula = "lightradius * lightintensity / (1 + ((1 - lightneverfades) * lightdistance) / 1000) * (1 + max(0, 1 - lightframessincerender / 8) * 0.4) * (1 + lightisspot * lightspotvisible)"; + + /// Redraw interval formula (per light). Higher = less frequent redraws. + /// Uses min(lightdistance, playerlightdistance) so that a light near the player + /// character is always treated as close even in third-person (camera is further away). + /// `lightdisplacement` further reduces the interval for lights that have moved. + std::string RedrawIntervalFormula = "min(10, (max(0, min(lightdistance, playerlightdistance) - lightradius * 0.5) / 500) / max(0.5, lightintensity)) * (lightconverted * 5 + 1) - min(lightdisplacement / 5, 10)"; + + /// Redraw budget formula (per frame, in ms). Used in Formula mode. + /// Default mirrors Intellightent's original behaviour: a flat 1 ms outdoors + /// (`isinterior` = 0) and 2 ms indoors (`isinterior` = 1). Predictable and + /// doesn't oscillate. + /// + /// Available variables: frametime (smoothed ms), frametarget (90th-pct ms), + /// stableframes, isinterior, plus the per-light variables (used by ScoreFormula + /// and RedrawIntervalFormula but evaluated to last-light values here). + /// + /// Avoid adaptive formulas that subtract shadow GPU cost from frametime + /// headroom -- they oscillate (rendering shadows raises frametime, + /// which zeroes the budget, which drops frametime, restoring the + /// budget). exprtk has no hysteresis state. Use static expressions. + std::string RedrawBudgetFormula = "1 + isinterior"; + + // --- Importance scheduling curve --- + + /// Interval multiplier applied to high-importance lights (importance >= 1). + /// Lower values make frequently-contributing lights update shadows more aggressively. + /// Default: 0.05 (updates 40x more frequently than unimportant lights). + float ImportanceMinScale = 0.05f; + + /// Interval multiplier applied to unimportant lights (importance == 0). + /// Higher values defer dim or distant lights more aggressively. + /// Default: 2.0. + float ImportanceMaxScale = 2.0f; + }; + + NLOHMANN_JSON_SERIALIZE_ENUM(BudgetModeEnum, + { { BudgetModeEnum::Auto, 0 }, { BudgetModeEnum::Manual, 1 }, { BudgetModeEnum::Formula, 2 } }) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Settings, + Enabled, + ShadowLightCount, + ConvertedShadowSlots, + AllowDrawNewLight, + MaxRedrawPerFrame, + BudgetMode, + RedrawBudgetMs, + ConvertExcessToNormal, + PromoteNormalToShadow, + ForceEnablePortalStrictOmni, + ForceEnablePortalStrictHemi, + ForceEnablePortalStrictSpot, + ScoreFormula, + RedrawIntervalFormula, + RedrawBudgetFormula, + ImportanceMinScale, + ImportanceMaxScale) + + // ------------------------------------------------------------------------- + // Per-light schedule entry + // ------------------------------------------------------------------------- + struct LightEntry + { + RE::BSShadowLight* Light{ nullptr }; + + /// Sort key: LastDrawnFrame + computed interval. Lower = higher priority. + double RedrawScore{ 0.0 }; + + /// Frame number this light last rendered its shadow map. + int32_t LastDrawnFrame{ -1 }; + + /// Set each frame by the scheduler; consumed by the render hook. + bool RedrawFrame{ false }; + + /// Slot index in the LightContainer array. + int32_t Index{ -1 }; + + /// World position of the light at its last rendered shadow map frame. + /// Used to prioritise redraws for lights that have moved significantly. + RE::NiPoint3 lastRenderedPos{ 0.0f, 0.0f, 0.0f }; + + /// Contribution-weighted importance score from the last scheduling frame. + /// importance = luminance(diffuse × fade) × attenuation²(viewer, radius) + /// where attenuation = max(1 − (dist/radius)², 0) (Skyrim's quadratic falloff). + /// Typically in [0, 1]; can exceed 1 for very bright lights at close range. + /// Higher = light strongly illuminates the area around the viewer. + float lastImportance{ 0.0f }; + + /// Hash of the shadow scene at the most recent successful redraw: + /// light pose + radius + each caster's worldBound + identity. Compared + /// against the current frame's hash to detect when the cached shadow + /// map is still pixel-correct (no geometric change since last render). + /// Industry term: "cached shadow maps" (UE5, CryEngine, Frostbite). + /// 0 sentinel = never-rendered; treat as "needs redraw" on first frame. + std::uint64_t lastGeomHash{ 0 }; + + /// Hash computed for the current frame's scoring pass. Promoted into + /// lastGeomHash only when this entry actually redraws (RedrawFrame=true), + /// so the cache key reflects what's *in the slot*, not what we observed. + std::uint64_t pendingGeomHash{ 0 }; + + void Clear() + { + Light = nullptr; + LastDrawnFrame = -1; + RedrawFrame = false; + lastRenderedPos = { 0.0f, 0.0f, 0.0f }; + lastImportance = 0.0f; + lastGeomHash = 0; + pendingGeomHash = 0; + } + }; + + // ------------------------------------------------------------------------- + // Container for the active light pool + // ------------------------------------------------------------------------- + struct LightContainer + { + LightEntry* Lights{ nullptr }; + + /// true when index 0 is the directional sun (always active, never rescheduled). + bool Sun{ false }; + + /// Total allocated slots (ShadowLightCount + ConvertedShadowSlots). + int32_t Size{ 0 }; + + /// Returns the first free shadow-caster slot index, or -1 if full. + int32_t FindFreeIndex(bool shadowSlot, int32_t shadowCount, int32_t convertCount) const; + + /// Returns the index of a light pointer in the shadow-caster range, or -1. + int32_t FindLight(RE::BSShadowLight* light, int32_t shadowCount) const; + + /// First pool index of the point-light range. Equals 1 when Sun=true + /// (slot 0 reserved for sun bookkeeping), 0 when Sun=false. + int32_t PointLightFirst() const { return Sun ? 1 : 0; } + + /// One-past-last pool index of the point-light range, given the + /// configured ShadowLightCount. Use as the exclusive upper bound for + /// `for (i = PointLightFirst(); i < PointLightEnd(N); ++i)` iteration + /// over chosen+candidate point lights (excludes converted slots which + /// follow at [PointLightEnd..PointLightEnd + ConvertedShadowSlots)). + /// + /// Off-by-one history: pre-this-helper, code iterated [0, shadowCount), + /// which missed pool[shadowCount] when Sun=true. The highest point-light + /// slot was then unfindable / unrendered / un-redrawn — silent loss of + /// one shadow caster slot when a sun is present. + int32_t PointLightEnd(int32_t shadowCount) const { return PointLightFirst() + shadowCount; } + }; + + // ------------------------------------------------------------------------- + // Per-light GPU timing tracker (sliding-window average over 8 frames) + // ------------------------------------------------------------------------- + static constexpr int kBudgetWindowSize = 8; + + struct BudgetEntry + { + uint64_t Key{ 0 }; + uint32_t Tracked[kBudgetWindowSize]{}; ///< Ring buffer of per-frame µs costs. + int32_t TrackedCount{ 0 }; + int32_t LastTrackedHelper{ -1 }; + uint32_t Progress{ 0 }; ///< Accumulated step-0 cost awaiting step-1. + int32_t Current{ 0 }; ///< Rolling sum of Tracked[]. + + void BeginStep(int32_t step); + void EndStep(int32_t step, int32_t helperCounter); + + /// Returns true when the entry hasn't been updated in ~600 scheduler ticks. + bool IsExpired(int32_t helperCounter) const; + + private: + int64_t _startTime{ 0 }; + }; + + struct BudgetTracker + { + void Begin(int32_t step); + void BeginLight(RE::BSShadowLight* light, int32_t step); + void EndLight(RE::BSShadowLight* light, int32_t step); + + /// Returns estimated render cost (µs) for a light. + /// Falls back to the mean of all tracked lights for unseen lights. + int32_t GetCost(RE::BSShadowLight* light) const; + + /// Returns the mean GPU cost (µs) averaged over all currently tracked lights. + int32_t GetAverageCostUs() const; + + private: + int32_t _counter{ 0 }; + std::unordered_map> _map; + + void CleanupExpired(); + }; + + // ------------------------------------------------------------------------- + // Per-slot visualization metadata (filled by LLF::CopyShadowLightData) + // ------------------------------------------------------------------------- + struct ShadowSlotInfo + { + uint32_t type = 0; ///< Shadow type: 0=spot/frustum, 1=hemisphere, 2=omnidirectional + float range = 0.0f; ///< Light range (world units) -- radius for point lights, cone distance for spots + bool valid = false; ///< true when this slot was written this frame + uintptr_t lightKey = 0; ///< Light object pointer (stable key for suppression) + }; + + /// Resets slot metadata for a new frame. Call at the start of CopyShadowLightData. + void BeginSlotFrame(uint32_t slotCount); + + /// Records metadata for one filled shadow slot. + void RecordSlot(uint32_t depthSlot, const ShadowSlotInfo& info); + + /// Returns true if the light with this pointer key has been suppressed by the user. + /// Includes implicit suppression from solo mode (every key except the soloed one). + bool IsSuppressed(uintptr_t lightKey); + + /// Returns true if any lights are currently suppressed (explicit or via solo). + bool HasSuppressedLights(); + + /// Returns true if any debug override is active (suppress / pin shadow / + /// pin convert / solo). Used by the LLF overlay's visibility gate so the + /// overlay stays available while users have any override in effect, even + /// without the visualisation modes or the explicit ShowShadowOverlay toggle. + bool HasAnyOverrides(); + + // ------------------------------------------------------------------------- + // Debugging override API + // + // Per-light state pins (Shadow / Convert) override the scheduler's automatic + // chosen/excess decision. Useful for isolating a single light's behaviour + // when chasing scheduler / cluster pipeline regressions: + // - Pin Shadow: bias scoring so the light is forced into the chosen pool + // (gets a real shadow slot up to ShadowLightCount). + // - Pin Convert: bias scoring to the bottom and force ConvertLight in the + // excess branch regardless of the ConvertExcessToNormal user setting + // (still honours the spot-gate -- spots that can't safely convert are + // disabled instead). + // - Suppress: existing behaviour (ShadowParam.y = -1 for casters; cluster + // filter for converted / non-shadow lights via solo). + // - Solo: when set, every key OTHER than the soloed one is reported as + // suppressed via IsSuppressed(). Lets you isolate one light's + // contribution against a black scene. + // ------------------------------------------------------------------------- + bool IsPinnedShadow(uintptr_t lightKey); + bool IsPinnedConvert(uintptr_t lightKey); + + void SetPinnedShadow(uintptr_t lightKey, bool pinned); + void SetPinnedConvert(uintptr_t lightKey, bool pinned); + + uintptr_t GetSoloLight(); + void SetSoloLight(uintptr_t lightKey); // 0 clears solo + + /// Mouse-hover key for the per-frame debug pulse. Set per row by the table + /// when the row is hovered; reset to 0 when the table redraws or the cursor + /// leaves the table. The cluster light builder (LightLimitFix::UpdateLights) + /// reads this to apply a magenta pulse to the matching light, making it + /// visible in 3D against the rest of the scene. + uintptr_t GetHoveredLight(); + void SetHoveredLight(uintptr_t lightKey); + + /// Drops every override (suppress / pin shadow / pin convert / solo). + /// Useful when a debugging session has accumulated state and lights are + /// mysteriously hidden — one click resets to the scheduler's auto behaviour. + void ClearAllOverrides(); + + /// Returns the number of shadow slots consumed this frame. + uint32_t GetSlotUsage(); + + /// Returns the number of active shadow-casting lights whose importance score + /// exceeds 0.1 (lights meaningfully illuminating the camera or player area). + uint32_t GetHighImportanceCount(); + + /// Read-only view of the per-slot metadata for the current frame. + const std::vector& GetSlotInfos(); + + /// Returns the display name for a shadow type index (0=Spot, 1=Hemi, 2=Omni). + const char* GetShadowTypeName(uint32_t type); + + /// Returns the golden-ratio hue colour for shadow-map slot slotIdx as an ImVec4. + /// Matches the mode-8 shader visualisation colour. + ImVec4 ShadowSlotHueColor(uint32_t slotIdx); + + /// Draw the interactive shadow caster table (suppress/filter/sort). + /// compact=true caps height; showColor adds a hue swatch column (viz mode 8). + /// sceneOnly=true shows only lights currently in the scene (overlay); false shows all known lights including disabled ones (settings). + /// readOnly hides the per-row Mode/Solo buttons (overlay when the menu is + /// closed isn't interactive anyway, so the buttons just take up space). + void DrawShadowLightTable(bool compact, bool showColor, bool sceneOnly = false, bool readOnly = false); + + /// Canonical one-place "where are we vs the limits" summary. Used by both + /// the menu's Active Casters block and the overlay header so the same + /// numbers appear identically in both views. clusterCount/clusterMax come + /// from LightLimitFix; the rest is read from SCM internal state. + void DrawShadowSummary(uint32_t clusterCount, uint32_t clusterMax, uint32_t shadowUnshadowedLightCount); + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + /// Call once from LightLimitFix::PostPostLoad() before Install(). + /// Allocates the light container and initialises state from settings. + void Init(const Settings& settings); + + /// Install all game hooks. Call from LightLimitFix::PostPostLoad(). + void Install(const Settings& settings); + + /// Per-frame update: refreshes installed slot count from the texture-array + /// capacity and applies settings changes (pool resize, etc.). The actual + /// scheduling -- choosing which lights cast shadows this frame -- happens + /// in the hooked `CalculateActiveShadowCasters` path via ScheduleShadowCasters. + /// Call from LightLimitFix::Prepass(). + void Update(const Settings& settings, RE::ShadowSceneNode* shadowSceneNode, + RE::NiCamera* worldCamera); + + /// Clear all transient session state (pool entries, converted-light + /// tracking, debug overrides). Used by the LoadingMenu hook to drop + /// pointers to lights the engine is about to free during fast-travel + /// / cell change. The per-frame reconciliation in ScheduleShadowCasters + /// covers the same ground for incremental changes; this is the wholesale + /// reset for known scene boundaries so the UI counter and table read 0 + /// during the loading screen instead of carrying stale entries forward. + void ResetSession(); + + /// Register the LoadingMenu open/close handler so ResetSession fires + /// when the user starts a fast-travel or cell transition. Call once + /// from SCM::Install after the rest of the hooks are in place. + void RegisterSceneTransitionEvents(); + + /// Returns a read-only view of the active light pool for UI/visualization. + const LightContainer& GetLights(); + + /// Returns the kSHADOWMAPS texture-array slot for an active point/spot + /// shadow light as a raw slice index 0..GetInstalledSlotCount()-1, or -1 + /// when the light is either not active in the SCM pool OR is the sun. + /// Consumers (ShadowRenderer upload, LightLimitFix cluster builder, + /// strict-light shadow-flag setup) treat the -1 sentinel as "skip" -- + /// the sun renders to kSHADOWMAPS_ESRAM (a separate texture) and has no + /// kSHADOWMAPS slice; inactive lights have no slot at all. + /// Uses the internal s_lights pool -- does not read the descriptor's + /// shadowmapIndex field, which may be corrupted by ReturnShadowmaps(). + int32_t GetShadowSlot(RE::BSShadowLight* light); + + /// Visit every shadow light currently demoted to non-shadow rendering via + /// ConvertExcessToNormal. These lights live in the engine's activeShadowLights + /// list (0x148) but are reported as non-shadow by Hook_IsShadowLight. The + /// cluster pipeline (LightLimitFix::UpdateLights) needs to inject them into + /// lightsData[] without the Shadow flag so they still contribute diffuse light. + /// + /// Visitor signature: void(RE::BSShadowLight* light). Pointers are stable for + /// the duration of the call (no concurrent scheduler mutation). + void ForEachConvertedLight(const std::function& visitor); + + /// Draw scheduler stats (avg redraws/frame and avg per-light cost). + /// Reads internal SCM state so the caller doesn't need accessors. Intended + /// to render directly under DrawShadowLightTable for testing context. + void DrawShadowSchedulerStats(); + + /// Draw per-mode overlay info for shadow-related visualisation modes (3-9). + /// Call from LightLimitFix::DrawOverlay() inside the vizOn block for modes >= 3. + /// totalLightCount is the current clustered light count owned by LightLimitFix. + void DrawOverlayShadowModeInfo(uint32_t mode, uint32_t shadowUnshadowedLightCount, uint32_t totalLightCount); + + /// Appends tooltip text for visualisation modes 3-9 (all shadow-specific). + /// Call from LightLimitFix::DrawSettings() inside the LightsVisualisationMode hover tooltip, + /// immediately after the LLF-owned entries for modes 0-2. + void DrawVisualisationTooltipShadowModes(); + + /// Draw the ImGui settings panel for the shadow caster scheduler. + /// Call from LightLimitFix::DrawSettings(). + void DrawSettings(Settings& settings); + + /// Apply any Skyrim-side INI overrides SCM owns (currently just + /// iShadowMapResolution:Display) at LoadSettings time. The engine has + /// already read SkyrimPrefs.ini at startup so this is a no-op for now, + /// but the seam exists for future overrides that need a pre-Install + /// hook. Call from LightLimitFix::LoadSettings. + void LoadINISettings(); + + /// Persist any Skyrim-side INI settings SCM owns directly to the user's + /// SkyrimPrefs.ini in their Documents folder. Only writes when the user + /// actually edited a value this session. Call from + /// LightLimitFix::SaveSettings. + void SaveINISettings(); + +} diff --git a/src/Features/LightLimitFix/ShadowRenderer.cpp b/src/Features/LightLimitFix/ShadowRenderer.cpp new file mode 100644 index 0000000000..9b44116d6b --- /dev/null +++ b/src/Features/LightLimitFix/ShadowRenderer.cpp @@ -0,0 +1,353 @@ +// Shadow rendering operations for LightLimitFix. +// Contains: resource setup, per-frame data copy, and shadow-specific UI. + +#include "../LightLimitFix.h" +#include "Deferred.h" +#include "Menu/ThemeManager.h" +#include "State.h" +#include "Util.h" + +// Fills a ShadowLightData entry from a light's shadowmap descriptor transform. +// Returns true on success, false when the light has no usable descriptors -- +// the caller must treat false as "do not advertise a valid shadow for this +// slot", because ShadowProj remains at its default zero matrix and the +// shader's depth-comparison sampling against that matrix collapses to +// "fully shadowed" (the worst possible visual outcome -- e.g. grass goes +// pitch black under any shadow-flagged point light). Pair this with a +// ShadowParam.y = 0 fallback in the caller so the shader's safe sentinel +// (`if (ShadowLightParam.y == 0) return 1.0;`) keeps the slot fully lit +// instead of fully dark. +template +static bool SetShadowParameters(T& lightData, Deferred::ShadowLightData& sd) +{ + if (lightData.shadowmapDescriptors.empty()) + return false; + + auto& desc = lightData.shadowmapDescriptors[0]; + DirectX::XMMATRIX proj = DirectX::XMLoadFloat4x4(reinterpret_cast(&desc.lightTransform)); + DirectX::XMStoreFloat4x4(&sd.ShadowProj, proj); + + DirectX::XMMATRIX invProj = DirectX::XMMatrixInverse(nullptr, proj); + DirectX::XMStoreFloat4x4(&sd.InvShadowProj, invProj); + + sd.ShadowParam.z = lightData.shadowBiasScale * 0.00025f; + return true; +} + +// ─── Per-frame shadow data copy ─────────────────────────────────────────────── + +void LightLimitFix::EarlyPrepass() +{ + auto state = globals::state; + state->BeginPerfEvent("LLF CopyShadowLightData"); + CopyShadowLightData(); + state->EndPerfEvent(); +} + +void LightLimitFix::CopyShadowLightData() +{ + ZoneScoped; +#ifdef TRACY_ENABLE + TracyD3D11Zone(globals::state->tracyCtx, "LLF CopyShadowLightData"); +#endif + + uint32_t slots = ShadowCasterManager::GetInstalledSlotCount(); + if (slots == 0) { + // Clean degradation when SCM hasn't published a usable slot count yet + // (e.g. before SetupResources finishes, or after a transient + // reallocation failure). Without this clear, the previous frame's + // slot metadata, counters, and PS bindings at t102/t103 stay live -- + // the overlay shows stale shadow rows and shaders keep sampling + // stale shadow records instead of degrading cleanly to unshadowed. + ShadowCasterManager::BeginSlotFrame(0); + shadowLightCount = 0; + shadowUnshadowedLightCount = 0; + ID3D11ShaderResourceView* nullSRVs[2]{ nullptr, nullptr }; + globals::d3d::context->PSSetShaderResources(102, ARRAYSIZE(nullSRVs), nullSRVs); + return; + } + + auto* shadowSceneNode = globals::game::smState->shadowSceneNode[0]; + if (!shadowSceneNode) { + // Same cleanup contract as the slots==0 path above: clear slot + // metadata + counters and unbind t102/t103 so the overlay doesn't + // show stale rows and shaders degrade to unshadowed instead of + // sampling a previous frame's records. + ShadowCasterManager::BeginSlotFrame(0); + shadowLightCount = 0; + shadowUnshadowedLightCount = 0; + ID3D11ShaderResourceView* nullSRVs[2]{ nullptr, nullptr }; + globals::d3d::context->PSSetShaderResources(102, ARRAYSIZE(nullSRVs), nullSRVs); + return; + } + + // Lazy (re)allocation when slot count changes (e.g. on resolution change). + if (!shadowLights || shadowLightsCapacity != slots) { + delete shadowLights; + shadowLights = nullptr; + + D3D11_BUFFER_DESC sbDesc{}; + sbDesc.Usage = D3D11_USAGE_DYNAMIC; + sbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + sbDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + sbDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; + sbDesc.StructureByteStride = sizeof(Deferred::ShadowLightData); + sbDesc.ByteWidth = slots * sizeof(Deferred::ShadowLightData); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{}; + srvDesc.Format = DXGI_FORMAT_UNKNOWN; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER; + srvDesc.Buffer.FirstElement = 0; + srvDesc.Buffer.NumElements = slots; + + shadowLights = new Buffer(sbDesc, nullptr, "LLF::ShadowLights"); + shadowLights->CreateSRV(srvDesc); + shadowLightsCapacity = slots; + } + + // Static reusable buffer for per-frame shadow light data. The previous + // `std::vector(slots)` ctor heap-allocated every frame in this render + // hot path -- avoidable churn given the slot count only changes on + // resolution / settings reconfigures (matched by the shadowLights + // buffer reallocation block above). assign(slots, {}) reuses the + // backing storage when slot count is stable and zero-fills entries. + static std::vector sd; + sd.assign(slots, {}); + uint32_t prevSlotUsage = ShadowCasterManager::GetSlotUsage(); + ShadowCasterManager::BeginSlotFrame(slots); + auto context = globals::d3d::context; + + ID3D11ShaderResourceView* shadowMapsSRV = + globals::game::renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGET_DEPTHSTENCIL::kSHADOWMAPS].depthSRV; + + uint32_t plCount = 0; + uint32_t unshadowedLights = 0; + ShadowCasterManager::ForEachShadowLight(shadowSceneNode->GetRuntimeData().shadowLightsAccum, + [&](RE::BSShadowLight* light) { + // Use the stable container-slot index from s_lights rather than + // reading shadowmapDescriptors[0].shadowmapIndex, which can drift + // relative to our scheduler-assigned slot when ReturnShadowmaps + // fires between ScheduleShadowCasters and this function. + int32_t stableSlot = ShadowCasterManager::GetShadowSlot(light); + if (stableSlot < 0) { + // Sun (BSShadowDirectionalLight) — no kSHADOWMAPS slice. Its + // shadow lives in kSHADOWMAPS_ESRAM and is sampled through a + // separate path (DirectionalShadowCascades at t99). Skip + // silently so we don't count it as an "unshadowed point + // light" or scribble garbage into sd[0]. + return; + } + if (static_cast(stableSlot) >= slots) { + unshadowedLights++; + plCount++; + return; + } + uint32_t depthSlot = static_cast(stableSlot); + + { + float shadowTypeF = light->GetIsParabolicLight() ? float(light->shadowMapCount == 2 ? 2 : 1) : 0.f; + sd[depthSlot].ShadowParam.x = shadowTypeF; + + const bool projValid = globals::game::isVR ? + SetShadowParameters(light->GetVRRuntimeData(), sd[depthSlot]) : + SetShadowParameters(light->GetRuntimeData(), sd[depthSlot]); + + float range = light->light->GetLightRuntimeData().radius.x; + // ShadowParam.y semantics in the shader: + // > 0 → valid radius; sample kSHADOWMAPS via ShadowProj at the slot. + // == 0 → safe sentinel; shader returns 1.0 (fully lit, no shadow). + // < 0 → suppression sentinel; shader returns 0.0 (fully dark). + // If SetShadowParameters skipped (empty descriptors -> ShadowProj + // stays default zero matrix), we MUST leave ShadowParam.y at 0 so + // the safe sentinel fires. Otherwise the shader samples a zero + // projection -> depth comparison says fully shadowed -> any + // shadow-flagged light with stale descriptors makes grass go + // pitch black under that light. + uintptr_t lightKey = reinterpret_cast(light); + const bool suppressed = ShadowCasterManager::IsSuppressed(lightKey); + sd[depthSlot].ShadowParam.y = suppressed ? -1.0f : (projValid ? range : 0.0f); + ShadowCasterManager::RecordSlot(depthSlot, + { static_cast(shadowTypeF), range, true, lightKey }); + } + + plCount++; + }); + + if (plCount != shadowLightCount || ShadowCasterManager::GetSlotUsage() != prevSlotUsage || unshadowedLights != shadowUnshadowedLightCount) { + shadowLightCount = plCount; + shadowUnshadowedLightCount = unshadowedLights; + + // Throttle the count-change log: this fires every time plCount or + // slot usage moves by even 1, which in busy outdoor scenes is + // effectively every frame. Earlier logs averaged ~10 entries/sec + // (25k lines over a 39-minute session, dwarfing every other + // signal). Two filters: + // - Significance: only log when the count moves by >= 4 from + // the last logged value, OR when the unshadowed-lights count + // changes at all (rarer, more interesting). + // - Rate: floor at 1 line/sec via QueryPerformanceCounter + // (project convention; State.h uses QPC, std::chrono is disfavored). + static int s_lastLoggedShadowCount = -1; + static uint32_t s_lastLoggedUnshadowed = 0; + static LARGE_INTEGER s_lastLogQpc = { .QuadPart = 0 }; + static LARGE_INTEGER s_qpcFrequency = []() { + LARGE_INTEGER f{}; + QueryPerformanceFrequency(&f); + return f; + }(); + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + const bool unshadowedChanged = unshadowedLights != s_lastLoggedUnshadowed; + const bool countSignificant = s_lastLoggedShadowCount < 0 || + std::abs(static_cast(plCount) - s_lastLoggedShadowCount) >= 4; + const bool rateOk = (now.QuadPart - s_lastLogQpc.QuadPart) >= s_qpcFrequency.QuadPart; + if ((countSignificant || unshadowedChanged) && rateOk) { + s_lastLoggedShadowCount = static_cast(plCount); + s_lastLoggedUnshadowed = unshadowedLights; + s_lastLogQpc = now; + if (unshadowedLights > 0) + logger::debug("[LLF] {} shadow lights, {} / {} slots used; {} lights dropped (no shadow)", + plCount, ShadowCasterManager::GetSlotUsage(), slots, unshadowedLights); + else + logger::debug("[LLF] {} shadow lights, {} / {} slots used", plCount, ShadowCasterManager::GetSlotUsage(), slots); + } + } + + { + D3D11_MAPPED_SUBRESOURCE mapped{}; + DX::ThrowIfFailed(context->Map(shadowLights->resource.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); + memcpy(mapped.pData, sd.data(), slots * sizeof(Deferred::ShadowLightData)); + context->Unmap(shadowLights->resource.get(), 0); + ID3D11ShaderResourceView* srv = shadowLights->srv.get(); + context->PSSetShaderResources(102, 1, &srv); + } + + context->PSSetShaderResources(103, 1, &shadowMapsSRV); +} + +// ─── Debug helpers ──────────────────────────────────────────────────────────── + +std::string LightLimitFix::BuildShadowSlotColorLegend() const +{ + const auto& shadowSlotInfos = ShadowCasterManager::GetSlotInfos(); + if (shadowSlotInfos.empty()) + return {}; + + std::string out = "Shadow Slot Color Map (Mode 8):\n"; + for (uint32_t i = 0; i < static_cast(shadowSlotInfos.size()); ++i) { + const auto& info = shadowSlotInfos[i]; + if (!info.valid) + continue; + + float hue = fmodf(float(i) * 0.618033988f, 1.0f); + ImVec4 c = ShadowCasterManager::ShadowSlotHueColor(i); + auto ri = static_cast(c.x * 255.0f); + auto gi = static_cast(c.y * 255.0f); + auto bi = static_cast(c.z * 255.0f); + + out += std::format(" Slot {:2d} | hue {:5.3f} | #{:02X}{:02X}{:02X} | {:11s} | r={:.0f}\n", + i, hue, ri, gi, bi, ShadowCasterManager::GetShadowTypeName(info.type), info.range); + } + return out; +} + +// ─── Overlay ───────────────────────────────────────────────────────────────── + +void LightLimitFix::DrawOverlay() +{ + // Overlay shows when: + // - visualisation modes are active (debug heatmaps), OR + // - any light is suppressed (so the suppression list stays accessible), OR + // - any debug override is in effect (pin shadow / pin convert / solo) so + // users can find what they pinned without remembering to toggle anything, OR + // - the user explicitly opted in via Show Shadow Overlay (lets the table's + // debug controls — cycle button, solo, hover-pulse — be reachable in + // the default state without first triggering a side-effect). + bool vizOn = EnableLightsVisualisation; + bool hasSuppressed = ShadowCasterManager::HasSuppressedLights(); + bool hasOverrides = ShadowCasterManager::HasAnyOverrides(); + bool showOverlay = settings.ShowShadowOverlay; + if (!vizOn && !hasSuppressed && !hasOverrides && !showOverlay) + return; + + // When the CS menu is open, show a draggable/resizable window so the user can + // move it out of the way and expand the table. When the menu is closed, keep + // it as a compact pinned overlay (no title bar, no chrome). + bool menuOpen = globals::menu->IsEnabled; + const float pos = ThemeManager::Constants::OVERLAY_WINDOW_POSITION * Util::GetUIScale(); + + // Single unified window: same ImGui ID across menu open/closed so the + // user's resize persists. Title bar and Move are toggled via flags -- + // hidden when the menu is closed (pinned debug overlay) and shown when + // the menu is open (so the user can drag/resize via the title bar). + // We deliberately don't pass NoSavedSettings so ImGui retains the size + // the user picked across sessions. + ImGuiWindowFlags flags = ImGuiWindowFlags_None; + if (!menuOpen) + flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove; + + ImGui::SetNextWindowPos(ImVec2(pos, pos), menuOpen ? ImGuiCond_Appearing : ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(340, 480), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSizeConstraints(ImVec2(280, 200), ImVec2(800, 1200)); + ImGui::Begin("LLF Shadow Slots", nullptr, flags); + + if (vizOn) { + static const char* kVizNames[] = { + "Light Limit", "Strict Lights Count", "Clustered Lights Count", + "Shadow Mask", "Shadow Light Count", "Point Light Shadow Factor", + "Unshadowed Point Lights", "Shadow Caster Density", + "Shadow Slot Index Color", "Light Type Visualization" + }; + uint32_t m = LightsVisualisationMode; + const char* vizName = (m < IM_ARRAYSIZE(kVizNames)) ? kVizNames[m] : "Unknown"; + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "LLF DEBUG - %s", vizName); + } else + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "LLF - Shadow Suppression"); + ImGui::Separator(); + + uint32_t mode = vizOn ? LightsVisualisationMode : UINT32_MAX; + + // ── All stats grouped above the table (same order as menu) ───────── + // Summary always visible. Scheduler stats only when not in a viz mode + // that has its own legend competing for the same space. + ShadowCasterManager::DrawShadowSummary(lightCount, MAX_LIGHTS, shadowUnshadowedLightCount); + if (!vizOn) + ShadowCasterManager::DrawShadowSchedulerStats(); + + // ── Per-mode informational panels (visualization-mode-specific only) ── + if (vizOn) { + if (mode == 2) { + uint32_t cx = clusterSize[0], cy = clusterSize[1], cz = clusterSize[2]; + ImGui::Text("Cluster grid : %ux%ux%u (%u total)", cx, cy, cz, cx * cy * cz); + ImGui::Text("Max lights/cluster : %u", CLUSTER_MAX_LIGHTS); + } else if (mode >= 3) { + ShadowCasterManager::DrawOverlayShadowModeInfo(mode, shadowUnshadowedLightCount, lightCount); + } + } + + // ── Shadow slot toggle table ───────────────────────────────────── + // Show when in a shadow-related viz mode, or when lights are suppressed. + // readOnly=true when the menu is closed -- the overlay isn't interactive + // then, so the per-row Mode/Solo buttons would be dead pixels. readOnly + // also bounds the table height so the stats above stay visible even + // when many lights are present (the table scrolls internally instead + // of pushing the window past its max-height constraint). + bool shadowRelatedMode = !vizOn || (mode >= 4); + // Also show the table when the user explicitly opened the overlay + // (Show Shadow Overlay toggle) or has any per-light overrides -- the + // tooltip promises the table's debug controls are reachable any time + // once the overlay is open, but viz modes 0-3 leave shadowRelatedMode + // false so without these extra terms the user gets an empty window. + if (showOverlay || hasOverrides || hasSuppressed || shadowRelatedMode) { + ImGui::Separator(); + // compact=false in the overlay: the table fills the remaining + // content region of the user-sized window and scrolls internally + // (ScrollY in Util::ShowSortedStringTableCustom). Stats above stay + // visible regardless of how many lights exist or how the user has + // resized the window. readOnly is still true when the menu is + // closed -- buttons would be dead pixels. + ShadowCasterManager::DrawShadowLightTable(false, vizOn && (mode == 8), true, !menuOpen); + } + + ImGui::End(); +} diff --git a/src/Features/VR.cpp b/src/Features/VR.cpp index 1f011fc01b..49a5f099f3 100644 --- a/src/Features/VR.cpp +++ b/src/Features/VR.cpp @@ -294,3 +294,18 @@ void VR::Reset() { stereoOpt.Reset(); } + +float VR::GetHMDRefreshRate() const +{ + if (!globals::game::isVR) + return 0.0f; + auto* openvr = RE::BSOpenVR::GetSingleton(); + if (!openvr || !openvr->vrSystem) + return 0.0f; + vr::ETrackedPropertyError err = vr::TrackedProp_Success; + float hz = openvr->vrSystem->GetFloatTrackedDeviceProperty( + vr::k_unTrackedDeviceIndex_Hmd, + vr::Prop_DisplayFrequency_Float, + &err); + return (err == vr::TrackedProp_Success && hz > 1.0f) ? hz : 0.0f; +} diff --git a/src/Features/VR.h b/src/Features/VR.h index b08cebc193..e910a38db8 100644 --- a/src/Features/VR.h +++ b/src/Features/VR.h @@ -548,6 +548,10 @@ struct VR : OverlayFeature void DetectOpenVRInfo(); bool IsOpenVRCompatible() const; + /// Returns the HMD display refresh rate in Hz, or 0.0 if unavailable. + /// Queries IVRSystem via the game's already-loaded OpenVR DLL — no extra linking required. + float GetHMDRefreshRate() const; + private: //============================================================================= // PRIVATE HELPERS diff --git a/src/Globals.cpp b/src/Globals.cpp index 68aea71e0c..b62d509246 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -94,6 +94,8 @@ namespace globals namespace llf { + void** normalDepthBuffer = nullptr; + void** readOnlyDepthBuffer = nullptr; } } @@ -133,6 +135,11 @@ namespace globals D3D11_MAPPED_SUBRESOURCE* mappedFrameBuffer = nullptr; FrameBufferCache frameBufferCached{}; + + int32_t* frameCounter = nullptr; + int* viewWidth = nullptr; + int* viewHeight = nullptr; + bool* drawStereo = nullptr; } static void RefreshTES() @@ -194,6 +201,12 @@ namespace globals perFrame = { REL::RelocationID(524768, 411384) }; currentAccumulator = { REL::RelocationID(527650, 414600) }; + + frameCounter = reinterpret_cast(REL::RelocationID(525008, 411489).address()); + viewWidth = reinterpret_cast(REL::RelocationID(524978, 411459).address()); + viewHeight = reinterpret_cast(REL::RelocationID(524979, 411460).address()); + if (REL::Module::IsVR()) + drawStereo = reinterpret_cast(REL::RelocationID(524907, 411393).address() + sizeof(void*)); } { diff --git a/src/Globals.h b/src/Globals.h index 4556d5ab18..a2ca155e0b 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -101,6 +101,8 @@ namespace globals namespace llf { + extern void** normalDepthBuffer; + extern void** readOnlyDepthBuffer; } } @@ -246,6 +248,11 @@ namespace globals extern D3D11_MAPPED_SUBRESOURCE* mappedFrameBuffer; extern FrameBufferCache frameBufferCached; + + extern int32_t* frameCounter; + extern int* viewWidth; + extern int* viewHeight; + extern bool* drawStereo; } namespace rtti diff --git a/src/Utils/UI.h b/src/Utils/UI.h index c48adeefba..77289ada38 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -626,7 +626,13 @@ namespace Util const std::vector& footerRows = {}, const ImVec2& outerSize = ImVec2(0, 0)) { - ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp; + // ScrollY makes the table scroll internally when its bounded + // outerSize is smaller than its content. For unbounded tables + // (outerSize.y==0, auto-sized below) the size always fits the rows + // so the scrollbar stays hidden -- adding the flag is harmless in + // that case and lets bounded callers (overlay's host window) keep + // content above the table visible regardless of row count. + ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY; ImVec2 tableSize = outerSize; if (outerSize.y == 0.0f) { size_t totalRows = rows.size() + footerRows.size(); diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index 72c3f7a509..44b94558c8 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -53,7 +53,7 @@ extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_s InitializeLog(); logger::info("Loaded {} {}", Plugin::NAME, Plugin::VERSION.string()); SKSE::Init(a_skse); - SKSE::AllocTrampoline(1 << 10); + SKSE::AllocTrampoline(1 << 12); return Load(); } diff --git a/vcpkg.json b/vcpkg.json index 1d6bd1ab78..1c5a5b57b2 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -17,6 +17,7 @@ "directxtex", "eastl", "efsw", + "exprtk", { "name": "imgui", "features": ["dx11-binding", "win32-binding", "docking-experimental"] From 59fa87d5e9986a6c64cc580685068bf69f43cfba Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 27 May 2026 00:54:48 -0700 Subject: [PATCH 22/24] fix(slf): VR shadow-mask OOB CTD (#46) Co-authored-by: Claude Opus 4.7 (1M context) --- .../LightLimitFix/ShadowCasterManager.cpp | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/src/Features/LightLimitFix/ShadowCasterManager.cpp b/src/Features/LightLimitFix/ShadowCasterManager.cpp index 2bd5a146d4..4388152dda 100644 --- a/src/Features/LightLimitFix/ShadowCasterManager.cpp +++ b/src/Features/LightLimitFix/ShadowCasterManager.cpp @@ -3579,18 +3579,53 @@ namespace ShadowCasterManager } // ---- Screen-space shadow-mask pass: clamp to vanilla 4 slices --------- - // Detour RenderShadowLightsWithUtilityShader (100423/107141). The wrapper - // runs vanilla but writes a null sentinel into shadowLightsAccum at the - // 4-slice cutoff so the engine's loop never OOB-reads the 4-entry - // per-slot blend-mode table for any extended-mode slot. The mask's R - // channel (sun cascades) is the only channel LIGHT_LIMIT_FIX consumes; - // extended shadow casters are served by LLF's cluster pipeline reading - // kSHADOWMAPS directly. See the Hook_RenderShadowLightsWithUtilityShader - // definition above for the full rationale, including the previous - // Hook_DisableColorMask's misread (it patched out the inner call, not a - // color-mask call -- verified via Ghidra). - stl::detour_thunk( - REL::RelocationID(100423, 107141)); + // Suppress the engine's screen-space shadow-mask inner loop. With + // extended slot counts SLF can produce maskIndex >= 4, which makes + // the loop OOB-read the 4-entry per-slot blend-mode table + // (DAT_141861380); the mask's R channel (sun cascades) is the only + // channel LIGHT_LIMIT_FIX consumes anyway -- extended shadow casters + // are served by LLF's cluster pipeline reading kSHADOWMAPS directly. + // See Hook_RenderShadowLightsWithUtilityShader above for the full + // rationale, including the previous Hook_DisableColorMask's misread + // (it patched out the inner call, not a color-mask call -- verified + // via Ghidra). + if (globals::game::isVR) { + // VR's Main::RenderShadowmasks (100422) inlines the inner loop + // instead of calling the standalone 100423, so the detour below + // would never fire. NOP the near-CALL at +0x9E directly. Only + // needed when extended slot counts can produce maskIndex >= 4; + // vanilla 4-slice VR doesn't trip the OOB. + if (settings.ShadowLightCount > 4) { + // Site verification: the call at +0x9E must be `E8 rel32` + // targeting the inlined helper at +0xC0 (rel32 = 0xC0 - + // next-instruction-addr = 0xC0 - 0xA3 = 0x1D). If either the + // opcode or the target drifts, fail closed -- clamp the + // scheduler back to vanilla 4 slots so a drifted binary + // degrades to "no extended shadows" rather than the original + // CTD path this patch protects. + static REL::RelocationID renderShadowmasks(100422, 107140); + constexpr std::uint8_t kCallOffset = 0x9E; + constexpr std::uint8_t kCallOpcode = 0xE8; // near CALL rel32 + constexpr std::int32_t kExpectedRel32 = 0x1D; + const auto site = renderShadowmasks.address() + kCallOffset; + const auto opcode = *reinterpret_cast(site); + const auto rel32 = *reinterpret_cast(site + 1); + if (opcode != kCallOpcode || rel32 != kExpectedRel32) { + logger::warn("[SCM] VR shadow-mask site drift: expected E8 rel32=0x{:X} at RenderShadowmasks+0x{:X}, " + "found 0x{:02X} rel32=0x{:X}. Clamping ShadowLightCount to 4 (vanilla) to avoid OOB CTD.", + kExpectedRel32, kCallOffset, opcode, rel32); + s_installedShadowLightCount = 4; + } else { + REL::safe_fill(site, REL::NOP, 5); + logger::info("[SCM] VR: NOPed inner shadow-mask call at RenderShadowmasks+0x{:X}", kCallOffset); + } + } + } else { + // Flat (SE/AE): RenderShadowmasks calls the standalone 100423, + // so detour that function to a no-op. + stl::detour_thunk( + REL::RelocationID(100423, 107141)); + } // ---- Shadow caster selection ----------------------------------------- From 694504c49f2031ad17ffabf514de55eae7ff4f1a Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 27 May 2026 19:02:52 -0700 Subject: [PATCH 23/24] feat(VR): add foveated rendering (#44) Co-authored-by: YtzyFvra <59631290+YtzyFvra@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 --- .../FoveatedRender/SubrectStretchCS.hlsl | 97 +++ src/Features/ScreenshotFeature.cpp | 32 +- src/Features/Upscaling.cpp | 123 +++- src/Features/Upscaling.h | 18 +- src/Features/Upscaling/FoveatedRender.cpp | 323 ++++++++++ src/Features/Upscaling/FoveatedRender.h | 111 ++++ .../Upscaling/FoveatedRender/Bridge.cpp | 79 +++ .../Upscaling/FoveatedRender/Bridge.h | 42 ++ .../Upscaling/FoveatedRender/Core.cpp | 581 ++++++++++++++++++ src/Features/Upscaling/FoveatedRender/Core.h | 93 +++ .../Upscaling/FoveatedRender/Modes.cpp | 254 ++++++++ src/Features/Upscaling/FoveatedRender/Ops.h | 66 ++ .../Upscaling/FoveatedRender/Params.cpp | 70 +++ .../Upscaling/FoveatedRender/Params.h | 50 ++ .../Upscaling/FoveatedRender/Postprocess.cpp | 62 ++ .../Upscaling/FoveatedRender/Postprocess.h | 16 + .../Upscaling/FoveatedRender/Preprocess.cpp | 109 ++++ .../Upscaling/FoveatedRender/Preprocess.h | 15 + src/Features/Upscaling/PerfMode.cpp | 3 +- src/Features/Upscaling/PerfMode.h | 3 +- src/Features/Upscaling/Streamline.cpp | 14 +- src/Features/Upscaling/Streamline.h | 14 +- src/Hooks.cpp | 8 + src/Utils/Subrect.cpp | 4 + src/Utils/Subrect.h | 21 +- src/Utils/Subrect_PreviewBlend.cpp | 42 ++ 26 files changed, 2197 insertions(+), 53 deletions(-) create mode 100644 features/Upscaling/Shaders/Upscaling/FoveatedRender/SubrectStretchCS.hlsl create mode 100644 src/Features/Upscaling/FoveatedRender.cpp create mode 100644 src/Features/Upscaling/FoveatedRender.h create mode 100644 src/Features/Upscaling/FoveatedRender/Bridge.cpp create mode 100644 src/Features/Upscaling/FoveatedRender/Bridge.h create mode 100644 src/Features/Upscaling/FoveatedRender/Core.cpp create mode 100644 src/Features/Upscaling/FoveatedRender/Core.h create mode 100644 src/Features/Upscaling/FoveatedRender/Modes.cpp create mode 100644 src/Features/Upscaling/FoveatedRender/Ops.h create mode 100644 src/Features/Upscaling/FoveatedRender/Params.cpp create mode 100644 src/Features/Upscaling/FoveatedRender/Params.h create mode 100644 src/Features/Upscaling/FoveatedRender/Postprocess.cpp create mode 100644 src/Features/Upscaling/FoveatedRender/Postprocess.h create mode 100644 src/Features/Upscaling/FoveatedRender/Preprocess.cpp create mode 100644 src/Features/Upscaling/FoveatedRender/Preprocess.h create mode 100644 src/Utils/Subrect_PreviewBlend.cpp diff --git a/features/Upscaling/Shaders/Upscaling/FoveatedRender/SubrectStretchCS.hlsl b/features/Upscaling/Shaders/Upscaling/FoveatedRender/SubrectStretchCS.hlsl new file mode 100644 index 0000000000..266fe5fd50 --- /dev/null +++ b/features/Upscaling/Shaders/Upscaling/FoveatedRender/SubrectStretchCS.hlsl @@ -0,0 +1,97 @@ +// Stretches the DRS-rendered region from a temporary render-resolution SBS texture +// to fill the entire eye in the display-resolution kMAIN SBS texture. +// Dispatched once per eye. Supports multiple sampling modes: +// 0 = Bilinear (clean upscale) +// 1 = Point / Nearest (cheapest, VRS-like broadcast) +// 2 = Gaussian Blur 3x3 (soft periphery background) + +cbuffer StretchCB : register(b0) +{ + uint DstOffsetX; // SBS destination X offset for this eye (0 or eyeWidthOut) + uint DstWidth; // display-resolution eye width + uint DstHeight; // display-resolution eye height + uint SrcOffsetX; // SBS source X offset for this eye (0 or renderEyeW) + uint SrcWidth; // render-resolution SBS total width (for UV normalisation) + uint SrcHeight; // render-resolution SBS total height + uint SrcEyeWidth; // render-resolution per-eye width + uint SrcEyeHeight; // render-resolution per-eye height + uint StretchMode; // 0=Bilinear, 1=Point, 2=GaussianBlur + float BlurRadius; // Texel-space radius for Gaussian blur (typical 0.5-4.0) + uint DebugVisualize; // 0=off, 1=tint stretched periphery red so the DLSS region pops + uint _pad; +}; + +Texture2D SrcTex : register(t0); +SamplerState BilinearSampler : register(s0); +RWTexture2D DstTex : register(u0); + +[numthreads(8, 8, 1)] void main(uint3 tid : SV_DispatchThreadID) { + // Zero-dim guard: a misconfigured dispatch with any zero extent would + // divide-by-zero into NaN UVs and underflow point-mode coords into + // huge uint values. Bail before any math. + if (DstWidth == 0 || DstHeight == 0 || SrcWidth == 0 || SrcHeight == 0 || + SrcEyeWidth == 0 || SrcEyeHeight == 0) + return; + + if (tid.x >= DstWidth || tid.y >= DstHeight) + return; + + // Map output pixel to normalised position within this eye [0,1] + float u = ((float)tid.x + 0.5) / (float)DstWidth; + float v = ((float)tid.y + 0.5) / (float)DstHeight; + + // Map to source texel coordinates within this eye's render region + // then convert to full SBS texture UV (adding eye offset) + float srcU = (u * (float)SrcEyeWidth + (float)SrcOffsetX) / (float)SrcWidth; + float srcV = (v * (float)SrcEyeHeight) / (float)SrcHeight; + + // Clamp sample UVs to per-eye texel bounds so the bilinear footprint and + // blur kernel can't reach across the SBS midline into the neighboring + // eye's pixels. + float2 eyeMinUV = float2(((float)SrcOffsetX + 0.5) / (float)SrcWidth, + 0.5 / (float)SrcHeight); + float2 eyeMaxUV = float2(((float)(SrcOffsetX + SrcEyeWidth) - 0.5) / (float)SrcWidth, + ((float)SrcEyeHeight - 0.5) / (float)SrcHeight); + + float4 color; + + if (StretchMode == 1) { + // Point / Nearest: integer texel lookup, cheapest. min() keeps us + // inside [0, SrcEyeWidth-1] / [0, SrcEyeHeight-1] when u/v == 1. + uint2 srcPixel = uint2( + min((uint)(u * (float)SrcEyeWidth), SrcEyeWidth - 1) + SrcOffsetX, + min((uint)(v * (float)SrcEyeHeight), SrcEyeHeight - 1)); + color = SrcTex.Load(int3(srcPixel, 0)); + } else if (StretchMode == 2) { + // Gaussian blur 3x3: 9-tap weighted average around center + float2 texelSize = float2(1.0 / (float)SrcWidth, 1.0 / (float)SrcHeight); + float2 center = float2(srcU, srcV); + float2 step = texelSize * BlurRadius; + + // Gaussian weights for 3x3 kernel (sigma ~ 0.85 * radius) + // Center=4, Edge=2, Corner=1, sum=16 + float4 sum = SrcTex.SampleLevel(BilinearSampler, clamp(center, eyeMinUV, eyeMaxUV), 0) * 4.0; + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(-step.x, 0), eyeMinUV, eyeMaxUV), 0) * 2.0; + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(step.x, 0), eyeMinUV, eyeMaxUV), 0) * 2.0; + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(0, -step.y), eyeMinUV, eyeMaxUV), 0) * 2.0; + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(0, step.y), eyeMinUV, eyeMaxUV), 0) * 2.0; + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(-step.x, -step.y), eyeMinUV, eyeMaxUV), 0); + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(step.x, -step.y), eyeMinUV, eyeMaxUV), 0); + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(-step.x, step.y), eyeMinUV, eyeMaxUV), 0); + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(step.x, step.y), eyeMinUV, eyeMaxUV), 0); + color = sum * (1.0 / 16.0); + } else { + // Bilinear (default): single hardware-filtered sample + color = SrcTex.SampleLevel(BilinearSampler, clamp(float2(srcU, srcV), eyeMinUV, eyeMaxUV), 0); + } + + // Debug visualizer: tint the cheap-stretched periphery red so the DLSS + // subrect (which BlendSubrectToOutput overwrites on top of us) reads as + // the un-tinted region. Lets users see at a glance where DLSS is actually + // reconstructing vs where the cheap stretch is filling. + if (DebugVisualize != 0) { + color.rgb = lerp(color.rgb, color.rgb * float3(1.6, 0.35, 0.35), 0.6); + } + + DstTex[uint2(tid.x + DstOffsetX, tid.y)] = color; +} diff --git a/src/Features/ScreenshotFeature.cpp b/src/Features/ScreenshotFeature.cpp index 8f6d445293..68ea6502c3 100644 --- a/src/Features/ScreenshotFeature.cpp +++ b/src/Features/ScreenshotFeature.cpp @@ -8,6 +8,7 @@ #include "Globals.h" #include "Menu.h" #include "Utils/FileSystem.h" +#include "Utils/Subrect.h" #include #include #include @@ -273,35 +274,6 @@ namespace combo[0].GetKey() == VK_SNAPSHOT; } - // Blend state used around the preview's ImGui::Image draw. Two regression - // risks if this is changed: - // 1. BlendEnable must stay FALSE - the source texture carries non-1 alpha - // where Skyrim composited UI plates; default SRC_ALPHA blend lets the - // host window background show through (visible on the desktop mirror). - // 2. WriteMask must exclude alpha (RGB only). In VR, Skyrim's menu UI - // shader recomposites our menu plate over the SBS framebuffer with - // alpha blending; writing texture alpha into the menu plate RT - // produces a cutout visible only through the HMD. RGB-only writes - // leave the plate's pre-cleared alpha=1 in place. - // Paired with ImDrawCallback_ResetRenderState queued by Subrect::DrawEditor - // immediately after the image draw. - void OpaquePreviewBlendCallback(const ImDrawList*, const ImDrawCmd*) - { - static winrt::com_ptr opaqueBlend; - if (!opaqueBlend) { - D3D11_BLEND_DESC desc{}; - desc.RenderTarget[0].BlendEnable = FALSE; - desc.RenderTarget[0].RenderTargetWriteMask = - D3D11_COLOR_WRITE_ENABLE_RED | - D3D11_COLOR_WRITE_ENABLE_GREEN | - D3D11_COLOR_WRITE_ENABLE_BLUE; - globals::d3d::device->CreateBlendState(&desc, opaqueBlend.put()); - } - if (opaqueBlend) { - globals::d3d::context->OMSetBlendState(opaqueBlend.get(), nullptr, 0xFFFFFFFF); - } - } - std::filesystem::path BuildScreenshotPath(const std::string& screenshotPath) { SYSTEMTIME st; @@ -429,7 +401,7 @@ void ScreenshotFeature::DrawSettings() } } - subrect.DrawEditor(previewView, src.texture, 1.0f, 0.0f, OpaquePreviewBlendCallback); + subrect.DrawEditor(previewView, src.texture, 1.0f, 0.0f, Util::Subrect::OpaquePreviewBlendCallback); } void ScreenshotFeature::EnsurePreviewCache(ID3D11Texture2D* sourceTexture) diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 32c0cd170c..c793eabc1b 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -4,9 +4,14 @@ #include "HDRDisplay.h" #include "Hooks.h" #include "State.h" -#include "Upscaling/PerfMode.h" #include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" +#include "Upscaling/FoveatedRender.h" +#include "Upscaling/FoveatedRender/Bridge.h" +#include "Upscaling/FoveatedRender/Core.h" +#include "Upscaling/FoveatedRender/Postprocess.h" +#include "Upscaling/FoveatedRender/Preprocess.h" +#include "Upscaling/PerfMode.h" #include "Upscaling/Streamline.h" #include "Utils/UI.h" #include @@ -272,7 +277,7 @@ void Upscaling::DrawSettings() // Derive scale from live `settings.qualityMode` — `resolution- // Scale` is locked to the PerfMode boot snapshot, so reusing it // here would mismatch the slider position the user sees. - const float displayScale = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)std::clamp(settings.qualityMode, 0u, 4u)); + const float displayScale = 1.0f / GetQualityModeRatio(settings.qualityMode); std::string labelWithScale = std::format("{} ( {:.2f}x )", baseLabel, displayScale); ImGui::SliderInt("Upscale Preset", (int*)&settings.qualityMode, 0, 4, labelWithScale.c_str()); @@ -286,7 +291,7 @@ void Upscaling::DrawSettings() const char* bootLabel = (upscaleMethod == UpscaleMethod::kDLSS) ? upscalePresetsDLSS[std::clamp(4 - (int)bm, 0, 4)] : upscalePresets[std::clamp(4 - (int)bm, 0, 4)]; Util::Text::RestartNeeded( "Pending restart: currently active = %s ( %.2fx ). Change applies after game restart.", - bootLabel, 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)bm)); + bootLabel, 1.0f / GetQualityModeRatio(bm)); } } @@ -472,6 +477,25 @@ void Upscaling::DrawSettings() ImGui::TreePop(); } + // FoveatedRender: foveated subrect DLSS — VR-only, opt-in mode of this + // feature. Like DLSSperf, lives here rather than as a peer Feature so + // all DLSS surfaces share one settings panel. Enable lives at the top + // level for discoverability; the body knobs are collapsed by default and + // greyed out until the user opts in. + if (globals::game::isVR) { + ImGui::Separator(); + foveatedRender.DrawEnable(); + const bool enabled = foveatedRender.settings.enabled != 0; + if (!enabled) + ImGui::BeginDisabled(); + if (ImGui::TreeNodeEx("Foveated DLSS — Tuning")) { + foveatedRender.DrawSettings(); + ImGui::TreePop(); + } + if (!enabled) + ImGui::EndDisabled(); + } + if (ImGui::TreeNodeEx("Backend Diagnostics")) { // Streamline log level selection const char* logLevels[] = { "Off", "Default", "Verbose" }; @@ -556,6 +580,11 @@ void Upscaling::DrawSettings() void Upscaling::SaveSettings(json& o_json) { o_json = settings; + // Nest FoveatedRender's settings under a sub-key so they round-trip alongside + // Upscaling's own. Subrect controller persistence is owned by FoveatedRender. + json foveatedRenderJson; + foveatedRender.SaveSettings(foveatedRenderJson); + o_json["foveatedRender"] = foveatedRenderJson; auto iniSettingCollection = globals::game::iniPrefSettingCollection; if (iniSettingCollection) { auto setting = iniSettingCollection->GetSetting("bUseTAA:Display"); @@ -567,6 +596,15 @@ void Upscaling::SaveSettings(json& o_json) void Upscaling::LoadSettings(json& o_json) { + // Pull FoveatedRender's nested block first so its absence doesn't fail the + // outer settings deserialize. FoveatedRender::ClampSettings touches sibling + // presetDLSS (cross-feature compat), so re-run it after `settings = o_json` + // below — otherwise the JSON re-assign overwrites the clamp and an + // incompatible preset slips through. (Copilot on PR #44.) + if (o_json.contains("foveatedRender")) { + foveatedRender.LoadSettings(o_json["foveatedRender"]); + o_json.erase("foveatedRender"); + } settings = o_json; // Sanitize loaded settings to ensure enum indices are valid @@ -587,6 +625,12 @@ void Upscaling::LoadSettings(json& o_json) logger::warn("[Upscaling] Loaded presetDLSS {} out of range, resetting to 0 (Default)", settings.presetDLSS); settings.presetDLSS = 0; } + // Re-apply FoveatedRender's cross-feature clamp now that the JSON + // re-assign above has overwritten anything it set during its own + // LoadSettings (which fired before this block ran). Idempotent — no-op + // if FoveatedRender is inactive or the preset is already compatible. + // (Copilot on PR #44.) + foveatedRender.ClampSettings(); const float originalReflexFPSLimit = settings.reflexFPSLimit; if (!std::isfinite(settings.reflexFPSLimit)) { settings.reflexFPSLimit = 60.0f; @@ -615,6 +659,7 @@ void Upscaling::LoadSettings(json& o_json) void Upscaling::RestoreDefaultSettings() { settings = {}; + foveatedRender.RestoreDefaultSettings(); } void Upscaling::DataLoaded() @@ -672,6 +717,10 @@ struct BSImageSpace_Init_FXAA }; void Upscaling::PostPostLoad() { + // Subrect controller defaults + stereo flag (FoveatedRender is no longer a + // Feature subclass so we drive its lifecycle from here). + foveatedRender.PostPostLoad(); + bool isGOG = !GetModuleHandle(L"steam_api64.dll"); stl::detour_thunk(REL::RelocationID(79947, 82084)); @@ -701,6 +750,19 @@ void Upscaling::PostPostLoad() logger::info("[Upscaling] Installed hooks"); } +float Upscaling::GetQualityModeRatio(uint qualityMode) +{ + // Lower bound is 0, not 1: qualityMode=0 is DLAA / NATIVEAA (1.0x — + // render at display resolution). The FfxFsr3QualityMode enum header + // doesn't *declare* a 0 value, but the implementation delegates to + // FfxFsr3UpscalerQualityMode which has NATIVEAA=0 → 1.0f. Clamping to + // 1 would force DLAA into Quality (1.5x) and shrink the rendered + // region of kMAIN to 67%. + const float ratio = ffxFsr3GetUpscaleRatioFromQualityMode( + static_cast(std::clamp(qualityMode, 0u, 4u))); + return std::isfinite(ratio) && ratio > 0.0f ? ratio : 3.0f; +} + Upscaling::UpscaleMethod Upscaling::GetUpscaleMethod() const { // Lock runtime to the boot upscaler under PerfMode — engine RTs are @@ -1342,7 +1404,7 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) // Boot qualityMode under PerfMode so projection stays coherent // with the engine RTs sized at install. const uint32_t qm = globals::features::upscaling.perfMode.IsHookActive() ? bootSnapshot.Boot(&Settings::qualityMode) : settings.qualityMode; - float resolutionScaleBase = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)qm); + float resolutionScaleBase = 1.0f / GetQualityModeRatio(qm); auto renderWidth = static_cast(screenWidth * resolutionScaleBase); auto renderHeight = static_cast(screenHeight * resolutionScaleBase); @@ -1480,6 +1542,7 @@ void Upscaling::SetupResources() void Upscaling::ClearShaderCache() { + foveatedRender.ClearShaderCache(); for (int i = 0; i < 5; ++i) { encodeTexturesCS[i] = nullptr; // com_ptr automatically releases } @@ -1909,7 +1972,42 @@ void Upscaling::Upscale() logger::debug("[Upscaling] LoadingMenu close detected — rebuilding DLSS feature"); streamline.DestroyDLSSResources(); } - streamline.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVectorCopyTexture->resource.get()); + + // PR-3 MVP-B: opt-in FoveatedRender route. When active, runs the + // per-eye DLSS dispatch with optional foveal subrect through + // FoveatedRenderImpl::Core; falls through to dev's standard path on + // any failure so users always see DLSS output (graceful + // degradation — no black frames if the enhancer preflights bad). + // + // Menu-skip: in menus the world stops producing fresh motion + // vectors and depth, but kMAIN keeps changing (UI plate composites). + // The route's subrect DLSS evaluate then accumulates temporal + // history against stale neighbourhood data and the subrect region + // renders as visible reconstruction garbage. Standard full-eye DLSS + // (the fall-through below) is robust to this because it reconstructs + // across the whole image — the foveated crop is what makes the + // stale-history bleed visible. Same menu-open predicate dev uses + // at Upscaling.cpp:1748 for ShouldUseFrameGenerationThisFrame. + auto* ui = globals::game::ui; + auto* st = globals::state; + const bool menuOpen = (ui && ui->GameIsPaused()) || (st && st->IsMainOrLoadingMenuOpen(ui)); + bool routeHandled = false; + if (FoveatedRenderImpl::Bridge::IsRouteActive() && globals::game::isVR && !menuOpen) { + if (FoveatedRenderImpl::Preprocess::EncodeUpscalingTextures(*this)) { + routeHandled = FoveatedRenderImpl::Core::ExecuteVRDlssCore(streamline, + main.texture, + globals::game::renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].texture, + reactiveMaskTexture->resource.get(), + transparencyCompositionMaskTexture->resource.get(), + motionVectorCopyTexture->resource.get()); + if (!routeHandled) { + logger::warn("[FOVEATED] route preflight failed — falling through to standard DLSS path"); + } + } + } + if (!routeHandled) { + streamline.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVectorCopyTexture->resource.get()); + } } else if (upscaleMethod == UpscaleMethod::kFSR) { // PerfMode bridge: when the engine RTs are shrunk to renderRes, FSR's displayRes // output must land in perfMode.testTexture (the private displayRes target used for @@ -2313,8 +2411,19 @@ void Upscaling::Main_PostProcessing::thunk(RE::ImageSpaceManager* a_this, uint32 upscaling.UpscaleDepth(); } - if (upscaleMethod == UpscaleMethod::kDLSS) - upscaling.ApplySharpening(); + if (upscaleMethod == UpscaleMethod::kDLSS) { + // FoveatedRender's DLSS output doesn't land in sharpenerTexture the + // way dev's path does (the route writes to its own per-eye intermediates + // and copies back to kMAIN/testTexture), so dev's zero-copy + // ApplySharpening can't read sharpenerTexture. Route through + // Postprocess::ApplyDlssSharpening which does the kMAIN → sharpener → + // kMAIN round-trip. Both paths honor sharpnessDLSS=0 to disable RCAS. + if (FoveatedRenderImpl::Bridge::IsRouteActive()) { + FoveatedRenderImpl::Postprocess::ApplyDlssSharpening(upscaling); + } else { + upscaling.ApplySharpening(); + } + } auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); GET_INSTANCE_MEMBER(BSImagespaceShaderISTemporalAA, imageSpaceManager); diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index 9fbdcf47a4..d84b422854 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -1,9 +1,10 @@ #pragma once #include "Feature.h" -#include "Upscaling/PerfMode.h" #include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" +#include "Upscaling/FoveatedRender.h" +#include "Upscaling/PerfMode.h" #include "Upscaling/RCAS/RCAS.h" #include "Upscaling/Streamline.h" #include "Utils/BootSnapshot.h" @@ -160,6 +161,16 @@ struct Upscaling : Feature UpscaleMethod GetUpscaleMethod() const; + /// Render-to-display scale ratio for a quality mode index + /// (1=Quality, 2=Balanced, 3=Performance, 4=UltraPerformance). + /// Single source of truth across DLSS, FSR, and FoveatedRender paths: + /// the four "quality preset" ratios (1.5/1.7/2.0/3.0) are aligned across + /// DLSS and FSR3 by NVIDIA's DLSS Programming Guide and FFX's + /// FfxFsr3QualityMode enum, so all upscalers in this plugin route their + /// quality lookups through here rather than duplicating the table. Returns + /// 3.0 (UltraPerformance) on out-of-range input. + static float GetQualityModeRatio(uint qualityMode); + void CheckResources(UpscaleMethod a_upscalemethod); void CreateUpscalingTextureResources(UpscaleMethod a_upscalemethod); void DestroyUpscalingTextureResources(UpscaleMethod a_upscalemethod); @@ -234,8 +245,9 @@ struct Upscaling : Feature static inline Streamline streamline; static inline FidelityFX fidelityFX; ///< Only for frame generation static inline DX12SwapChain dx12SwapChain; - static inline RCAS rcas; ///< Standalone RCAS sharpening for DLSS - static inline PerfMode perfMode; ///< VR-only: render engine at upscaled-render res + static inline RCAS rcas; ///< Standalone RCAS sharpening for DLSS + static inline PerfMode perfMode; ///< VR-only: render engine at upscaled-render res + static inline FoveatedRender foveatedRender; ///< VR-only: foveated subrect DLSS winrt::com_ptr copyDepthToSharedBufferPS; diff --git a/src/Features/Upscaling/FoveatedRender.cpp b/src/Features/Upscaling/FoveatedRender.cpp new file mode 100644 index 0000000000..52cc6cfbfc --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender.cpp @@ -0,0 +1,323 @@ +#include "FoveatedRender.h" + +#include "../../Globals.h" +#include "../../Utils/Subrect.h" +#include "../../Utils/UI.h" +#include "../Upscaling.h" +#include "FoveatedRender/Core.h" + +#include + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + FoveatedRender::Settings, + enabled, + dlssMode, + stretchMode, + debugVisualize); + +// ============================================================================ +// Lifecycle +// ============================================================================ + +void FoveatedRender::PostPostLoad() +{ + // Opt into PR-1's stereo extension so the controller tracks a separate + // right-eye UV (HMD nose-side overlap symmetry). + subrectController.SetStereoEnabled(true); + + // Seed sensible foveal presets. Empty-case only — user edits persist. + // "Center N%" presets are symmetric per eye (no rightUV → auto-mirror, which + // for centered UVs produces an identical right-eye UV). "Nasal Convergence" + // is asymmetric: left eye biased toward its right edge, right eye biased + // toward its left edge — both targeting the nose-side region where HMD + // binocular fusion is strongest, so DLSS reconstruction lands in the actual + // stereo overlap zone rather than diverging left/right fields. + subrectController.SeedDefaultPresets({ + { .name = "Full Eye", .uv = { 0.0f, 0.0f, 1.0f, 1.0f } }, + { .name = "Center 75%", .uv = { 0.125f, 0.125f, 0.75f, 0.75f } }, + { .name = "Center 50%", .uv = { 0.25f, 0.25f, 0.5f, 0.5f } }, + { .name = "Nasal Convergence 50%", + .uv = { 0.5f, 0.25f, 0.5f, 0.5f }, + .rightUV = Util::Subrect::UVRegion{ 0.0f, 0.25f, 0.5f, 0.5f } }, + }); +} + +void FoveatedRender::ClearShaderCache() +{ + FoveatedRenderImpl::Core::ClearShaderCache(); +} + +// ============================================================================ +// Settings I/O — driven from Upscaling::Save/LoadSettings under a nested key +// ============================================================================ + +void FoveatedRender::SaveSettings(json& o_json) +{ + o_json = settings; + subrectController.SaveSettings(o_json); +} + +void FoveatedRender::LoadSettings(const json& o_json) +{ + settings = o_json; + // Util::Subrect::Controller::LoadSettings takes `const json&` (Subrect.h:68) + // so no const_cast is needed — keeping it would imply mutation that never + // happens. (Copilot + CodeRabbit on PR #44.) + subrectController.LoadSettings(o_json); + ClampSettings(); +} + +void FoveatedRender::RestoreDefaultSettings() +{ + settings = {}; + ClampSettings(); +} + +void FoveatedRender::ClampSettings() +{ + settings.enabled = std::min(settings.enabled, 1u); + settings.dlssMode = std::min(settings.dlssMode, 1u); + settings.stretchMode = std::min(settings.stretchMode, 2u); + settings.debugVisualize = std::min(settings.debugVisualize, 1u); + // Preset clamping reads from Upscaling::Settings now. + auto& sharedPreset = globals::features::upscaling.settings.presetDLSS; + sharedPreset = std::min(sharedPreset, 5u); + if (!IsPresetCompatibleWithMode(sharedPreset)) { + sharedPreset = 3; // Fall back to L + } +} + +// ============================================================================ +// Activation + accessors +// ============================================================================ + +bool FoveatedRender::IsActive() const +{ + return enabledAtBoot && IsRuntimeSupported(); +} + +bool FoveatedRender::IsRuntimeSupported() const +{ + return globals::game::isVR && globals::features::upscaling.streamline.featureDLSS; +} + +void FoveatedRender::LatchQualityMode() +{ + qualityModeAtBoot = std::clamp(globals::features::upscaling.settings.qualityMode, 1u, 4u); +} + +uint FoveatedRender::GetActiveQualityMode() const +{ + return std::clamp(globals::features::upscaling.settings.qualityMode, 1u, 4u); +} + +uint FoveatedRender::GetActivePresetDLSS() const +{ + return std::min(globals::features::upscaling.settings.presetDLSS, 5u); +} + +float FoveatedRender::GetActiveSharpnessDLSS() const +{ + return std::clamp(globals::features::upscaling.settings.sharpnessDLSS, 0.0f, 1.0f); +} + +float FoveatedRender::GetRenderScaleForQuality(uint qualityMode) +{ + return Upscaling::GetQualityModeRatio(qualityMode); +} + +bool FoveatedRender::IsPresetCompatibleWithMode(uint presetIndex) const +{ + // Preset indices: 0=Default, 1=J, 2=K, 3=L, 4=M, 5=F + // Faster mode: J(1) and K(2) are incompatible. + if (GetDlssMode() == DlssMode::kFaster) { + return presetIndex != 1 && presetIndex != 2; + } + return true; +} + +void FoveatedRender::ClampPresetToMode() +{ + auto& sharedPreset = globals::features::upscaling.settings.presetDLSS; + if (!IsPresetCompatibleWithMode(sharedPreset)) { + sharedPreset = 3; // Fall back to L + } +} + +// ============================================================================ +// UI — FoveatedRender-specific knobs only. Quality / sharpness / preset / +// Streamline log level live on Upscaling's panel and apply to both DLSS paths. +// Called from Upscaling::DrawSettings inside a TreeNode. +// ============================================================================ + +void FoveatedRender::DrawEnable() +{ + ClampSettings(); + + ImGui::TextWrapped( + "Foveated subrect DLSS: only the user-selected region gets full DLSS upscaling, " + "the periphery is cheaply stretched. Significant DLSS cost reduction at the cost " + "of peripheral sharpness. VR + DLSS only."); + + const bool runtimeSupported = IsRuntimeSupported(); + if (!runtimeSupported) { + settings.enabled = 0; + } + + if (!runtimeSupported) + ImGui::BeginDisabled(); + bool enabledBool = settings.enabled != 0; + if (ImGui::Checkbox("Enable Foveated DLSS", &enabledBool)) + settings.enabled = enabledBool ? 1u : 0u; + if (!runtimeSupported) + ImGui::EndDisabled(); + + if ((settings.enabled != 0) != enabledAtBoot) { + Util::Text::RestartNeeded("Pending restart: FoveatedRender will %s on next launch.", + settings.enabled ? "enable" : "disable"); + } + + if (enabledAtBoot) { + Util::Text::WrappedInfo("Active: upscaling is forced to DLSS while enabled."); + } + + if (!globals::game::isVR) { + Util::Text::Warning("VR only. Non-VR / FSR support pending future contributors."); + } + if (globals::game::isVR && !globals::features::upscaling.streamline.featureDLSS) { + Util::Text::Warning("DLSS runtime not available. Enable is blocked."); + } +} + +void FoveatedRender::DrawSettings() +{ + static const char* dlssModes[] = { "Default", "Faster" }; + static const char* stretchModes[] = { "Bilinear", "Point", "Gaussian Blur" }; + + ClampSettings(); + + Util::Text::WrappedInfo("Quality / Sharpness / DLSS Preset / Streamline log level are shared with the standard DLSS path above."); + + // ── VR-only knobs ── + if (globals::game::isVR) { + ImGui::Separator(); + ImGui::Text("VR DLSS Mode"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Default vs Faster: trade per-eye image quality for setup cost. Switch only when you\n" + "can see a difference in your scene — otherwise prefer Faster.\n" + "\n" + "Default — use when: image quality matters more than the small overhead — cinematic\n" + "scenes, screenshot/recording, or if you notice ghosting/edge artifacts in Faster.\n" + "Each eye gets isolated per-eye intermediates for color/depth/MV/reactive/transparency\n" + "so DLSS can't sample across the SBS midline. Costs five per-eye copies per frame.\n" + "All DLSS presets (Default, J, K, L, M, F) supported.\n" + "\n" + "Faster — use when: you want the cheapest foveated path and aren't seeing artifacts —\n" + "fast-motion gameplay, exploration, anywhere small quality losses go unnoticed.\n" + "DLSS reads kMAIN directly via extent offsets, so bilinear sampling can touch 1-2\n" + "texels of the neighboring eye near the SBS midline. We snapshot kMAIN once and\n" + "clear the HMD hidden-area ring to prevent sky-blue bleed on fast head motion.\n" + "Presets J and K are unavailable — switching here auto-clamps preset to L."); + } + + uint prevMode = settings.dlssMode; + ImGui::SliderInt("DLSS Mode", reinterpret_cast(&settings.dlssMode), 0, 1, dlssModes[settings.dlssMode]); + if (settings.dlssMode != prevMode) { + const uint prevPreset = globals::features::upscaling.settings.presetDLSS; + ClampPresetToMode(); + if (globals::features::upscaling.settings.presetDLSS != prevPreset) { + logger::info("[FOVEATED] DLSS preset clamped from {} to {} after Faster switch (J/K incompatible)", + prevPreset, globals::features::upscaling.settings.presetDLSS); + } + } + switch (GetDlssMode()) { + case DlssMode::kDefault: + ImGui::TextWrapped("Per-eye isolation: 2 resource sets, 2 DLSS evaluates."); + break; + case DlssMode::kFaster: + ImGui::TextWrapped("SBS viewport: 1 snapshot + 2 mask clears, 2 evaluates. Presets J/K unavailable."); + break; + } + + ImGui::Separator(); + ImGui::Text("Background Stretch"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "How the cheap periphery is reconstructed to fill the area outside the DLSS subrect.\n" + "This is the cost-saving step — DLSS only runs on the subrect, the rest is filled by\n" + "this cheaper pass. Only affects pixels outside your selected region."); + } + ImGui::SliderInt("Stretch Mode", reinterpret_cast(&settings.stretchMode), 0, 2, stretchModes[settings.stretchMode]); + switch (GetStretchMode()) { + case StretchMode::kBilinear: + ImGui::TextWrapped("Bilinear: clean linear upscale. Looks like a soft DLSS-Performance result."); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Use when: you want the periphery to look like a sensible low-quality reconstruction,\n" + "close to how DLSS-Performance would look. Default-ish choice.\n" + "\n" + "Visual artifact: typical bilinear softness — fine geometry in the periphery looks\n" + "slightly out of focus but not visibly stretched."); + } + break; + case StretchMode::kPoint: + ImGui::TextWrapped("Point (nearest-neighbor): cheapest. Visibly pixelated periphery."); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Use when: you want the smallest possible cost in the periphery and don't mind\n" + "obvious pixelation outside your gaze region. Useful for benchmarking the upper\n" + "bound of foveated savings.\n" + "\n" + "Visual artifact: chunky pixel blocks in the periphery, very visible if you look\n" + "away from the subrect center."); + } + break; + case StretchMode::kGaussianBlur: + ImGui::TextWrapped("Gaussian blur: softens periphery further. Hides upscale artifacts behind blur."); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Use when: you want the periphery to fall away into soft focus — closer to how\n" + "natural human peripheral vision feels. Good default for actual foveated use.\n" + "\n" + "Visual artifact: noticeable blur in the periphery. If your subrect is large this\n" + "is barely visible; if small, the blur is the dominant visual signal."); + } + break; + } + + ImGui::Separator(); + ImGui::Text("Subrect Region"); + ImGui::TextWrapped( + "Drag in the preview below to select the region that gets full DLSS upscaling. " + "The rest is cheaply stretched — saves significant DLSS cost."); + Util::Text::WrappedInfo("Screenshot has its own subrect; align them only if you want pixel-matched captures."); + + bool debugBool = settings.debugVisualize != 0; + if (ImGui::Checkbox("Visualize regions", &debugBool)) + settings.debugVisualize = debugBool ? 1u : 0u; + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Diagnostic: tint the cheap-stretched periphery red so the DLSS-reconstructed\n" + "subrect (un-tinted) pops visually in-game. Lets you confirm at a glance where\n" + "DLSS is actually running vs where the cheap stretch is filling. No perf impact;\n" + "runtime toggle, no restart needed."); + } + + // Preview off kVR_FRAMEBUFFER (the final composed SBS image the headset + // sees) rather than kMAIN. kMAIN is mid-pipeline and carries non-1 + // alpha where Skyrim composited UI plates, so even with the opaque + // blend callback you see the menu mask outline instead of the rendered + // world. ScreenshotFeature picks the same RT for the same reason + // (ScreenshotFeature.cpp:243). Foveated is VR-only so kVR_FRAMEBUFFER + // is always populated when we get here. + auto renderer = globals::game::renderer; + if (renderer) { + auto& fb = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kVR_FRAMEBUFFER]; + auto* tex = static_cast(fb.texture); + subrectController.DrawEditor(fb.SRV, tex, 0.5f, 0.0f, Util::Subrect::OpaquePreviewBlendCallback); + } else { + subrectController.DrawEditor(nullptr, nullptr, 0.5f); + } + } +} diff --git a/src/Features/Upscaling/FoveatedRender.h b/src/Features/Upscaling/FoveatedRender.h new file mode 100644 index 0000000000..0d7ee3b62c --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender.h @@ -0,0 +1,111 @@ +#pragma once + +// ============================================================================ +// FoveatedRender — VR DLSS enhancement mode of Upscaling +// ============================================================================ +// +// Foveated subrect-DLSS path: only the user-selected region gets full DLSS +// upscaling; the periphery is cheaply stretched via SubrectStretchCS. Halves +// (or more) the DLSS workload. Composes with VRS, Screenshot, and the lossless +// recording feature through the shared Util::Subrect module — use the same +// preset for consistent results across them. +// +// Architecturally a mode inside Upscaling (mirroring DLSSperf): a static- +// inline member, not a peer Feature. Settings that overlap with Upscaling's +// (quality mode, sharpness, DLSS preset, Streamline log level) read directly +// from `globals::features::upscaling.settings` rather than being duplicated. +// VR + DLSS only at present; non-VR / FSR extension is left to future work. +// +// ============================================================================ + +#include "../../Utils/Subrect.h" + +struct FoveatedRender +{ + // DLSS execution mode for VR + enum class DlssMode : uint + { + kDefault = 0, // Per-eye isolation: 2 extra resource sets, 2 evaluates. Supports F/J/K/L/M. + kFaster = 1, // SBS viewport: tell SL to read subrect from SBS directly, no extra resources, 2 evaluates. J/K incompatible, only L/M/F. + }; + + // Stretch algorithm for DRS → full-eye background (used by SubrectStretchCS shader) + enum class StretchMode : uint + { + kBilinear = 0, // Default bilinear sampling (clean upscale) + kPoint = 1, // Nearest-neighbor / point (cheapest, VRS-like broadcast) + kGaussianBlur = 2, // 3x3 Gaussian blur (soft periphery) + }; + + // FoveatedRender-specific settings. Quality mode / sharpness / DLSS preset / + // Streamline log level live on Upscaling::Settings and are read through + // the accessors below — do not duplicate them here. Sharpening on/off is + // controlled by the shared sharpnessDLSS slider (0 disables RCAS). + // + // Deferred to PR-3b: per-input DLSS hint toggles (MV dilation, reactive mask, + // transparency mask). The original PR #2096 declared the Settings fields and + // UI sliders but never plumbed them to EncodeTexturesCS or to the EvaluateDLSS + // arg list, so they were no-ops there too. Bringing them back in PR-3b means + // shader permutations (per-toggle defines), conditional encode-pass skip when + // all are off, and per-toggle DLSS arg gating — ship the implementation and + // the UI together so the knobs don't lie. + struct Settings + { + uint enabled = 0; // opt-in: requires restart to take effect via LatchEnabled() + uint dlssMode = (uint)DlssMode::kDefault; + uint stretchMode = (uint)StretchMode::kGaussianBlur; + uint debugVisualize = 0; // tint cheap-stretched periphery red; runtime toggle + }; + + Settings settings; + Util::Subrect::Controller subrectController; + + // Called from Upscaling::DrawSettings. DrawEnable renders the always-visible + // header + Enable checkbox at the parent's top level; DrawSettings renders + // the body knobs inside a collapsible TreeNode (Upscaling wraps it in + // BeginDisabled when settings.enabled == 0). + void DrawEnable(); + void DrawSettings(); + // Called from Upscaling::SaveSettings / LoadSettings to round-trip JSON. + void SaveSettings(json& o_json); + void LoadSettings(const json& o_json); + void RestoreDefaultSettings(); + void ClearShaderCache(); + // Called from Upscaling::PostPostLoad to seed subrect presets. + void PostPostLoad(); + + bool IsRuntimeSupported() const; + bool IsActive() const; + bool IsLoaded() const { return enabledAtBoot; } + + // Main enable: latched at boot, change requires restart + void LatchEnabled() { enabledAtBoot = (settings.enabled != 0); } + + // Quality mode reads through Upscaling::Settings — latch the boot value so + // downstream RT allocations stay coherent if the user moves the slider. + void LatchQualityMode(); + uint GetQualityModeAtBoot() const { return qualityModeAtBoot; } + + /// Render-to-display scale denominator for a quality mode index + /// (1=Quality .. 4=UltraPerformance). Delegates to the FFX SDK ratio table. + static float GetRenderScaleForQuality(uint qualityMode); + + DlssMode GetDlssMode() const { return (DlssMode)std::min(settings.dlssMode, 1u); } + StretchMode GetStretchMode() const { return (StretchMode)std::min(settings.stretchMode, 2u); } + + // Active getters: clamp + route shared fields through Upscaling::Settings. + uint GetActiveQualityMode() const; + uint GetActivePresetDLSS() const; + float GetActiveSharpnessDLSS() const; + + // Re-clamp cross-feature settings (preset vs DLSS mode). Idempotent; safe to call + // from Upscaling::LoadSettings after JSON has overwritten shared fields. + void ClampSettings(); + +private: + bool enabledAtBoot = false; // latched from settings.enabled at boot + uint qualityModeAtBoot = 4; // latched from Upscaling::Settings::qualityMode at boot + + bool IsPresetCompatibleWithMode(uint presetIndex) const; + void ClampPresetToMode(); +}; diff --git a/src/Features/Upscaling/FoveatedRender/Bridge.cpp b/src/Features/Upscaling/FoveatedRender/Bridge.cpp new file mode 100644 index 0000000000..bc4309c9df --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Bridge.cpp @@ -0,0 +1,79 @@ +#include "Bridge.h" + +#include "../../../Globals.h" +#include "../../Upscaling.h" +#include "../FoveatedRender.h" + +bool FoveatedRenderImpl::Bridge::IsRouteActive() +{ + // IsActive() already checks: globals::game::isVR + // && globals::features::upscaling.streamline.featureDLSS + // && enabledAtBoot + return globals::features::upscaling.foveatedRender.IsActive(); +} + +// Bridge.h contract: when the route is inactive, getters return a neutral / +// identity value so callers that forget to check IsRouteActive() don't +// silently pick up FoveatedRender values. + +uint32_t FoveatedRenderImpl::Bridge::GetQualityMode() +{ + if (!IsRouteActive()) + return 0u; + return globals::features::upscaling.foveatedRender.GetActiveQualityMode(); +} + +uint32_t FoveatedRenderImpl::Bridge::GetPresetDLSS() +{ + if (!IsRouteActive()) + return 0u; + return globals::features::upscaling.foveatedRender.GetActivePresetDLSS(); +} + +float FoveatedRenderImpl::Bridge::GetSharpnessDLSS() +{ + if (!IsRouteActive()) + return 0.0f; + return globals::features::upscaling.foveatedRender.GetActiveSharpnessDLSS(); +} + +void FoveatedRenderImpl::Bridge::BootSequence() +{ + auto& enhancer = globals::features::upscaling.foveatedRender; + enhancer.LatchEnabled(); + enhancer.LatchQualityMode(); +} + +void FoveatedRenderImpl::Bridge::ComputeMvecScale(float& outX, float& outY) +{ + // Default: identity (caller's normal Streamline path). + outX = 1.0f; + outY = 1.0f; + + if (!IsRouteActive()) + return; + + auto& enhancer = globals::features::upscaling.foveatedRender; + const auto& uv = enhancer.subrectController.GetUV(); // PR-1 stereo Subrect: GetUV() == left-eye in stereo mode + const bool isFullEye = (uv.w >= 0.999f && uv.h >= 0.999f); + + if (isFullEye) + return; + + // Default + Faster both use per-eye DLSS calls (not strip-merged), so + // motion vectors scale by 1/UV.w on x. + outX = (uv.w > 0.0f) ? (1.0f / uv.w) : 1.0f; + outY = (uv.h > 0.0f) ? (1.0f / uv.h) : 1.0f; +} + +float FoveatedRenderImpl::Bridge::GetRenderScaleForQuality(uint32_t qualityMode) +{ + return FoveatedRender::GetRenderScaleForQuality(qualityMode); +} + +uint32_t FoveatedRenderImpl::Bridge::GetQualityModeAtBoot() +{ + if (!IsRouteActive()) + return 0u; + return globals::features::upscaling.foveatedRender.GetQualityModeAtBoot(); +} diff --git a/src/Features/Upscaling/FoveatedRender/Bridge.h b/src/Features/Upscaling/FoveatedRender/Bridge.h new file mode 100644 index 0000000000..dd408dfc2c --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Bridge.h @@ -0,0 +1,42 @@ +#pragma once + +// FoveatedRenderImpl::Bridge — single point of contact between the FoveatedRender +// subsystem and the rest of Community Shaders (Upscaling, Streamline). +// +// All "is FoveatedRender active?", "what settings should DLSS use?", and +// "what happened at boot?" questions are answered here, so consumers never +// need to #include FoveatedRender.h or poke globals::features::upscaling.foveatedRender +// directly. +// +// IMPORTANT: when the FoveatedRender route is inactive every query returns a +// neutral / identity value — callers must still check IsRouteActive() and +// fall back to their own settings when it returns false. + +#include + +namespace FoveatedRenderImpl::Bridge +{ + // True when VR + DLSS available + FoveatedRender enabled-at-boot. + bool IsRouteActive(); + + // Settings forwarding (live values from FoveatedRender GUI). + uint32_t GetQualityMode(); + uint32_t GetPresetDLSS(); + float GetSharpnessDLSS(); + + // Boot-time latches. Run once during BSShaderRenderTargets::Create. + // Latches enable + qualityMode so settings cannot drift mid-frame. + void BootSequence(); + + // Compute motion-vector scale for Streamline constants. + // Returns {1,1} when route is inactive or subrect is full-eye. + void ComputeMvecScale(float& outX, float& outY); + + // Render-to-display scale for a quality mode index (1=Quality .. 4=UltraPerf). + // Delegates to the FFX SDK ratio table. + float GetRenderScaleForQuality(uint32_t qualityMode); + + // Quality mode latched at boot (resource sizing decisions consult this so + // they don't shift mid-game when the user changes the live setting). + uint32_t GetQualityModeAtBoot(); +} diff --git a/src/Features/Upscaling/FoveatedRender/Core.cpp b/src/Features/Upscaling/FoveatedRender/Core.cpp new file mode 100644 index 0000000000..78bd1ab1d3 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Core.cpp @@ -0,0 +1,581 @@ +#include "Core.h" +#include "Ops.h" + +#include "../../../State.h" +#include "../../../Util.h" +#include "../../Upscaling.h" +#include "../FoveatedRender.h" + +#include + +namespace FoveatedRenderImpl::Ops +{ + // Mirrors the StretchCB layout in SubrectStretchCS.hlsl — 8 dims + mode + + // blur radius + debug flag + pad. Kept at namespace scope so the create-CB + // path can size against sizeof(StretchCB) instead of a magic number. + struct StretchCB + { + uint32_t data[8]; + uint32_t stretchMode; + float blurRadius; + uint32_t debugVisualize; + uint32_t pad; + }; + + eastl::unique_ptr CreateTextureFromSource(ID3D11Resource* src, uint32_t width, uint32_t height, + bool copyBindFlags, bool createSRV, bool createUAV, const char* name) + { + if (!src) { + logger::error("[FOVEATED] CreateTextureFromSource called with null src ({})", name ? name : ""); + return nullptr; + } + + // QueryInterface for ID3D11Texture2D rather than blind static_cast — a + // non-texture resource passed here would crash GetDesc otherwise. + // (CodeRabbit on PR #44.) + winrt::com_ptr srcTex; + if (FAILED(src->QueryInterface(IID_PPV_ARGS(srcTex.put())))) { + logger::error("[FOVEATED] CreateTextureFromSource src is not an ID3D11Texture2D ({})", name ? name : ""); + return nullptr; + } + + D3D11_TEXTURE2D_DESC srcDesc; + srcTex->GetDesc(&srcDesc); + + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = srcDesc.Format; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = copyBindFlags ? srcDesc.BindFlags : (D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + + auto tex = eastl::make_unique(desc); + + if (name) { + Util::SetResourceName(tex->resource.get(), name); + } + + if (createSRV) { + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = srcDesc.Format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MostDetailedMip = 0; + srvDesc.Texture2D.MipLevels = 1; + tex->CreateSRV(srvDesc); + } + + if (createUAV) { + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; + uavDesc.Format = srcDesc.Format; + uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; + uavDesc.Texture2D.MipSlice = 0; + tex->CreateUAV(uavDesc); + } + + return tex; + } + + void EnsureVRIntermediateTextures( + uint32_t inWidth, + uint32_t inHeight, + uint32_t outWidth, + uint32_t outHeight, + ID3D11Resource* colorSrc, + ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, + ID3D11Resource* transparencySrc) + { + bool needsRecreate = !Core::vrIntermediateColorIn[0] || !Core::vrIntermediateColorOut[0]; + if (!needsRecreate) { + needsRecreate = (Core::vrIntermediateColorIn[0]->desc.Width != inWidth || + Core::vrIntermediateColorIn[0]->desc.Height != inHeight || + Core::vrIntermediateColorOut[0]->desc.Width != outWidth || + Core::vrIntermediateColorOut[0]->desc.Height != outHeight); + } + // Recreate if reactive/transparency source appeared but intermediate is missing + if (!needsRecreate) { + needsRecreate = (reactiveSrc && !Core::vrIntermediateReactiveMask[0]) || + (transparencySrc && !Core::vrIntermediateTransparencyMask[0]); + } + + // Also reset stale intermediates when a source DISAPPEARED. Otherwise + // the per-eye reactive/transparency intermediates keep their last-known + // data and PreparePerEyeInputs's null-source branch skips the copy — + // DLSS then samples stale masks. Drop the intermediate so subsequent + // frames don't read it. Independent of the recreate path: shrinking is + // cheap and the next non-null source will trigger recreate above. + // (Copilot on PR #44.) + if (!reactiveSrc && Core::vrIntermediateReactiveMask[0]) { + Core::vrIntermediateReactiveMask[0].reset(); + Core::vrIntermediateReactiveMask[1].reset(); + } + if (!transparencySrc && Core::vrIntermediateTransparencyMask[0]) { + Core::vrIntermediateTransparencyMask[0].reset(); + Core::vrIntermediateTransparencyMask[1].reset(); + } + + if (!needsRecreate) { + return; + } + + for (int i = 0; i < 2; i++) { + std::string suffix = (i == 0) ? "Left" : "Right"; + + Core::vrIntermediateColorIn[i] = CreateTextureFromSource(colorSrc, inWidth, inHeight, false, true, true, ("FoveatedRender_ColorIn_" + suffix).c_str()); + Core::vrIntermediateColorOut[i] = CreateTextureFromSource(colorSrc, outWidth, outHeight, false, true, false, ("FoveatedRender_ColorOut_" + suffix).c_str()); + + D3D11_TEXTURE2D_DESC depthDesc = {}; + depthDesc.Width = inWidth; + depthDesc.Height = inHeight; + depthDesc.MipLevels = 1; + depthDesc.ArraySize = 1; + depthDesc.Format = DXGI_FORMAT_R32_TYPELESS; + depthDesc.SampleDesc.Count = 1; + depthDesc.Usage = D3D11_USAGE_DEFAULT; + depthDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + Core::vrIntermediateDepth[i] = eastl::make_unique(depthDesc); + Util::SetResourceName(Core::vrIntermediateDepth[i]->resource.get(), ("FoveatedRender_Depth_" + suffix).c_str()); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = DXGI_FORMAT_R32_FLOAT; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + Core::vrIntermediateDepth[i]->CreateSRV(srvDesc); + + Core::vrIntermediateMotionVectors[i] = CreateTextureFromSource(mvecSrc, inWidth, inHeight, false, true, false, ("FoveatedRender_MVec_" + suffix).c_str()); + if (reactiveSrc) + Core::vrIntermediateReactiveMask[i] = CreateTextureFromSource(reactiveSrc, inWidth, inHeight, false, true, false, ("FoveatedRender_Reactive_" + suffix).c_str()); + else + Core::vrIntermediateReactiveMask[i].reset(); + if (transparencySrc) + Core::vrIntermediateTransparencyMask[i] = CreateTextureFromSource(transparencySrc, inWidth, inHeight, false, true, false, ("FoveatedRender_Transparency_" + suffix).c_str()); + else + Core::vrIntermediateTransparencyMask[i].reset(); + } + } + + void EnsureVRSubrectTextures( + uint32_t subInW, + uint32_t subInH, + uint32_t subOutW, + uint32_t subOutH, + ID3D11Resource* colorSrc, + ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, + ID3D11Resource* transparencySrc) + { + bool needsRecreate = !Core::vrSubrectColorIn[0] || + Core::vrSubrectInW != subInW || Core::vrSubrectInH != subInH || + Core::vrSubrectOutW != subOutW || Core::vrSubrectOutH != subOutH; + // Recreate if reactive/transparency source appeared but intermediate is missing + if (!needsRecreate) { + needsRecreate = (reactiveSrc && !Core::vrSubrectReactiveMask[0]) || + (transparencySrc && !Core::vrSubrectTransparencyMask[0]); + } + + if (needsRecreate) { + for (int i = 0; i < 2; i++) { + std::string suffix = (i == 0) ? "Left" : "Right"; + Core::vrSubrectColorIn[i] = CreateTextureFromSource(colorSrc, subInW, subInH, false, true, true, ("FoveatedRender_Subrect_ColorIn_" + suffix).c_str()); + Core::vrSubrectColorOut[i] = CreateTextureFromSource(colorSrc, subOutW, subOutH, false, true, false, ("FoveatedRender_Subrect_ColorOut_" + suffix).c_str()); + + D3D11_TEXTURE2D_DESC depthDesc = {}; + depthDesc.Width = subInW; + depthDesc.Height = subInH; + depthDesc.MipLevels = 1; + depthDesc.ArraySize = 1; + depthDesc.Format = DXGI_FORMAT_R32_TYPELESS; + depthDesc.SampleDesc.Count = 1; + depthDesc.Usage = D3D11_USAGE_DEFAULT; + depthDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + Core::vrSubrectDepth[i] = eastl::make_unique(depthDesc); + Util::SetResourceName(Core::vrSubrectDepth[i]->resource.get(), ("FoveatedRender_Subrect_Depth_" + suffix).c_str()); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = DXGI_FORMAT_R32_FLOAT; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + Core::vrSubrectDepth[i]->CreateSRV(srvDesc); + + Core::vrSubrectMotionVectors[i] = CreateTextureFromSource(mvecSrc, subInW, subInH, false, true, false, ("FoveatedRender_Subrect_MVec_" + suffix).c_str()); + if (reactiveSrc) + Core::vrSubrectReactiveMask[i] = CreateTextureFromSource(reactiveSrc, subInW, subInH, false, true, false, ("FoveatedRender_Subrect_Reactive_" + suffix).c_str()); + else + Core::vrSubrectReactiveMask[i].reset(); + if (transparencySrc) + Core::vrSubrectTransparencyMask[i] = CreateTextureFromSource(transparencySrc, subInW, subInH, false, true, false, ("FoveatedRender_Subrect_Transparency_" + suffix).c_str()); + else + Core::vrSubrectTransparencyMask[i].reset(); + } + + Core::vrSubrectInW = subInW; + Core::vrSubrectInH = subInH; + Core::vrSubrectOutW = subOutW; + Core::vrSubrectOutH = subOutH; + } + } + + bool PreparePerEyeInputs( + ID3D11Resource* colorSrc, + ID3D11Resource* depthSrc, + ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, + ID3D11Resource* transparencySrc, + uint32_t eyeWidthIn, + uint32_t eyeHeightIn, + uint32_t eyeWidthOut, + uint32_t eyeHeightOut) + { + // Required sources are dereferenced unconditionally below; bail + // rather than null-deref CopySubresourceRegion. Reactive/transparency + // are optional and already conditionally copied. + if (!colorSrc || !depthSrc || !mvecSrc) { + logger::error("[FOVEATED] PreparePerEyeInputs missing required source textures"); + return false; + } + + EnsureVRIntermediateTextures( + eyeWidthIn, + eyeHeightIn, + eyeWidthOut, + eyeHeightOut, + colorSrc, + mvecSrc, + reactiveSrc, + transparencySrc); + + for (uint32_t i = 0; i < 2; ++i) { + if (!Core::vrIntermediateColorIn[i] || !Core::vrIntermediateColorOut[i] || + !Core::vrIntermediateDepth[i] || !Core::vrIntermediateMotionVectors[i] || + (reactiveSrc && !Core::vrIntermediateReactiveMask[i]) || + (transparencySrc && !Core::vrIntermediateTransparencyMask[i])) { + logger::error("[FOVEATED] Missing per-eye intermediate resources for eye {}", i); + return false; + } + } + + auto context = globals::d3d::context; + auto* depthSRV = globals::game::renderer->GetDepthStencilData() + .depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN] + .depthSRV; + for (uint32_t i = 0; i < 2; ++i) { + uint32_t offsetXIn = (i == 1) ? eyeWidthIn : 0; + D3D11_BOX srcBox = { offsetXIn, 0, 0, offsetXIn + eyeWidthIn, eyeHeightIn, 1 }; + + context->CopySubresourceRegion(Core::vrIntermediateColorIn[i]->resource.get(), 0, 0, 0, 0, colorSrc, 0, &srcBox); + context->CopySubresourceRegion(Core::vrIntermediateDepth[i]->resource.get(), 0, 0, 0, 0, depthSrc, 0, &srcBox); + context->CopySubresourceRegion(Core::vrIntermediateMotionVectors[i]->resource.get(), 0, 0, 0, 0, mvecSrc, 0, &srcBox); + if (transparencySrc) + context->CopySubresourceRegion(Core::vrIntermediateTransparencyMask[i]->resource.get(), 0, 0, 0, 0, transparencySrc, 0, &srcBox); + if (reactiveSrc) + context->CopySubresourceRegion(Core::vrIntermediateReactiveMask[i]->resource.get(), 0, 0, 0, 0, reactiveSrc, 0, &srcBox); + + // Reapply HMD hidden-area mask clear into the per-eye intermediate so DLSS + // history doesn't accumulate garbage from the masked-out region. + // Depth source is full SBS (read at per-eye offset); color destination is per-eye + // sized (write at offset 0). + globals::features::upscaling.ClearHMDMask( + Core::vrIntermediateColorIn[i]->uav.get(), + depthSRV, + eyeWidthIn, + eyeHeightIn, + i * eyeWidthIn, + 0); + } + + return true; + } + + bool FinalizePerEyeOutputs(ID3D11Resource* colorDst, uint32_t eyeWidthOut, uint32_t eyeHeightOut) + { + if (!colorDst) { + logger::error("[FOVEATED] FinalizePerEyeOutputs received null destination color resource"); + return false; + } + + for (uint32_t i = 0; i < 2; ++i) { + if (!Core::vrIntermediateColorOut[i]) { + logger::error("[FOVEATED] Missing per-eye output resource for eye {}", i); + return false; + } + } + + auto context = globals::d3d::context; + for (uint32_t i = 0; i < 2; ++i) { + uint32_t offsetXOut = (i == 1) ? eyeWidthOut : 0; + D3D11_BOX outBox = { 0, 0, 0, eyeWidthOut, eyeHeightOut, 1 }; + context->CopySubresourceRegion(colorDst, 0, offsetXOut, 0, 0, Core::vrIntermediateColorOut[i]->resource.get(), 0, &outBox); + } + + return true; + } + + void StretchDRSToFullEye( + ID3D11ShaderResourceView* renderSBSSRV, + ID3D11UnorderedAccessView* kMainUAV, + uint32_t dstOffsetX, + uint32_t dstWidth, + uint32_t dstHeight, + uint32_t srcOffsetX, + uint32_t srcWidth, + uint32_t srcHeight, + uint32_t srcEyeWidth, + uint32_t srcEyeHeight) + { + auto context = globals::d3d::context; + + if (!Core::vrSubrectStretchCS) { + Core::vrSubrectStretchCS.attach((ID3D11ComputeShader*)Util::CompileShader(L"Data/Shaders/Upscaling/FoveatedRender/SubrectStretchCS.hlsl", {}, "cs_5_0")); + Util::SetResourceName(Core::vrSubrectStretchCS.get(), "FoveatedRender::SubrectStretchCS"); + + D3D11_BUFFER_DESC cbDesc = {}; + cbDesc.ByteWidth = sizeof(StretchCB); + cbDesc.Usage = D3D11_USAGE_DYNAMIC; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + if (FAILED(globals::d3d::device->CreateBuffer(&cbDesc, nullptr, Core::vrSubrectStretchCB.put()))) { + logger::error("[FOVEATED] Failed to create SubrectStretch constant buffer"); + // Drop the partially-attached CS so the next frame retries the + // whole init block — otherwise the outer !vrSubrectStretchCS + // guard above stays false forever and Faster mode is dead for + // the rest of the session. + Core::vrSubrectStretchCS = nullptr; + return; + } + Util::SetResourceName(Core::vrSubrectStretchCB.get(), "FoveatedRender::SubrectStretchCB"); + + D3D11_SAMPLER_DESC sampDesc = {}; + sampDesc.Filter = D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT; + sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + if (FAILED(globals::d3d::device->CreateSamplerState(&sampDesc, Core::vrSubrectStretchSampler.put()))) { + logger::error("[FOVEATED] Failed to create SubrectStretch sampler"); + Core::vrSubrectStretchCS = nullptr; + Core::vrSubrectStretchCB = nullptr; + return; + } + Util::SetResourceName(Core::vrSubrectStretchSampler.get(), "FoveatedRender::SubrectStretchSampler"); + } + + if (!Core::vrSubrectStretchCS || !Core::vrSubrectStretchCB || !Core::vrSubrectStretchSampler) { + return; + } + + // Guard against a null destination UAV — CSSetUnorderedAccessViews + + // Dispatch with nullptr would either no-op silently or assert in + // debug builds. Returning lets the route's `routeHandled=false` path + // fall back to standard DLSS so users still see output. (CodeRabbit + // on PR #44.) + if (!kMainUAV) { + logger::error("[FOVEATED] StretchDRSToFullEye called with null kMainUAV"); + return; + } + + D3D11_MAPPED_SUBRESOURCE mapped{}; + // If Map fails the constant buffer keeps stale data from a prior + // dispatch. CSSetConstantBuffers + Dispatch would then run the + // shader against stale geometry/scale parameters, producing wrong + // pixels rather than no pixels. Early-return preserves the prior + // frame's output. (CodeRabbit on PR #44.) + if (FAILED(context->Map(Core::vrSubrectStretchCB.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + logger::error("[FOVEATED] StretchDRSToFullEye Map(vrSubrectStretchCB) failed; skipping dispatch"); + return; + } + { + auto& enhSettings = globals::features::upscaling.foveatedRender.settings; + StretchCB cb = {}; + cb.data[0] = dstOffsetX; + cb.data[1] = dstWidth; + cb.data[2] = dstHeight; + cb.data[3] = srcOffsetX; + cb.data[4] = srcWidth; + cb.data[5] = srcHeight; + cb.data[6] = srcEyeWidth; + cb.data[7] = srcEyeHeight; + cb.stretchMode = enhSettings.stretchMode; + // Fixed 1.0 blur radius for the GaussianBlur stretch path. + cb.blurRadius = 1.0f; + cb.debugVisualize = enhSettings.debugVisualize; + std::memcpy(mapped.pData, &cb, sizeof(cb)); + context->Unmap(Core::vrSubrectStretchCB.get(), 0); + } + + context->CSSetShader(Core::vrSubrectStretchCS.get(), nullptr, 0); + ID3D11Buffer* cbs[1] = { Core::vrSubrectStretchCB.get() }; + context->CSSetConstantBuffers(0, 1, cbs); + ID3D11ShaderResourceView* srvs[1] = { renderSBSSRV }; + context->CSSetShaderResources(0, 1, srvs); + ID3D11SamplerState* samplers[1] = { Core::vrSubrectStretchSampler.get() }; + context->CSSetSamplers(0, 1, samplers); + ID3D11UnorderedAccessView* uavs[1] = { kMainUAV }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + + context->Dispatch((dstWidth + 7) / 8, (dstHeight + 7) / 8, 1); + + ID3D11ShaderResourceView* nullSRV[1] = { nullptr }; + ID3D11UnorderedAccessView* nullUAV[1] = { nullptr }; + ID3D11Buffer* nullCB[1] = { nullptr }; + ID3D11SamplerState* nullSampler[1] = { nullptr }; + context->CSSetShaderResources(0, 1, nullSRV); + context->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr); + context->CSSetConstantBuffers(0, 1, nullCB); + context->CSSetSamplers(0, 1, nullSampler); + context->CSSetShader(nullptr, nullptr, 0); + } + + void EnsureVRRenderSBS(uint32_t renderW, uint32_t renderH, ID3D11Resource* colorSrc) + { + if (!Core::vrRenderSBS || Core::vrRenderSBSW != renderW || Core::vrRenderSBSH != renderH) { + // UAV is required for the Faster-mode HMD mask clear pass (ClearHMDMask + // writes through the UAV before DLSS reads the SBS via extent offsets). + Core::vrRenderSBS = CreateTextureFromSource(colorSrc, renderW, renderH, false, true, true, "FoveatedRender_RenderSBS"); + Core::vrRenderSBSW = renderW; + Core::vrRenderSBSH = renderH; + } + } + + void EnsureFasterOutputTextures(uint32_t subOutW, uint32_t subOutH, ID3D11Resource* colorSrc) + { + bool needsRecreate = !Core::vrFasterColorOut[0] || + Core::vrFasterOutW != subOutW || Core::vrFasterOutH != subOutH; + if (!needsRecreate) + return; + for (int i = 0; i < 2; i++) { + std::string suffix = (i == 0) ? "Left" : "Right"; + Core::vrFasterColorOut[i] = CreateTextureFromSource(colorSrc, subOutW, subOutH, false, true, false, ("FoveatedRender_Faster_ColorOut_" + suffix).c_str()); + } + Core::vrFasterOutW = subOutW; + Core::vrFasterOutH = subOutH; + } + + uint64_t ComputeSubrectUVHash(const Util::Subrect::UVRegion& leftUV, + const Util::Subrect::UVRegion& rightUV, uint32_t mode) + { + uint64_t h = 0; + auto mix = [&](uint64_t v) { h ^= v + 0x9e3779b97f4a7c15ULL + (h << 12) + (h >> 4); }; + auto mixUV = [&](const Util::Subrect::UVRegion& uv) { + mix(std::hash{}(uv.x)); + mix(std::hash{}(uv.y)); + mix(std::hash{}(uv.w)); + mix(std::hash{}(uv.h)); + }; + mixUV(leftUV); + mixUV(rightUV); + mix(std::hash{}(mode)); + return h; + } + + void SnapshotSBS(ID3D11Resource* src, uint32_t renderW, uint32_t renderH) + { + EnsureVRRenderSBS(renderW, renderH, src); + auto context = globals::d3d::context; + D3D11_BOX drsBox = { 0, 0, 0, renderW, renderH, 1 }; + context->CopySubresourceRegion(Core::vrRenderSBS->resource.get(), 0, 0, 0, 0, src, 0, &drsBox); + } + + void StretchDRSBothEyes(ID3D11UnorderedAccessView* dstUAV, uint32_t eyeWidthOut, uint32_t eyeHeightOut, + uint32_t eyeWidthIn, uint32_t eyeHeightIn, uint32_t renderW, uint32_t renderH, + ID3D11ShaderResourceView* srcOverride) + { + // Snapshot creation can fail or be skipped on a fresh frame; degrade + // rather than dereference vrRenderSBS->srv on the null path. + auto* src = srcOverride ? srcOverride : + (Core::vrRenderSBS ? Core::vrRenderSBS->srv.get() : nullptr); + if (!src) { + logger::error("[FOVEATED] StretchDRSBothEyes missing source SRV"); + return; + } + for (uint32_t i = 0; i < 2; ++i) { + uint32_t dstX = (i == 1) ? eyeWidthOut : 0; + uint32_t srcX = (i == 1) ? eyeWidthIn : 0; + StretchDRSToFullEye( + src, dstUAV, + dstX, eyeWidthOut, eyeHeightOut, + srcX, renderW, renderH, + eyeWidthIn, eyeHeightIn); + } + } + + void BlendSubrectToOutput(ID3D11Resource* dlssSrc, ID3D11Resource* dst, + uint32_t dstOffsetX, uint32_t dstOffsetY, uint32_t subWidth, uint32_t subHeight, uint32_t srcOffsetX) + { + auto context = globals::d3d::context; + D3D11_BOX srcBox = { srcOffsetX, 0, 0, srcOffsetX + subWidth, subHeight, 1 }; + context->CopySubresourceRegion(dst, 0, dstOffsetX, dstOffsetY, 0, dlssSrc, 0, &srcBox); + } + +} // namespace FoveatedRenderImpl::Ops + +namespace FoveatedRenderImpl +{ + bool Core::PrepareVRPerEyeInputs( + ID3D11Resource* colorSrc, + ID3D11Resource* depthSrc, + ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, + ID3D11Resource* transparencySrc, + uint32_t eyeWidthIn, + uint32_t eyeHeightIn, + uint32_t eyeWidthOut, + uint32_t eyeHeightOut) + { + return Ops::PreparePerEyeInputs( + colorSrc, + depthSrc, + mvecSrc, + reactiveSrc, + transparencySrc, + eyeWidthIn, + eyeHeightIn, + eyeWidthOut, + eyeHeightOut); + } + + bool Core::FinalizeVRPerEyeOutputs( + ID3D11Resource* colorDst, + uint32_t eyeWidthOut, + uint32_t eyeHeightOut) + { + return Ops::FinalizePerEyeOutputs(colorDst, eyeWidthOut, eyeHeightOut); + } + + void Core::ClearResources() + { + for (int i = 0; i < 2; ++i) { + vrIntermediateColorIn[i].reset(); + vrIntermediateColorOut[i].reset(); + vrIntermediateDepth[i].reset(); + vrIntermediateMotionVectors[i].reset(); + vrIntermediateReactiveMask[i].reset(); + vrIntermediateTransparencyMask[i].reset(); + + vrSubrectColorIn[i].reset(); + vrSubrectColorOut[i].reset(); + vrSubrectDepth[i].reset(); + vrSubrectMotionVectors[i].reset(); + vrSubrectReactiveMask[i].reset(); + vrSubrectTransparencyMask[i].reset(); + } + vrSubrectInW = vrSubrectInH = vrSubrectOutW = vrSubrectOutH = 0; + + vrRenderSBS.reset(); + vrRenderSBSW = vrRenderSBSH = 0; + + vrFasterColorOut[0].reset(); + vrFasterColorOut[1].reset(); + vrFasterOutW = vrFasterOutH = 0; + + activeSubrectUVHash = 0; + } + + void Core::ClearShaderCache() + { + vrSubrectStretchCS = nullptr; + vrSubrectStretchCB = nullptr; + vrSubrectStretchSampler = nullptr; + } +} diff --git a/src/Features/Upscaling/FoveatedRender/Core.h b/src/Features/Upscaling/FoveatedRender/Core.h new file mode 100644 index 0000000000..7b4a04e869 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Core.h @@ -0,0 +1,93 @@ +#pragma once + +// ============================================================================ +// FoveatedRenderImpl::Core — GPU resource pool & mode-dispatch entry point +// ============================================================================ +// +// Owns all per-mode intermediate textures (Default / Faster), compute-shader +// objects (subrect stretch), and the public entry points consumed by +// Upscaling.cpp. +// +// ============================================================================ + +#include "Buffer.h" +#include "Params.h" +#include +#include + +class Streamline; + +namespace FoveatedRenderImpl +{ + class Core + { + public: + // Stage1: dispatches across Default / Faster modes. + static bool ExecuteVRDlssCore(Streamline& streamline, + ID3D11Resource* upscalingTexture, + ID3D11Resource* depthTexture, + ID3D11Resource* reactiveMask, + ID3D11Resource* transparencyMask, + ID3D11Resource* motionVectors); + + // Shared VR per-eye preprocessing/finalization for non-DLSS callers (e.g. FSR). + static bool PrepareVRPerEyeInputs( + ID3D11Resource* colorSrc, + ID3D11Resource* depthSrc, + ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, + ID3D11Resource* transparencySrc, + uint32_t eyeWidthIn, + uint32_t eyeHeightIn, + uint32_t eyeWidthOut, + uint32_t eyeHeightOut); + + static bool FinalizeVRPerEyeOutputs( + ID3D11Resource* colorDst, + uint32_t eyeWidthOut, + uint32_t eyeHeightOut); + + // Release all GPU resources owned by Core. + static void ClearResources(); + static void ClearShaderCache(); + + // ── Own VR resources (independent from Upscaling) ── + + // Per-eye intermediate buffers (Default full-eye mode) + static inline eastl::unique_ptr vrIntermediateColorIn[2]; + static inline eastl::unique_ptr vrIntermediateColorOut[2]; + static inline eastl::unique_ptr vrIntermediateDepth[2]; + static inline eastl::unique_ptr vrIntermediateMotionVectors[2]; + static inline eastl::unique_ptr vrIntermediateReactiveMask[2]; + static inline eastl::unique_ptr vrIntermediateTransparencyMask[2]; + + // Subrect-sized textures (Default/Faster subrect mode) + static inline eastl::unique_ptr vrSubrectColorIn[2]; + static inline eastl::unique_ptr vrSubrectColorOut[2]; + static inline eastl::unique_ptr vrSubrectDepth[2]; + static inline eastl::unique_ptr vrSubrectMotionVectors[2]; + static inline eastl::unique_ptr vrSubrectReactiveMask[2]; + static inline eastl::unique_ptr vrSubrectTransparencyMask[2]; + static inline uint32_t vrSubrectInW = 0, vrSubrectInH = 0, vrSubrectOutW = 0, vrSubrectOutH = 0; + + // Faster mode per-eye output textures (subOutW × subOutH) + static inline eastl::unique_ptr vrFasterColorOut[2]; + static inline uint32_t vrFasterOutW = 0, vrFasterOutH = 0; + + // DRS region copy (render-resolution SBS) + static inline eastl::unique_ptr vrRenderSBS; + static inline uint32_t vrRenderSBSW = 0, vrRenderSBSH = 0; + + // DRS stretch compute shader resources + static inline winrt::com_ptr vrSubrectStretchCS; + static inline winrt::com_ptr vrSubrectStretchCB; + static inline winrt::com_ptr vrSubrectStretchSampler; + + // Subrect UV hash for resource recreation detection + static inline uint64_t activeSubrectUVHash = 0; + + private: + static bool ExecuteDefaultMode(Streamline& streamline, const VRDlssParams& p); + static bool ExecuteFasterMode(Streamline& streamline, const VRDlssParams& p); + }; +} diff --git a/src/Features/Upscaling/FoveatedRender/Modes.cpp b/src/Features/Upscaling/FoveatedRender/Modes.cpp new file mode 100644 index 0000000000..56628cc365 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Modes.cpp @@ -0,0 +1,254 @@ +// ============================================================================ +// Modes.cpp — Default / Faster DLSS execution strategies +// ============================================================================ +// +// Each mode composes Ops primitives (snapshot, stretch, crop, blend…) in a +// different order. Router resolves VRDlssParams and dispatches. +// +// ============================================================================ + +#include "Core.h" +#include "Ops.h" +#include "Params.h" + +#include "../../../Globals.h" +#include "../../../Utils/Subrect.h" +#include "../../Upscaling.h" +#include "../Streamline.h" + +namespace FoveatedRenderImpl +{ + using namespace Ops; + + // ── Router: resolves params via Params module, dispatches to the selected mode ── + + bool Core::ExecuteVRDlssCore(Streamline& streamline, + ID3D11Resource* upscalingTexture, ID3D11Resource* depthTexture, + ID3D11Resource* reactiveMask, ID3D11Resource* transparencyMask, ID3D11Resource* motionVectors) + { + auto p = VRDlssParams::Resolve(upscalingTexture, depthTexture, reactiveMask, transparencyMask, motionVectors); + + // Detect UV/mode change → destroy DLSS resources so SL recreates them at + // the new size. Both eye UVs feed the hash; asymmetric presets (e.g. + // Nasal Convergence) can change rightUV while leftUV stays put. + uint64_t uvHash = ComputeSubrectUVHash(p.leftUV, p.rightUV, (uint32_t)p.mode); + if (uvHash != Core::activeSubrectUVHash) { + logger::info("[FOVEATED] Subrect UV or mode changed, recreating DLSS resources"); + streamline.DestroyDLSSResources(); + Core::activeSubrectUVHash = uvHash; + } + + switch (p.mode) { + case FoveatedRender::DlssMode::kFaster: + return ExecuteFasterMode(streamline, p); + default: + return ExecuteDefaultMode(streamline, p); + } + } + + // ── Default mode: per-eye isolation, 2 resource sets, 2 evaluates ── + + bool Core::ExecuteDefaultMode(Streamline& streamline, const VRDlssParams& p) + { + // Subrect path needs colorDstUAV (StretchDRSBothEyes writes through it). + // Full-eye path doesn't touch it. Return false on the subrect path so + // the router falls back to standard DLSS rather than hitting the null + // guard inside StretchDRSToFullEye every frame. (CodeRabbit on PR #44.) + if (!p.isFullEye && !p.colorDstUAV) { + logger::error("[FOVEATED] ExecuteDefaultMode subrect path missing colorDstUAV — falling back"); + return false; + } + if (p.isFullEye) { + // Full-eye path: same as standard VR DLSS + if (!PreparePerEyeInputs( + p.colorSrc, p.depthTexture, p.motionVectors, p.reactiveMask, p.transparencyMask, + p.eyeWidthIn, p.eyeHeightIn, p.eyeWidthOut, p.eyeHeightOut)) + return false; + + for (uint32_t i = 0; i < 2; ++i) { + sl::ViewportHandle vp = (i == 1) ? streamline.viewportRight : streamline.viewport; + sl::Extent extentIn{ 0, 0, p.eyeWidthIn, p.eyeHeightIn }; + sl::Extent extentOut{ 0, 0, p.eyeWidthOut, p.eyeHeightOut }; + streamline.EvaluateDLSS(vp, i, + Core::vrIntermediateColorIn[i]->resource.get(), Core::vrIntermediateColorOut[i]->resource.get(), + Core::vrIntermediateDepth[i]->resource.get(), Core::vrIntermediateMotionVectors[i]->resource.get(), + p.reactiveMask ? Core::vrIntermediateReactiveMask[i]->resource.get() : nullptr, + p.transparencyMask ? Core::vrIntermediateTransparencyMask[i]->resource.get() : nullptr, + extentIn, extentOut, p.eyeWidthOut); + } + + return FinalizePerEyeOutputs(p.colorDst, p.eyeWidthOut, p.eyeHeightOut); + } + + // ── Subrect path: crop per-eye, DLSS at subrect size, stretch back ── + + const Util::Subrect::UVRegion* eyeUVs[2] = { &p.leftUV, &p.rightUV }; + + // NOTE: EnsureVRSubrectTextures allocates a single shared per-eye texture + // set sized to LEFT-eye subrect dimensions. Correct only while + // Util::Subrect's auto-mirror keeps leftUV.w/h == rightUV.w/h — the + // per-eye loop below uses the eye's own uv for the real extents. + uint32_t allocSubInW = std::max(1, (uint32_t)(p.eyeWidthIn * p.leftUV.w)); + uint32_t allocSubInH = std::max(1, (uint32_t)(p.eyeHeightIn * p.leftUV.h)); + uint32_t allocSubOutW = std::max(1, (uint32_t)(p.eyeWidthOut * p.leftUV.w)); + uint32_t allocSubOutH = std::max(1, (uint32_t)(p.eyeHeightOut * p.leftUV.h)); + + EnsureVRSubrectTextures(allocSubInW, allocSubInH, allocSubOutW, allocSubOutH, + p.colorSrc, p.motionVectors, p.reactiveMask, p.transparencyMask); + + // Snapshot + Stretch DRS → kMAIN (fill full-eye background) + SnapshotSBS(p.colorSrc, p.renderW, p.renderH); + + StretchDRSBothEyes(p.colorDstUAV, p.eyeWidthOut, p.eyeHeightOut, p.eyeWidthIn, p.eyeHeightIn, p.renderW, p.renderH); + + // Crop subrect per-eye from snapshot (not kMAIN which was overwritten by stretch) + auto context = globals::d3d::context; + for (uint32_t i = 0; i < 2; ++i) { + const auto& uv = *eyeUVs[i]; + // Per-eye sizing — right eye uses rightUV.w/h, not leftUV. + uint32_t subInW = std::max(1, (uint32_t)(p.eyeWidthIn * uv.w)); + uint32_t subInH = std::max(1, (uint32_t)(p.eyeHeightIn * uv.h)); + uint32_t subOutW = std::max(1, (uint32_t)(p.eyeWidthOut * uv.w)); + uint32_t subOutH = std::max(1, (uint32_t)(p.eyeHeightOut * uv.h)); + + uint32_t cropX = (uint32_t)(uv.x * p.eyeWidthIn); + uint32_t cropY = (uint32_t)(uv.y * p.eyeHeightIn); + uint32_t sbsX = (i == 1 ? p.eyeWidthIn : 0) + cropX; + D3D11_BOX sbsCrop = { sbsX, cropY, 0, sbsX + subInW, cropY + subInH, 1 }; + + context->CopySubresourceRegion(Core::vrSubrectColorIn[i]->resource.get(), 0, 0, 0, 0, Core::vrRenderSBS->resource.get(), 0, &sbsCrop); + context->CopySubresourceRegion(Core::vrSubrectDepth[i]->resource.get(), 0, 0, 0, 0, p.depthTexture, 0, &sbsCrop); + context->CopySubresourceRegion(Core::vrSubrectMotionVectors[i]->resource.get(), 0, 0, 0, 0, p.motionVectors, 0, &sbsCrop); + if (p.reactiveMask) + context->CopySubresourceRegion(Core::vrSubrectReactiveMask[i]->resource.get(), 0, 0, 0, 0, p.reactiveMask, 0, &sbsCrop); + if (p.transparencyMask) + context->CopySubresourceRegion(Core::vrSubrectTransparencyMask[i]->resource.get(), 0, 0, 0, 0, p.transparencyMask, 0, &sbsCrop); + + sl::ViewportHandle vp = (i == 1) ? streamline.viewportRight : streamline.viewport; + sl::Extent extentIn{ 0, 0, subInW, subInH }; + sl::Extent extentOut{ 0, 0, subOutW, subOutH }; + streamline.EvaluateDLSS(vp, i, + Core::vrSubrectColorIn[i]->resource.get(), Core::vrSubrectColorOut[i]->resource.get(), + Core::vrSubrectDepth[i]->resource.get(), Core::vrSubrectMotionVectors[i]->resource.get(), + p.reactiveMask ? Core::vrSubrectReactiveMask[i]->resource.get() : nullptr, + p.transparencyMask ? Core::vrSubrectTransparencyMask[i]->resource.get() : nullptr, + extentIn, extentOut, subOutW, subOutH); + } + + // Write DLSS output back at subrect position (with optional blend) + for (uint32_t i = 0; i < 2; ++i) { + const auto& uv = *eyeUVs[i]; + // Per-eye sizing. + uint32_t subOutW = std::max(1, (uint32_t)(p.eyeWidthOut * uv.w)); + uint32_t subOutH = std::max(1, (uint32_t)(p.eyeHeightOut * uv.h)); + + uint32_t dstCropX = (uint32_t)(uv.x * p.eyeWidthOut); + uint32_t dstCropY = (uint32_t)(uv.y * p.eyeHeightOut); + uint32_t dstX = (i == 1 ? p.eyeWidthOut : 0) + dstCropX; + BlendSubrectToOutput(Core::vrSubrectColorOut[i]->resource.get(), p.colorDst, + dstX, dstCropY, subOutW, subOutH); + } + + return true; + } + + // ── Faster mode: DLSS reads directly from SBS via extents, per-eye output, 2 evaluates ── + // Input: kMAIN/depth/mvec SBS textures using extent offsets (zero input copies). + // Output: per-eye independent textures with extent {0,0}. + // Flow: DLSS read → snapshot+stretch background → copy outputs back to kMAIN. + + bool Core::ExecuteFasterMode(Streamline& streamline, const VRDlssParams& p) + { + // Subrect path needs colorDstUAV (StretchDRSBothEyes writes through it + // in Step 3). Full-eye Faster skips Step 3 — don't reject it here just + // because the UAV isn't bound. + if (!p.isFullEye && !p.colorDstUAV) { + logger::error("[FOVEATED] ExecuteFasterMode subrect path missing colorDstUAV — falling back"); + return false; + } + const Util::Subrect::UVRegion* eyeUVs[2] = { &p.leftUV, &p.rightUV }; + + // NOTE: EnsureFasterOutputTextures allocates one per-eye texture set + // sized to LEFT-eye subrect dimensions. Correct only while Util::Subrect + // auto-mirror keeps leftUV.w/h == rightUV.w/h. Per-eye DLSS extents + // below use the eye's own uv. + uint32_t allocSubOutW = p.isFullEye ? p.eyeWidthOut : std::max(1, (uint32_t)(p.eyeWidthOut * p.leftUV.w)); + uint32_t allocSubOutH = p.isFullEye ? p.eyeHeightOut : std::max(1, (uint32_t)(p.eyeHeightOut * p.leftUV.h)); + + // Step 1: Ensure per-eye output textures + EnsureFasterOutputTextures(allocSubOutW, allocSubOutH, p.colorSrc); + + // Step 2a: Snapshot kMAIN into vrRenderSBS so we can clear the HMD + // hidden-area ring without writing to kMAIN itself. Without this clear + // DLSS's temporal accumulation drags Skyrim's default sky clear from + // the masked-out edge into the visible region on fast head motion — + // the standard Streamline path (Streamline.cpp) and Default mode both + // pre-clear via per-eye intermediates. + SnapshotSBS(p.colorSrc, p.renderW, p.renderH); + auto& upscaling = globals::features::upscaling; + auto* depthSRV = globals::game::renderer->GetDepthStencilData() + .depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN] + .depthSRV; + if (Core::vrRenderSBS && Core::vrRenderSBS->uav && depthSRV) { + // Color target IS the SBS snapshot (not a per-eye buffer), so + // colorOffsetX must select the eye's half — same as depthOffsetX. + // ClearHMDMaskCS's default contract assumes the color target is + // per-eye (colorOffsetX = 0) and was written for Streamline's + // per-eye intermediates; here we're routing both eyes through one + // SBS texture so we override both offsets together. + for (uint32_t i = 0; i < 2; ++i) { + const uint32_t eyeOffsetX = i * p.eyeWidthIn; + upscaling.ClearHMDMask(Core::vrRenderSBS->uav.get(), depthSRV, + p.eyeWidthIn, p.eyeHeightIn, eyeOffsetX, eyeOffsetX); + } + } + ID3D11Resource* dlssColorSrc = (Core::vrRenderSBS ? Core::vrRenderSBS->resource.get() : p.colorSrc); + + // Step 2b: DLSS reads from the mask-cleared SBS snapshot via extent offsets + // → per-eye output. sl::Extent field order is {top, left, width, height}. + for (uint32_t i = 0; i < 2; ++i) { + const auto& uv = *eyeUVs[i]; + // Per-eye sizing. + uint32_t subInW = p.isFullEye ? p.eyeWidthIn : std::max(1, (uint32_t)(p.eyeWidthIn * uv.w)); + uint32_t subInH = p.isFullEye ? p.eyeHeightIn : std::max(1, (uint32_t)(p.eyeHeightIn * uv.h)); + uint32_t subOutW = p.isFullEye ? p.eyeWidthOut : std::max(1, (uint32_t)(p.eyeWidthOut * uv.w)); + uint32_t subOutH = p.isFullEye ? p.eyeHeightOut : std::max(1, (uint32_t)(p.eyeHeightOut * uv.h)); + + uint32_t cropX = p.isFullEye ? 0 : (uint32_t)(uv.x * p.eyeWidthIn); + uint32_t cropY = p.isFullEye ? 0 : (uint32_t)(uv.y * p.eyeHeightIn); + uint32_t inOffsetX = (i == 1 ? p.eyeWidthIn : 0) + cropX; + uint32_t inOffsetY = cropY; + + sl::ViewportHandle vp = (i == 1) ? streamline.viewportRight : streamline.viewport; + sl::Extent extentIn{ inOffsetY, inOffsetX, subInW, subInH }; + sl::Extent extentOut{ 0, 0, subOutW, subOutH }; + + streamline.EvaluateDLSS(vp, i, + dlssColorSrc, Core::vrFasterColorOut[i]->resource.get(), + p.depthTexture, p.motionVectors, + p.reactiveMask, p.transparencyMask, + extentIn, extentOut, subOutW, subOutH); + } + + // Step 3: Stretch DRS → kMAIN (subrect only) — snapshot reused from Step 2a. + if (!p.isFullEye) { + StretchDRSBothEyes(p.colorDstUAV, p.eyeWidthOut, p.eyeHeightOut, p.eyeWidthIn, p.eyeHeightIn, p.renderW, p.renderH); + } + + // Step 4: Copy DLSS output back (with optional blend) + for (uint32_t i = 0; i < 2; ++i) { + const auto& uv = *eyeUVs[i]; + // Per-eye sizing. + uint32_t subOutW = p.isFullEye ? p.eyeWidthOut : std::max(1, (uint32_t)(p.eyeWidthOut * uv.w)); + uint32_t subOutH = p.isFullEye ? p.eyeHeightOut : std::max(1, (uint32_t)(p.eyeHeightOut * uv.h)); + + uint32_t dstCropX = p.isFullEye ? 0 : (uint32_t)(uv.x * p.eyeWidthOut); + uint32_t dstCropY = p.isFullEye ? 0 : (uint32_t)(uv.y * p.eyeHeightOut); + uint32_t dstX = (i == 1 ? p.eyeWidthOut : 0) + dstCropX; + BlendSubrectToOutput(Core::vrFasterColorOut[i]->resource.get(), p.colorDst, + dstX, dstCropY, subOutW, subOutH); + } + + return true; + } +} diff --git a/src/Features/Upscaling/FoveatedRender/Ops.h b/src/Features/Upscaling/FoveatedRender/Ops.h new file mode 100644 index 0000000000..44bcc8cd61 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Ops.h @@ -0,0 +1,66 @@ +#pragma once + +#include "Core.h" +#include "Utils/Subrect.h" + +#include +#include + +class Texture2D; + +// Primitive operations for the FoveatedRender VR DLSS pipeline. +// +// Each function is a self-contained building block. Mode pipelines in +// Modes.cpp compose these in different orders to form the Default and +// Faster strategies. +namespace FoveatedRenderImpl::Ops +{ + // Texture creation helper. + eastl::unique_ptr CreateTextureFromSource(ID3D11Resource* src, uint32_t width, uint32_t height, + bool copyBindFlags = false, bool createSRV = false, bool createUAV = false, const char* name = nullptr); + + // Lazy/idempotent resource ensure helpers. + void EnsureVRIntermediateTextures(uint32_t inW, uint32_t inH, uint32_t outW, uint32_t outH, + ID3D11Resource* colorSrc, ID3D11Resource* mvecSrc, ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc); + + void EnsureVRSubrectTextures(uint32_t subInW, uint32_t subInH, uint32_t subOutW, uint32_t subOutH, + ID3D11Resource* colorSrc, ID3D11Resource* mvecSrc, ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc); + + void EnsureFasterOutputTextures(uint32_t subOutW, uint32_t subOutH, ID3D11Resource* colorSrc); + + void EnsureVRRenderSBS(uint32_t renderW, uint32_t renderH, ID3D11Resource* colorSrc); + + // Copy full-eye slices from SBS textures into per-eye intermediates. + bool PreparePerEyeInputs(ID3D11Resource* colorSrc, ID3D11Resource* depthSrc, ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc, + uint32_t eyeWidthIn, uint32_t eyeHeightIn, uint32_t eyeWidthOut, uint32_t eyeHeightOut); + + // Copy per-eye output intermediates back into the SBS output texture. + bool FinalizePerEyeOutputs(ID3D11Resource* colorDst, uint32_t eyeWidthOut, uint32_t eyeHeightOut); + + // Snapshot kMAIN DRS data into vrRenderSBS. + void SnapshotSBS(ID3D11Resource* src, uint32_t renderW, uint32_t renderH); + + // Compute-shader stretch of a single eye region from renderSBS → kMAIN. + void StretchDRSToFullEye(ID3D11ShaderResourceView* renderSBSSRV, ID3D11UnorderedAccessView* kMainUAV, + uint32_t dstOffsetX, uint32_t dstWidth, uint32_t dstHeight, + uint32_t srcOffsetX, uint32_t srcWidth, uint32_t srcHeight, + uint32_t srcEyeWidth, uint32_t srcEyeHeight); + + // StretchDRS for both eyes (snapshot must already exist in vrRenderSBS). + void StretchDRSBothEyes(ID3D11UnorderedAccessView* dstUAV, uint32_t eyeWidthOut, uint32_t eyeHeightOut, + uint32_t eyeWidthIn, uint32_t eyeHeightIn, uint32_t renderW, uint32_t renderH, + ID3D11ShaderResourceView* srcOverride = nullptr); + + // Hard-copy a DLSS subrect output onto the destination at (offsetX, offsetY). + // No feather/dither — straight CopySubresourceRegion. + void BlendSubrectToOutput(ID3D11Resource* dlssSrc, ID3D11Resource* dst, + uint32_t dstOffsetX, uint32_t dstOffsetY, uint32_t subWidth, uint32_t subHeight, uint32_t srcOffsetX = 0); + + // Hash of per-eye UVs + mode for change detection (forces SL DLSS resource + // recreation). Both eyes are mixed in so asymmetric presets — e.g. Nasal + // Convergence, where rightUV differs from leftUV — don't collide on a + // left-eye-only hash and skip SL recreation. + uint64_t ComputeSubrectUVHash(const Util::Subrect::UVRegion& leftUV, + const Util::Subrect::UVRegion& rightUV, uint32_t mode); +} diff --git a/src/Features/Upscaling/FoveatedRender/Params.cpp b/src/Features/Upscaling/FoveatedRender/Params.cpp new file mode 100644 index 0000000000..ad6eb610c8 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Params.cpp @@ -0,0 +1,70 @@ +#include "Params.h" + +#include "../../../State.h" +#include "../../../Utils/Game.h" +#include "../../Upscaling.h" +#include "../FoveatedRender.h" +#include "../PerfMode.h" + +namespace FoveatedRenderImpl +{ + VRDlssParams VRDlssParams::Resolve( + ID3D11Resource* upscalingTexture, + ID3D11Resource* depth, + ID3D11Resource* reactive, + ID3D11Resource* transparency, + ID3D11Resource* mvec) + { + VRDlssParams p{}; + + // Dimensions. With DLSSperf (PerfMode) active, the engine RTs (kMAIN, + // depth, mvec) are allocated at RenderRes and state->screenSize is + // spoofed to RenderRes too. PerfMode owns a private DisplayRes + // testTexture that DLSS must target. Mirror Streamline::Upscale's + // plumbing (Streamline.cpp:617-626) so the foveated route works in + // both stacks: input extents read from kMAIN at RenderRes, output + // extents and colorDst point at DisplayRes / testTexture. + auto& perfMode = globals::features::upscaling.perfMode; + const bool dlssperfActive = perfMode.IsHookActive() && perfMode.GetTestTexture(); + + const auto screenSize = globals::state->screenSize; + const auto renderSize = Util::ConvertToDynamic(screenSize); + const auto displaySize = dlssperfActive ? perfMode.GetDisplayScreenSize() : screenSize; + + p.renderW = (uint32_t)renderSize.x; + p.renderH = (uint32_t)renderSize.y; + p.eyeWidthIn = (uint32_t)(renderSize.x / 2); + p.eyeHeightIn = (uint32_t)renderSize.y; + p.eyeWidthOut = (uint32_t)(displaySize.x / 2); + p.eyeHeightOut = (uint32_t)displaySize.y; + + // Textures. With DLSSperf, DLSS output lands in PerfMode's testTexture + // (DisplayRes); the stretched periphery also targets the testTexture's + // UAV. Without DLSSperf, both alias kMAIN at full size. + p.colorSrc = upscalingTexture; + p.colorDst = dlssperfActive ? static_cast(perfMode.GetTestTexture()) : upscalingTexture; + p.colorDstUAV = dlssperfActive ? perfMode.GetTestTextureUAV() : + globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN].UAV; + + p.depthTexture = depth; + p.reactiveMask = reactive; + p.transparencyMask = transparency; + p.motionVectors = mvec; + + // Mode & subrect. PR-1's stereo Subrect API: GetUV() returns the + // primary UV (= left-eye in stereo mode); GetRightEyeUV() returns + // the mirrored right-eye UV. + auto& enhancer = globals::features::upscaling.foveatedRender; + p.mode = enhancer.GetDlssMode(); + p.leftUV = enhancer.subrectController.GetUV(); + p.rightUV = enhancer.subrectController.GetRightEyeUV(); + p.isFullEye = (p.leftUV.w >= 0.999f && p.leftUV.h >= 0.999f); + + // Jitter — ConfigureUpscaling already computed correct DLSS jitter. + auto& upscaling = globals::features::upscaling; + p.jitterX = upscaling.jitter.x; + p.jitterY = upscaling.jitter.y; + + return p; + } +} diff --git a/src/Features/Upscaling/FoveatedRender/Params.h b/src/Features/Upscaling/FoveatedRender/Params.h new file mode 100644 index 0000000000..eb85cd70d2 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Params.h @@ -0,0 +1,50 @@ +#pragma once + +#include "../FoveatedRender.h" +#include "Utils/Subrect.h" +#include + +namespace FoveatedRenderImpl +{ + // Unified parameter block consumed by Mode functions. Resolved from + // current global state — when DLSSperf is active, Params::Resolve routes + // `colorDst`/`colorDstUAV` and the output extents through PerfMode's + // testTexture (see Params.cpp). + struct VRDlssParams + { + // Dimensions + uint32_t renderW; // SBS render width (after DRS) + uint32_t renderH; // SBS render height (after DRS) + uint32_t eyeWidthIn; // per-eye input (render) width + uint32_t eyeHeightIn; // per-eye input (render) height + uint32_t eyeWidthOut; // per-eye output (display) width + uint32_t eyeHeightOut; // per-eye output (display) height + + // Textures + ID3D11Resource* colorSrc; // input color (kMAIN) + ID3D11Resource* colorDst; // output color (kMAIN, or PerfMode's testTexture when DLSSperf is active) + ID3D11UnorderedAccessView* colorDstUAV; // UAV for stretch output target + ID3D11Resource* depthTexture; + ID3D11Resource* reactiveMask; + ID3D11Resource* transparencyMask; + ID3D11Resource* motionVectors; + + // Mode & subrect (mode set: kDefault, kFaster). + FoveatedRender::DlssMode mode; + Util::Subrect::UVRegion leftUV; + Util::Subrect::UVRegion rightUV; + bool isFullEye; + + // Jitter (pixel-space, render resolution) + float jitterX; + float jitterY; + + // Build a complete parameter block from current global state. + static VRDlssParams Resolve( + ID3D11Resource* upscalingTexture, + ID3D11Resource* depthTexture, + ID3D11Resource* reactiveMask, + ID3D11Resource* transparencyMask, + ID3D11Resource* motionVectors); + }; +} diff --git a/src/Features/Upscaling/FoveatedRender/Postprocess.cpp b/src/Features/Upscaling/FoveatedRender/Postprocess.cpp new file mode 100644 index 0000000000..535eea89fa --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Postprocess.cpp @@ -0,0 +1,62 @@ +#include "Postprocess.h" + +#include "../../../Globals.h" +#include "../../../State.h" +#include "../../Upscaling.h" +#include "../FoveatedRender.h" + +#include + +namespace FoveatedRenderImpl +{ + bool Postprocess::ApplyDlssSharpening(Upscaling& upscaling) + { + // sharpnessDLSS <= 0 is the single disable signal — sharpness lives on + // Upscaling::Settings so the route shares the global slider. + const float sharpnessSetting = upscaling.settings.sharpnessDLSS; + if (sharpnessSetting <= 0.0f) { + return true; + } + + if (!upscaling.sharpenerTexture || !upscaling.sharpenerTexture->uav || !upscaling.sharpenerTexture->resource) { + logger::error("[FOVEATED] Missing sharpener resources"); + return false; + } + + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + if (!context || !renderer) { + logger::error("[FOVEATED] Missing D3D context or renderer for sharpening"); + return false; + } + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + + if (!main.SRV) { + logger::error("[FOVEATED] Missing main SRV for sharpening"); + return false; + } + + // Same exponential mapping Upscaling::ApplySharpening uses: lower + // setting = stronger sharpen. + float currentSharpness = (-2.0f * sharpnessSetting) + 2.0f; + currentSharpness = exp2(-currentSharpness); + + // In-place RCAS on kMAIN through sharpenerTexture. + ID3D11Resource* mainResource = nullptr; + main.SRV->GetResource(&mainResource); + if (!mainResource) { + logger::error("[FOVEATED] Failed to acquire main resource for sharpening"); + return false; + } + + context->OMSetRenderTargets(0, nullptr, nullptr); + upscaling.rcas.ApplySharpen(main.SRV, upscaling.sharpenerTexture->uav.get(), currentSharpness); + context->CopyResource(mainResource, upscaling.sharpenerTexture->resource.get()); + mainResource->Release(); + + if (globals::game::stateUpdateFlags) { + globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); + } + return true; + } +} diff --git a/src/Features/Upscaling/FoveatedRender/Postprocess.h b/src/Features/Upscaling/FoveatedRender/Postprocess.h new file mode 100644 index 0000000000..7ccc8dc06b --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Postprocess.h @@ -0,0 +1,16 @@ +#pragma once + +struct Upscaling; + +namespace FoveatedRenderImpl +{ + class Postprocess + { + public: + // Sharpening pass for the FoveatedRender route. Mirrors what + // Upscaling::ApplySharpening does but is invoked from + // Main_PostProcessing only when the FoveatedRender route is active. + // Only the kRCAS path is wired. + static bool ApplyDlssSharpening(Upscaling& upscaling); + }; +} diff --git a/src/Features/Upscaling/FoveatedRender/Preprocess.cpp b/src/Features/Upscaling/FoveatedRender/Preprocess.cpp new file mode 100644 index 0000000000..74cfb4a7a4 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Preprocess.cpp @@ -0,0 +1,109 @@ +#include "Preprocess.h" + +#include "../../../Deferred.h" +#include "../../../State.h" +#include "../../../Util.h" +#include "../../Upscaling.h" + +namespace +{ + ID3D11ComputeShader* GetEnhancerEncodeTexturesCS(Upscaling& upscaling, Upscaling::UpscaleMethod upscaleMethod) + { + uint methodIndex = (uint)upscaleMethod; + if (!upscaling.encodeTexturesCS[methodIndex]) { + std::vector> defines; + defines.push_back({ "DLSS", "" }); + + upscaling.encodeTexturesCS[methodIndex].attach((ID3D11ComputeShader*)Util::CompileShader( + L"Data/Shaders/Upscaling/EncodeTexturesCS.hlsl", defines, "cs_5_0")); + } + + return upscaling.encodeTexturesCS[methodIndex].get(); + } +} + +namespace FoveatedRenderImpl +{ + bool Preprocess::EncodeUpscalingTextures(Upscaling& upscaling) + { + auto upscaleMethod = upscaling.GetUpscaleMethod(); + if (upscaleMethod != Upscaling::UpscaleMethod::kDLSS) { + logger::error("[FOVEATED] Non-DLSS preprocess path is disabled; method={}", (int)upscaleMethod); + return false; + } + + auto state = globals::state; + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + + if (!upscaling.upscalingDataCB || !upscaling.reactiveMaskTexture || !upscaling.transparencyCompositionMaskTexture) { + logger::error("[FOVEATED] Missing preprocess resources"); + return false; + } + + // motionVectorCopyTexture is dereferenced unconditionally in the UAV + // array below when method == kDLSS. The above resource check did not + // cover it. Fail closed rather than null-deref. (CodeRabbit on PR #44.) + if (upscaleMethod == Upscaling::UpscaleMethod::kDLSS && !upscaling.motionVectorCopyTexture) { + logger::error("[FOVEATED] Missing motionVectorCopyTexture for DLSS preprocess"); + return false; + } + + auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + auto& temporalAAMask = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kTEMPORAL_AA_MASK]; + auto& normals = renderer->GetRuntimeData().renderTargets[globals::deferred->forwardRenderTargets[2]]; + auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + + // Bail before BeginPerfEvent so the perf-event lifecycle stays balanced. + // CSSetShaderResources with a null view in the array doesn't crash, but + // the encode shader reads all four — a null among them silently corrupts + // the reactive/transparency masks DLSS will sample next. + if (!temporalAAMask.SRV || !normals.SRV || !motionVector.SRV || !depth.depthSRV) { + logger::error("[FOVEATED] Missing preprocess SRV inputs"); + return false; + } + + auto dispatchCount = Util::GetScreenDispatchCount(true); + + state->BeginPerfEvent("FOVEATED Encode Upscaling Textures"); + + auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); + Upscaling::UpscalingDataCB upscalingData{}; + upscalingData.trueSamplingDim = renderSize; + upscaling.upscalingDataCB->Update(upscalingData); + + auto upscalingBuffer = upscaling.upscalingDataCB->CB(); + context->CSSetConstantBuffers(0, 1, &upscalingBuffer); + + ID3D11ShaderResourceView* views[4] = { temporalAAMask.SRV, normals.SRV, motionVector.SRV, depth.depthSRV }; + context->CSSetShaderResources(0, ARRAYSIZE(views), views); + + ID3D11UnorderedAccessView* uavs[3] = { + upscaling.reactiveMaskTexture->uav.get(), + upscaling.transparencyCompositionMaskTexture->uav.get(), + upscaleMethod == Upscaling::UpscaleMethod::kDLSS ? upscaling.motionVectorCopyTexture->uav.get() : nullptr + }; + context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); + + ID3D11ComputeShader* cs = GetEnhancerEncodeTexturesCS(upscaling, upscaleMethod); + if (!cs) { + state->EndPerfEvent(); + logger::error("[FOVEATED] Failed to get encode compute shader"); + return false; + } + + context->CSSetShader(cs, nullptr, 0); + context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + + ID3D11ShaderResourceView* nullViews[4] = { nullptr, nullptr, nullptr, nullptr }; + context->CSSetShaderResources(0, ARRAYSIZE(nullViews), nullViews); + ID3D11UnorderedAccessView* nullUavs[3] = { nullptr, nullptr, nullptr }; + context->CSSetUnorderedAccessViews(0, ARRAYSIZE(nullUavs), nullUavs, nullptr); + ID3D11Buffer* nullBuffer = nullptr; + context->CSSetConstantBuffers(0, 1, &nullBuffer); + context->CSSetShader(nullptr, nullptr, 0); + + state->EndPerfEvent(); + return true; + } +} diff --git a/src/Features/Upscaling/FoveatedRender/Preprocess.h b/src/Features/Upscaling/FoveatedRender/Preprocess.h new file mode 100644 index 0000000000..e6bba9b321 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Preprocess.h @@ -0,0 +1,15 @@ +#pragma once + +struct Upscaling; + +namespace FoveatedRenderImpl +{ + class Preprocess + { + public: + // Mirrors Upscaling::EncodeUpscalingTextures (with a DLSS-specific + // shader define) so the FoveatedRender route can prepare reactive + + // transparency masks without touching dev's path. + static bool EncodeUpscalingTextures(Upscaling& upscaling); + }; +} diff --git a/src/Features/Upscaling/PerfMode.cpp b/src/Features/Upscaling/PerfMode.cpp index 611017c498..522773bba0 100644 --- a/src/Features/Upscaling/PerfMode.cpp +++ b/src/Features/Upscaling/PerfMode.cpp @@ -8,8 +8,7 @@ // Quality mode → render-scale resolution is supplied by the FFX SDK helper // (same one Upscaling.cpp uses at ConfigureUpscaling), avoiding a duplicate -// scale table here. Decoupled from the original PR's DlssEnhancer::Bridge so -// PerfMode can ship without the larger enhancer framework. +// scale table here. #include PerfMode::FullscreenPassScope::FullscreenPassScope(ID3D11DeviceContext* a_context) : diff --git a/src/Features/Upscaling/PerfMode.h b/src/Features/Upscaling/PerfMode.h index dee76bc168..0905d61b71 100644 --- a/src/Features/Upscaling/PerfMode.h +++ b/src/Features/Upscaling/PerfMode.h @@ -6,8 +6,7 @@ // // Opt-in VR upscaling feature. Hooks BSOpenVR::GetRenderTargetSize so all // engine render targets are allocated at a small RenderRes while DLSS writes -// its output to a private DisplayRes testTexture. Ships standalone — the -// "DlssEnhancer" prerequisite from earlier drafts no longer applies. +// its output to a private DisplayRes testTexture. Ships standalone. // // Benefits: // - VRAM and bandwidth savings proportional to the quality-mode scale ratio. diff --git a/src/Features/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index 29e4aebe74..3fc429d2e3 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -10,8 +10,8 @@ #include "../../State.h" #include "../../Util.h" #include "../Upscaling.h" -#include "PerfMode.h" #include "DX12SwapChain.h" +#include "PerfMode.h" namespace { @@ -417,7 +417,7 @@ bool Streamline::IsRTXAndBelow40Series(IDXGIAdapter* a_adapter) return false; } -void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) +void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width, uint32_t height) { sl::DLSSOptions dlssOptions{}; @@ -452,7 +452,10 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) const bool dlssperfActive = perfMode.IsHookActive() && perfMode.GetTestTexture(); dlssOptions.outputWidth = width; - dlssOptions.outputHeight = dlssperfActive ? (uint)perfMode.GetDisplayScreenSize().y : (uint)state->screenSize.y; + // height==0 → caller is the standard upscale path; use full per-eye DisplayRes height. + // Non-zero is the FoveatedRender subrect height — must match extentOut.height or NGX + // produces zeroed output. See SetDLSSOptions decl in Streamline.h for the rationale. + dlssOptions.outputHeight = height != 0 ? height : (dlssperfActive ? (uint)perfMode.GetDisplayScreenSize().y : (uint)state->screenSize.y); // Detect HDR from kMAIN format at runtime -- VR kMAIN may be 8-bit while SE is FP16 { @@ -515,7 +518,8 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) void Streamline::EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, ID3D11Resource* colorIn, ID3D11Resource* colorOut, ID3D11Resource* depth, ID3D11Resource* mvec, ID3D11Resource* reactiveMask, ID3D11Resource* transparencyMask, - const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth) + const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth, + uint32_t outputHeight) { auto context = globals::d3d::context; @@ -552,7 +556,7 @@ void Streamline::EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, } }; - SetDLSSOptions(vp, outputWidth); + SetDLSSOptions(vp, outputWidth, outputHeight); sl::ResourceTag tags[] = { { &colorInRes, sl::kBufferTypeScalingInputColor, sl::ResourceLifecycle::eOnlyValidNow, &extentIn }, diff --git a/src/Features/Upscaling/Streamline.h b/src/Features/Upscaling/Streamline.h index 2f1f11e866..ea8088f7bf 100644 --- a/src/Features/Upscaling/Streamline.h +++ b/src/Features/Upscaling/Streamline.h @@ -86,11 +86,17 @@ class Streamline ReflexOptionsCache reflexOptionsCache{}; uint32_t lastReflexSleepFrame = UINT32_MAX; - // Helper: Execute DLSS for a single viewport with given resources + // Helper: Execute DLSS for a single viewport with given resources. + // outputHeight defaults to 0 → SetDLSSOptions uses full per-eye DisplayRes height + // (matches the standard upscale path where every eval is full eye). FoveatedRender's + // subrect path must pass the actual subrect height so DLSS isn't configured for + // `subOutW × eyeHeightOut` while extentOut says `subOutW × subOutH` — that mismatch + // makes NGX return zeroed output and the subrect region renders black. void EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, ID3D11Resource* colorIn, ID3D11Resource* colorOut, ID3D11Resource* depth, ID3D11Resource* mvec, ID3D11Resource* reactiveMask, ID3D11Resource* transparencyMask, - const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth); + const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth, + uint32_t outputHeight = 0); // Cached DLL version info for Streamline plugin directory static std::vector> dllVersions; @@ -106,7 +112,9 @@ class Streamline bool IsRTXAndBelow40Series(IDXGIAdapter* a_adapter); - void SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width); + // height = 0 → use full per-eye DisplayRes height (default for the standard + // upscale path). Non-zero is the subrect height the FoveatedRender route needs. + void SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width, uint32_t height = 0); void Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors); void UpdateReflex(); diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 497945ae16..35963c05f5 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -14,6 +14,7 @@ #include "Features/InteriorSun.h" #include "Features/LightLimitFix.h" #include "Features/Upscaling.h" +#include "Features/Upscaling/FoveatedRender/Bridge.h" #include "Features/VR.h" #include "Features/VolumetricLighting.h" @@ -411,6 +412,13 @@ struct BSShaderRenderTargets_Create // upscaling toggle), so SetupResources runs here directly. if (perfMode.IsHookActive()) perfMode.SetupResources(); + + // PR-3 MVP-B: latch FoveatedRender enable + qualityMode at the moment + // the engine is fully initialized but before the first frame. After + // this point, live setting changes won't be honored mid-game (matches + // Streamline's DLSS option lifecycle — quality changes need a full + // resource recreate the user has to opt into). + FoveatedRenderImpl::Bridge::BootSequence(); } static inline REL::Relocation func; }; diff --git a/src/Utils/Subrect.cpp b/src/Utils/Subrect.cpp index 0284d42cb6..84d0eb61bb 100644 --- a/src/Utils/Subrect.cpp +++ b/src/Utils/Subrect.cpp @@ -5,6 +5,10 @@ #include #include +// OpaquePreviewBlendCallback lives in Subrect_PreviewBlend.cpp — that TU +// reaches into the plugin's d3d singletons, which the unit-test target +// (tests/cpp pulls Subrect.cpp standalone) can't link against. + namespace { Util::Subrect::UVRegion ClampUV(Util::Subrect::UVRegion uv) diff --git a/src/Utils/Subrect.h b/src/Utils/Subrect.h index c5c0a4705b..c5c600f257 100644 --- a/src/Utils/Subrect.h +++ b/src/Utils/Subrect.h @@ -82,7 +82,9 @@ namespace Util::Subrect // texture; crop UV stays in [0,1] of that window. imageRenderCallback, // when non-null, is queued via ImDrawList::AddCallback around the // preview Image draw (paired with ImDrawCallback_ResetRenderState) so - // hosts can override blend state for the image specifically. + // hosts can override blend state for the image specifically. Pass + // OpaquePreviewBlendCallback when the preview texture is an RT with + // non-1 alpha (kMAIN, etc.) to suppress menu-background bleed-through. void DrawEditor(ID3D11ShaderResourceView* previewSrv, ID3D11Texture2D* previewTexture, float uvVisibleWidth = 1.0f, float uvStartX = 0.0f, ImDrawCallback imageRenderCallback = nullptr); @@ -125,4 +127,21 @@ namespace Util::Subrect void ApplyPreset(int index); void SyncRightUV(); }; + + // Opaque-RGB blend state callback for Controller::DrawEditor. Pass when the + // preview SRV is a render target with non-1 alpha (kMAIN, kTOTAL, etc.). + // ImGui's default SRC_ALPHA blend would let the menu background bleed + // through where the source alpha is < 1, making the preview look like a + // transparency mask. This callback switches to opaque RGB-only writes + // around the Image draw; DrawEditor queues ImDrawCallback_ResetRenderState + // immediately after to restore default state. + // + // Two non-obvious regression risks if reimplemented: + // 1. BlendEnable must stay FALSE — SRC_ALPHA causes the bleed-through. + // 2. WriteMask must exclude alpha (RGB only). In VR, Skyrim's menu UI + // shader recomposites the menu plate over the SBS framebuffer with + // alpha blending; writing texture alpha into the menu plate RT + // produces a cutout visible only through the HMD. RGB-only writes + // leave the plate's pre-cleared alpha=1 in place. + void OpaquePreviewBlendCallback(const ImDrawList*, const ImDrawCmd*); } // namespace Util::Subrect diff --git a/src/Utils/Subrect_PreviewBlend.cpp b/src/Utils/Subrect_PreviewBlend.cpp new file mode 100644 index 0000000000..287eeed0cb --- /dev/null +++ b/src/Utils/Subrect_PreviewBlend.cpp @@ -0,0 +1,42 @@ +// Separate TU for Util::Subrect::OpaquePreviewBlendCallback. Split from +// Subrect.cpp because this needs the plugin's d3d singletons (globals::d3d), +// and Subrect.cpp is also compiled standalone by the unit-test target +// (tests/cpp/CMakeLists.txt) which has no PCH and no D3D context to bind. +// Plugin builds pick this up automatically via the src/*.cpp GLOB_RECURSE. + +#include "Globals.h" +#include "Utils/D3D.h" +#include "Utils/Subrect.h" + +#include +#include +#include + +namespace Util::Subrect +{ + void OpaquePreviewBlendCallback(const ImDrawList*, const ImDrawCmd*) + { + auto* device = globals::d3d::device; + auto* context = globals::d3d::context; + if (!device || !context) { + return; + } + + static winrt::com_ptr opaqueBlend; + if (!opaqueBlend) { + D3D11_BLEND_DESC desc{}; + desc.RenderTarget[0].BlendEnable = FALSE; + desc.RenderTarget[0].RenderTargetWriteMask = + D3D11_COLOR_WRITE_ENABLE_RED | + D3D11_COLOR_WRITE_ENABLE_GREEN | + D3D11_COLOR_WRITE_ENABLE_BLUE; + if (FAILED(device->CreateBlendState(&desc, opaqueBlend.put()))) { + return; + } + Util::SetResourceName(opaqueBlend.get(), "Subrect::OpaquePreviewBlend"); + } + if (opaqueBlend) { + context->OMSetBlendState(opaqueBlend.get(), nullptr, 0xFFFFFFFF); + } + } +} // namespace Util::Subrect From 25ead9cf7548769ec1c9e957b61a615a4d6bb191 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 28 May 2026 01:33:14 -0700 Subject: [PATCH 24/24] fix(slf): VR accumulator OOB heap write Hook_AccumulatedLightsArray bumped RDI (fill-loop count) but not RDX (BSTArray resize count) on VR. On SE and VR the patched 5 bytes are `LEA EDI,[RBP+0x8] ; MOV EDX,EDI`, executed via includeSize=+5 BEFORE the CONTEXT-capture stub, so EDX latches the un-bumped count. The resize then allocates the original 10-slot array while the fill loop writes (ShadowLightCount+ConvertedShadowSlots+1)*2 entries -- an OOB heap write proportional to the user's shadow count, corrupting a tbbmalloc freelist next-pointer. Symptom: random later crashes in the Papyrus VM string heap with RDI holding the prologue bytes of the inlined resize fn (0x83485708245C8948 = `48 89 5C 24 08 57 48 83`). Higher counts crash earlier. SE was already bumping RDX; AE inlines the resize and re-derives EDX from EDI AFTER the stub, so its RDX is dead at the hook point and must not be touched. New gate is !IsAE() instead of IsSE(). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Features/LightLimitFix/ShadowCasterManager.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Features/LightLimitFix/ShadowCasterManager.cpp b/src/Features/LightLimitFix/ShadowCasterManager.cpp index 4388152dda..d1bb5d8dc0 100644 --- a/src/Features/LightLimitFix/ShadowCasterManager.cpp +++ b/src/Features/LightLimitFix/ShadowCasterManager.cpp @@ -337,8 +337,12 @@ namespace ShadowCasterManager int extra = needed - have; if (extra > 0) { ctx.Rdi += extra; - // SE only: RDX is a second counter in the same loop. - if (!REL::Module::IsVR() && REL::Module::GetRuntime() != REL::Module::Runtime::AE) + // SE/VR latch EDX from EDI inside the patched 5 bytes (before the + // stub captures CONTEXT), so BSTArray::resize runs with the un- + // bumped count while the fill loop runs with the bumped one -- + // OOB heap write scaling with ShadowLightCount. AE inlines the + // resize and re-reads EDI after the stub, so RDX is dead here. + if (!REL::Module::IsAE()) ctx.Rdx += extra; } } @@ -3611,8 +3615,9 @@ namespace ShadowCasterManager const auto opcode = *reinterpret_cast(site); const auto rel32 = *reinterpret_cast(site + 1); if (opcode != kCallOpcode || rel32 != kExpectedRel32) { - logger::warn("[SCM] VR shadow-mask site drift: expected E8 rel32=0x{:X} at RenderShadowmasks+0x{:X}, " - "found 0x{:02X} rel32=0x{:X}. Clamping ShadowLightCount to 4 (vanilla) to avoid OOB CTD.", + logger::warn( + "[SCM] VR shadow-mask site drift: expected E8 rel32=0x{:X} at RenderShadowmasks+0x{:X}, " + "found 0x{:02X} rel32=0x{:X}. Clamping ShadowLightCount to 4 (vanilla) to avoid OOB CTD.", kExpectedRel32, kCallOffset, opcode, rel32); s_installedShadowLightCount = 4; } else {
  • +a{H;!ar#$27@q20_1IYHlIB?4F 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 G;r|2P)w(1A diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze/discord.png b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze/discord.png deleted file mode 100644 index c5cdae99ee0a028a13982730918c03297c992c51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 407478 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 (|=+3?}|&O#rjLumu;;S8~16u=Jw}OZxl0&sU45 zZsX*uc^!a1@mBRuDnB^aK2PX0WtsAW9qUON8ZAc9oE=9kJL%Wo%Pul}q3Pm^ds(&Z zs%qz`Bpq0)1xSiXyt~ThAbshL1Vb@3_10=Ix^%T$MY@dD=B8|(%SantwdDIkHMu07 z0WC?3h?hm_Lt_vRT(7-w<5lji_LGc#pUc;Dvb4=Bp3I?aw!sX`_Xw(N=;VpJP}`__jB&zUFvS6v1W)XcZqtFdm$Blj zWBeEM|1P6N@26Y#FbH4B4&&>3O~5u^Hp~gCx<>bh4jh%_Tow#K7XUDRWQX9vzY9?= zJJKnaKuRAmLD@`RrCmLz^3X|wx)+iUq)hU5p&LjYYyN;1_LFuKxrJMx4aru$!ye`S z3@58tL<@ZiCQaR$hh3L#Ih8fPDp3@5^;GI(6DB&f>B6A?ti^w&iS1VlL8J0a+aeS& zcM)O4s%#Knkj+^yn4gwy<@eCa>$~wg1k1C1yZZGSsFC~1hsBxZr}{ELYdu%T{u?g1TRPNioCc zs=6E8wPN60^Ve`n13izvWkddP-Bk(I%c?(Kq>tQ39mIl2u_%%G3BH|#T#uiV-ryNOIk8V;GzhAzi;X@Uor8h<-UhqyuMS%cgidA zIsNnAUMRBe8#zzh+;)n;okRW^`!lYq4Y6L$BfT6zzXPJUvPpYWM~KWJ=wu~d7yZ`# zud_F0O;hT$Rdx`x z%Ijp06BgG_`|x^ZV)4S#&-*?YZ|R$ZUp z)f(E!#6!2Wc9>7O0H#;kFhud3yd)3u23)@5#Ve<~Q8t!wt7KBwmrx6PVhj(~M<%r` z({}UQGZB$-$0SGfW+NuslU1CT9XtTvY(Z@ntQI={*JJ_yPF$HsE^Y=iw>O**YByC( z-L2m#>&5@{QOYfCn0*o=N96(qM9cpvul1>~B*-uA+yli)nDn2(9MT1}uEZE{oP-;a zF~i!%UjS%6=fYpcnGiPe=Z)m2`-v7^>Q;kB-Cw5UY3D;0;yi>FJNtlZ-_+}xVng-) zk}k6v>#MPwS3Q@EA^2hZN1(=q(J}vt-qS>ru{Hl^{yNQj%Te_HgbE|Q^mnw_!`HJS zIWA20z70_QzSIBX8ef}C|Mw2!VPRTM{Kr{F6dEwW{(D|n=X&0Qk!u;5+U!R~>8Eq) z|KK%C01}8%F!&-+;ugxI%igo+$FU z@qfs+sU-e6@&e+H9qMbms3~R5BGN)EOMehsXg1&-(|xq*E;tqPz)5TZgb~|k{9m%O zYB$GHfW1$^cKZAur{)50SUm7P$3b_y>2YfI6TIVsh)T=O`9H>dZ`V%#ue {Lh%@ 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