Developers - community-shaders/skyrim-community-shaders GitHub Wiki
This is the base package that loads all the custom shaders.
This is the main dll in the repo that enables the replacement of vanilla shaders with new shaders.
Can be found primarily in https://github.com/doodlum/skyrim-community-shaders/tree/main/src
The default shaders will be found in https://github.com/doodlum/skyrim-community-shaders/tree/main/package/Shaders.
These default shaders should be overwritten by feature specific shaders.
These are custom features that are loaded after the default CS install.
To add a new feature, the DLL needs to be modified. See prior examples of adding new features for the full list. https://github.com/doodlum/skyrim-community-shaders/pull/83
The main features should live here: https://github.com/doodlum/skyrim-community-shaders/tree/dev/src/Features
- GetName
- Used in ImGui feature list and json entry per feature
- GetShortName
- Used for ini loading (No spaces usually)
- SetupResources
- Called once in startup
- Reset
- Called once per frame
- DrawSettings
- Used for rendering imgui
- Draw
- Normal rendering code here
- Save
- Serialize settings to json
- RestoreDefaultSettings
- Reset feature settings to default value
- Load
- Deserialize settings from json, also need to call
Feature::Loadafter loading
- Deserialize settings from json, also need to call
These are already default defined in Feature.h but can be overriden if you need to do something extra like defined below
- GetShaderDefineName
- used as an additional macro added when compiling shaders specified by HasShaderDefine, if the feature is loaded
- HasShaderDefine
- if a shader type returns true, then GetShaderDefineName macro is added to its compilation, if the feature is loaded
- DrawDeferred
- This one is not called yet, that is in the subsurface-scattering branch
- DataLoaded
- Called by SKSE
kDataLoadedevent
- Called by SKSE
- PostPostLoad
- Called by SKSE
kPostPostLoadevent
- Called by SKSE
- ClearShaderCache
- Called by imgui clear shader cache button
- GetFeatureModLink
- Specify link to feature, which will be presented to the user when feature is unloaded: example
- DrawFailLoadMessage
- Returns true/false depending on whether the stock failed to load message should be printed: example
- DrawUnloadedUI
- Called when feature is unloaded to add any content to the panel (you probably want to return false for DrawFailLoadMessage if using this): example
Shaders are stored in https://github.com/doodlum/skyrim-community-shaders/tree/dev/features
To debug CS you will need to be able to debug both the cpp dll and the hlsl shader files.
- Save a copy of the original exe so you can replace it.
- Use Steamless to strip the SteamDRM from the Skyrim.exe. This is required for a debugger to attach.
Make sure to check
Keep Bind Sectionin steamless, game will not boot without it.
To ensure addresses don't move, disable ASLR. This can be done with CFF Explorer. Optional Header -> DLL Characteristics -> DLL can move. Disable this.

Renderdoc can be used to debug shaders. You can also try alandtse's fork. See extra features.
- Disable incompatible features:
- Skyrim Upscaler
- reshade
-
Optional Enable Global Hooking. Make sure to
Enable Global Hookwhich will grey out all settings. This also will prevent closing renderdoc until you disable the setting. - Set up
Launch Applicationso it will find the skse.exe (or Skyrim.exe if global hooking) when it launches.

- Launch Skyrim. You will know RenderDoc has connected because of the message in the top left of Skyrim
Capturing D3D11. If it doesn't show, try toggling the global hook and launching again.

- Enable Developer Mode. Developer Mode can be enabled by setting the
Advanced Settings->Log LeveltoTRACEorDEBUG.

- Enable
Advanced->Extended Frame Annotationsto help populate info in the rendering process.

-
Clear Shader Cache and Disk Cache. This is necessary to save debug information in the shaders to access named buffers/hlsl in renderdoc.
-
In game, press
F12to capture the scene. -
In Renderdoc,
File->Attach to Running Instance. Select Skyrim andConnect to App.

- Once attached, a new tab will appear. Double click any captures to load.

- You can verify you are seeing debug information by opening up the Pipeline tab and checking the Vertex or Pixel shaders. The resources should be named. In this case, we're getting
water.hlsldata.

With Claude Code or other agentic AI, you can connect RenderDoc as a MCP server to help debug hlsl shaders.
- Download agentic-renderdoc. This is alandtse's fork, the upstream may be better. Follow it's installation steps.
- Setup Claude Code with the CS repo.
- Confirm you can see the MCP server:
claude mcp list
Checking MCP server health...
renderdoc: agentic-renderdoc - β Connected- Capture the scene in renderdoc with the version of CS that is causing the issue.
- Make sure Claud knows about and can access the MCP. Just ask it to use the renderdoc MCP.
- Describe the problem to Claude to a reasonable degree to not waste tokens. Identify:
- What event ID (EID)? Find exactly which shader is drawing the bad item. E.g., scroll through the timeline to what is drawing it.
- What shader? Pixel/Compute etc. Check the
Pipeline State. If you can identify the specific CS hlsl file, it'll be even more efficient. - Identify the coordinates and the texture. E.g., (Rt0 kMain at 754, 587). In the Texture viewer, you can right click to save the information in the bottom of the window.
It is possible to block individual shaders in game to find a faulty shader.
- Find the faulty mesh in game. Confirm the mesh is caused by CS by toggling CS using the
Toggle Effects Key(defaultNumpad *). The faulty mesh should disappear. If it doesn't, it's a Vanilla bug. This is an example with CS disabled:

CS Enabled showing faulty mesh (note texture is missing in red circle):

- Enable Developer Mode. Developer Mode can be enabled by setting the
Advanced Settings->Log LeveltoTRACEorDEBUG.

- Hit the
PageUporPageDownto cycle all active shaders until the faulty mesh disappears. This should match the vanilla shader.

- [Optional] Stop blocking shaders by clicking on the
Advanced Settings->Stop Blocking Shadersbutton. This should also flush the log and also provide a noticeable log entry to identify the last shader blocked. This button only appears when shaders are being blocked.

- Review the CommunityShaders.log file. The blocked shader should be the last entry before
Stopped blocking shaders(from step 4). In the example below, the ID/descriptor is12000004and it is a Lighting/Vertex shader, with the compile options ofWETNESS_EFFECTS LIGHT_LIMIT_FIX COMPLEX_PARALLAX_MATERIALS DYNAMIC_CUBEMAPS LODLANDNOISE LODLANDSCAPE MODELSPACENORMALS SHADOWSPLITCOUNT=3. With this information, we can look atlighting.hlslto figure out what hlsl is active.
[2023-11-20 19:09:59.451] [debug] [33112] [ShaderCache.cpp:1557] Blocking shader (6/93) Lighting:Vertex:WETNESS_EFFECTS LIGHT_LIMIT_FIX COMPLEX_PARALLAX_MATERIALS DYNAMIC_CUBEMAPS LODLANDNOISE LODLANDSCAPE MODELSPACENORMALS SHADOWSPLITCOUNT=3
[2023-11-20 19:09:59.451] [debug] [33112] [ShaderCache.cpp:1209] Skipping blocked shader 12000004:Lighting:Vertex:WETNESS_EFFECTS LIGHT_LIMIT_FIX COMPLEX_PARALLAX_MATERIALS DYNAMIC_CUBEMAPS LODLANDNOISE LODLANDSCAPE MODELSPACENORMALS SHADOWSPLITCOUNT=3 total: 1
[2023-11-20 19:10:10.734] [debug] [33112] [ShaderCache.cpp:1568] Stopped blocking shaders
For comparison, if we block the corresponding Pixel shader, the coloring will be distorted instead which is revealed to be ID/descriptor 12000005 which is the Lighting:Pixel shader.

[2023-11-20 23:07:17.546] [debug] [70860] [ShaderCache.cpp:1537] Blocking shader (24/97) Lighting:Pixel:WETNESS_EFFECTS LIGHT_LIMIT_FIX COMPLEX_PARALLAX_MATERIALS DYNAMIC_CUBEMAPS LODLANDNOISE LODLANDSCAPE MODELSPACENORMALS SHADOWSPLITCOUNT=3 VC
[2023-11-20 23:07:17.564] [debug] [74308] [ShaderCache.cpp:1243] Skipping blocked shader 12000005:Lighting:Pixel:WETNESS_EFFECTS LIGHT_LIMIT_FIX COMPLEX_PARALLAX_MATERIALS DYNAMIC_CUBEMAPS LODLANDNOISE LODLANDSCAPE MODELSPACENORMALS SHADOWSPLITCOUNT=3 VC total: 1
Please note the hotreload works for changes to hlsl/hlsli files. Changes to cpp code require a restart of the game.
- Make sure 'Enable Async' is on
- Disable 'Enable Disk Cache' (Probably obsolete, but does not hurt)
- Change hlsl/hlsli
- Click 'Clear Shader Cache'
- The game should recompile shaders now
- Make sure 'Enable Async' is on
- Disable 'Enable Disk Cache' (Probably obsolete, but does not hurt)
- Enable File Watcher, this will add a watcher for hlsl files in the root folder and automatically clear shader cache for changed files
- Change hlsl file in root
- The game should recompile shaders now
| Stage | How triggered | Notes |
|---|---|---|
| 1. Semantic version | Manual β Release: Semantic Version
|
Bumps INIs, tags, creates draft release. On a devβmain promotion it also reconciles dev (see below). |
| 2. Build artifacts | Automatic β fires on the tag push from step 1 | Requires RELEASE_PAT secret to be set |
| 3. Publish release | Manual β edit and publish draft on GitHub | Review notes, confirm artifacts attached |
| 4. Nexus dry run | Automatic on publish β Release: Nexus Upload Trigger
|
Always runs as dry_run: true; verifies versions, does not upload |
| 5. Nexus upload | Manual β Nexus: Upload Release with dry_run: false
|
Run after reviewing dry-run output |
The release App (community-shaders-release-bot) must be in the "Allow specified actors to bypass required pull requests" list for both main and dev.
The app token alone cannot bypass the "Require a pull request before merging" rule β that is a separate control from "Allow force pushes." If the App is missing from a branch's bypass list, the workflow's fast-forward push fails with:
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: - Changes must be made through a pull request.
To set it: Settings β Branches β <branch> β Require a pull request before merging β "Allow specified actors to bypass required pull requests" β add community-shaders-release-bot. Repeat for both main and dev.
| Branch | Role | Releases it produces |
|---|---|---|
main |
Stable release channel | vX.Y.Z |
dev |
Integration / RC |
vX.Y.Z-rc.N prereleases |
hotfix/X.Y.x |
Maintenance for older release lines |
vX.Y.Z on the X.Y channel |
semantic-release infers behavior from the branch the workflow is dispatched on (channels defined in .releaserc.js). There is no release_type input. hotfix/X.Y.x is only valid once main has shipped a release on a newer minor/major β to patch the current line, use the "Patch Release Process" flow below (promote on main), not a direct dispatch on the hotfix branch.
main becomes an ancestor of dev at each minor/major promotion β not after every single release. Current-line hotfixes intentionally let main diverge from dev until the next promotion folds them back in with a single ancestry-only merge.
dev is never rewritten β no rebases of shared branches, no force pushes. The reconcile at promotion time is a normal merge commit fast-forwarded onto dev, never a history rewrite. This is safe because a devβmain promotion is always a minor/major (it carries a feat/breaking change), so dev's next version is always above any interim patch β there is no version collision.
The old behavior β rebase-reconciling
dev(force-push) after every hotfix, plus an auto-rebase-open-PRs workflow β has been removed. It rewrotedevhistory (breaking every open PR) and was blocked bydev's force-push protection anyway. The merge-at-promotion reconcile replaces it.
Use the standard process for the next minor or major β new features that have accumulated on dev and you're ready to promote to stable.
1. Merge all work to dev via normal PRs
All commits that should ship must be on dev. Semantic-release reads conventional commit messages (feat:, fix:, chore:, etc.) to determine the version bump:
-
feat:β minor bump (e.g.1.5.xβ1.6.0) -
fix:β patch bump (e.g.1.5.0β1.5.1) -
feat!:orBREAKING CHANGE:β major bump
chore:, docs:, style:, etc. produce no version bump on their own.
2. Cut a final RC for confirmation (recommended)
Go to Actions β Release: Semantic Version β Run workflow, branch dev, leave ff_target empty. This produces vX.Y.Z-rc.N and confirms the version stable will produce. Install and verify. (No effect on main.)
3. Promote dev β main
Copy the SHA of the verified RC (or dev's current HEAD). Go to Actions β Release: Semantic Version β Run workflow, branch main, paste the SHA into the ff_target input.
The workflow runs, in order:
-
Reconcile
dev(merge pre-step). If interim hotfixes have mademainnot an ancestor ofdev, the workflow checks outdevatff_targetand mergesmaininto it β a single ancestry-only merge commit. The merge tree is asserted to equaldev's tree (version-bump files βCMakeLists.txtandfeatures/**/Shaders/Features/*.iniβ are resolved todev's values). Any change onmainoutside that version-file allowlist hard-fails the run before any push β that is the tripwire for a non-dev-sourced commit having landed onmain. The merge is fast-forward pushed ontodev(no force; the App's PR-bypass authorizes it), and the promotion retargets to this merge commit. Ifmainis already an ancestor ofdev, this step is a no-op. -
Fast-forward
mainto the (possibly retargeted) commit β no PR, no merge commit onmain. - Auto-bump any feature
.iniversions whose shaders or code changed since the last stable tag. - Run
semantic-release, which determines the next stable version (e.g.v1.6.0), updatesCMakeLists.txt, and commitschore(release):back tomain. - Push the new tag (
v1.6.0) and create a draft GitHub release with auto-generated notes. -
Dedup release notes (best-effort). Strip from the new release's notes any fix already shipped in an interim hotfix on the current line (matched via
cherry picked from committrailers and PR numbers), so entries aren't duplicated. This step never fails the run. -
Fast-forward
devto absorb thechore(release):commit so the two branches reconverge.
dev is never rewritten at any step.
4. Wait for the build to complete
The tag push automatically triggers Release: Build Artifacts. Watch it under Actions β it will compile the DLL, validate shaders, and upload packaged .7z artifacts to the draft release.
If the build does not appear within a minute of the tag being created, see Build artifacts are missing.
5. Review and publish the draft release
Open the draft release on GitHub. Review the auto-generated release notes, edit as needed, then publish.
6. Nexus dry run fires automatically
Publishing triggers Release: Nexus Upload Trigger, which always runs as a dry run. It queries Nexus for each mod and prints a version table in the Actions summary β no files are uploaded. Review it to confirm the planned versions look correct.
7. Run the real Nexus upload
Go to Actions β Nexus: Upload Release β Run workflow. Enter the tag, include the v, so not 1.6.0 but v1.6.0, set dry_run: false(the text is:If true, do not upload to Nexus; only report the planned upload), and run.
The workflow uploads core and any features with a bumped .ini version. Already-present versions are skipped automatically, so re-running is always safe.
Any failed uploads can be manually uploaded.
8. Add the changelog to the nexus page
The current nexus API does not allow passing on changelogs, so this will need to be manually uploaded, you can just copy paste the release notes from the github release and post them into the changelog window on nexus.
Use this for any patch β current line or an older line. The flow is identical except for the final dispatch target. The hotfix branch acts as a verification staging area; the auto-built -prNNNN prerelease from PR checks gives you a real artifact to install and test before cutting the release.
1. Land the fix on dev first (normal PR flow), if applicable
If the fix is also relevant to current dev/main, land it on dev via normal flow. Skip if the fix only applies to an older line that no longer matches dev.
2. Dispatch the hotfix candidate workflow
Go to Actions β Release: Hotfix Candidate β Run workflow. Inputs:
-
release_line: e.g.1.5. Leave empty to auto-detect the latest stable tag. -
scope:fix-onlyis the default and safest. -
commits: optional explicit SHAs to cherry-pick instead of the scope filter. -
Plan only: true to plan only, will do a dry-run where it reports what commits will be pushed.
The workflow creates hotfix/X.Y.x from the latest stable tag on that line if it doesn't already exist, cherry-picks eligible commits from dev (with -x, so each carries a cherry picked from commit trailer), and opens a PR against hotfix/X.Y.x. Breaking changes are always skipped. Multiple fixes can be batched in one candidate by re-dispatching before merging.
3. Install the prerelease and verify
PR checks build the candidate and publish a vX.Y.Z-prNNNN prerelease. Install it, verify the fix, and check for regressions. If the candidate is bad, close the PR and re-dispatch (auto-supersedes prior candidates on the same line).
4. Merge the candidate PR
Standard PR merge against hotfix/X.Y.x. Don't squash.
5. Cut the release β branches depending on whether you're patching the current stable line or an older line:
-
Current line (
mainis still onX.Y): dispatch Release: Semantic Version, branchmain, withff_targetset to the hotfix-staging tip β the second parent of the candidate merge commit, not thehotfix/X.Y.xtip (which is a merge commitmain's protection rejects):git rev-parse origin/hotfix/X.Y.x^2
The workflow fast-forwards
mainto that SHA, runs semantic-release to cutvX.Y.Z+1, and publishes a draft release.devis not touched β the dev reconcile is intentionally skipped for hotfix-source promotions.devis reconciled at the next minor/major promotion via the merge pre-step (which folds these cherry-picked commits β and theirchore(release):bump β intodev's ancestry, version files resolved todev). -
Older line (
mainhas shipped a newer minor/major): dispatch Release: Semantic Version, branchhotfix/X.Y.x, leaveff_targetempty. Semantic-release sees the latest stable on theX.Ychannel as the baseline and produces the next patch.
6. Build, publish, Nexus β same as steps 4β8 of the Standard Release Process.
7. Clean up
The hotfix/X.Y.x branch can be left in place for future patches on the same line (the candidate workflow reuses it) or deleted if no further patches are expected.
RC releases let you ship a pre-release for testing before committing to a stable version.
- Go to Actions β Release: Semantic Version β Run workflow, branch
dev, leaveff_targetempty. - Semantic-release creates a pre-release tag like
v1.6.0-rc.1, thenv1.6.0-rc.2on the next run, etc. No effect onmain. - RC tags are pre-releases β
Release: Nexus Upload Triggerskips them (only stable tags, those without-, are uploaded). -
Release: Build Artifactsfires automatically on the RC tag push β same as stable releases.
Semantic-release computes the stable version by analyzing all commits since the last stable tag on main, not since the last RC tag. The RC tags only exist on the rc channel; when running stable on main, semantic-release operates on the default channel and treats the last stable tag as the baseline.
This means a post-RC commit can escalate the version only if its bump level exceeds what the RC series was already targeting:
| RC was targeting | Post-RC commit | Stable result | Escalation? |
|---|---|---|---|
v1.6.0 (minor) |
feat: |
v1.6.0 |
No β already minor |
v1.6.0 (minor) |
fix: |
v1.6.0 |
No |
v1.5.1 (patch) |
feat: |
v1.6.0 |
Yes β escalates to minor |
v1.6.0 (minor) |
feat!: |
v2.0.0 |
Yes β escalates to major |
v1.5.1 (patch) |
fix: |
v1.5.1 |
No |
Cutting another RC before going stable is the safe approach when in doubt. If a new commit could escalate the version, run RC again first β the new RC tag will reflect the escalated version, making the final stable version explicit and testable before you commit to it.
To promote an RC to stable, follow steps 2β8 of the Standard Release Process β dispatch on main with the verified RC's SHA in ff_target.
Semantic-release found no feat: or fix: commits since the last tag. Either:
- The new commits are all
chore:/docs:/style:β these intentionally produce no release. - You are running on the wrong branch or against the wrong base tag.
If you need to force a patch release anyway, add an empty fix commit:
git commit --allow-empty -m "fix: trigger release"
git push origin devThen re-run the workflow.
The build auto-triggers from the tag push via RELEASE_PAT. If it is missing, check:
-
RELEASE_PATis expired or missing β the most common cause. Regenerate the token, update the repo secret, and manually trigger Actions β Release: Build Artifacts on the tag to recover this release. - The build ran but failed β check the Actions log. Common causes are shader validation errors or a flaky submodule checkout. Fix and re-run.
-
App bypass not set β if the release App is not in the PR-bypass list for the branch being released, the
chore(release):push fails and no tag is created, so no build triggers. Check theRelease: Semantic Versionrun log and confirm the App is in the bypass list for bothmainanddev(see Prerequisite).
The merge pre-step (devβmain promotion) merged main into dev and found a change on main that is not a version-file bump and not present on dev. This is the intentional tripwire: something landed directly on main that never came from dev (a manual hotfix commit, an out-of-band edit, etc.).
The run mutates nothing β all assertions precede the first push. Reconcile manually: get that commit onto dev (cherry-pick or PR), then re-dispatch the promotion. The allowlist is CMakeLists.txt and features/**/Shaders/Features/*.ini; only those paths may legitimately differ between main and dev at promotion time.
UNEX_APIKEY is missing, expired, or belongs to an account that doesn't own the mod pages
Regenerate the API key at nexusmods.com β user settings β API keys, update the UNEX_APIKEY repo secret
- A feature has
autoupload = truein its[Nexus]ini section butnexusfilegroupidis not set or is a placeholder - Find the feature's ini file at
features/<Feature Name>/Shaders/Features/<Name>.iniand addnexusfilegroupid = <actual_id>to the[Nexus]section - The file group ID is visible in the Nexus mod page URL when managing files
Reruns are safe. If you need to replace the file, do it manually through the Nexus web UI.
The feature version auto-bump step may have produced a conflict, or the bot account lacks push access to the branch being released. Check the Apply feature version bumps step log. If the bump commit was partially applied, reset the branch to the last good commit and re-run.
Semantic-release's maintenance-branch contract: hotfix/X.Y.x is only valid once main has shipped a release on a newer minor or major. If main is still on the same line as the hotfix branch, the maintenance range is empty and semantic-release refuses to publish. Use the Patch Release Process current-line flow instead β promote on main with ff_target = $(git rev-parse origin/hotfix/X.Y.x^2).
Hotfix-source promotions still require a clean fast-forward: main must be an ancestor of ff_target. You likely passed the hotfix/X.Y.x tip (a merge commit) instead of its second parent. Use:
git rev-parse origin/hotfix/X.Y.x^2Note: dev-source promotions no longer require
mainto be an ancestor offf_target. A divergedmain(from interim hotfixes) is expected and is resolved automatically by the merge pre-step. The validation only enforces thatff_targetis reachable fromdev.
Edit the draft release body directly on GitHub before publishing. The notes are generated from conventional commits; you can add context, screenshots, or upgrade instructions by hand. (The best-effort dedup step only removes already-shipped interim-hotfix entries; it never adds or rewrites other lines.)
The stable build runs from the stable tag, not from an RC tag, so it always produces a fresh build. RC artifacts in GitHub releases are separate and are not reused.
Hotfix branches represent a maintenance line, not a single patch:
hotfix/1.5.x β correct (the 1.5 line)
hotfix/1.5.1 β wrong (semantic-release won't match this)
See Patch Release Process for how patches are cut on either the current line (via ff_target = origin/hotfix/X.Y.x^2 into main) or an older line (semantic-release directly on hotfix/X.Y.x).