├── Citra AddOn
├── citra.addon
├── citra32.addon
├── README.md
├── Citra.fx
└── citra.cpp
├── .github
└── FUNDING.yml
├── README.md
├── mpv
└── looking-glass-mpv.glsl
└── interlaced-shader
├── lookingglass.glsl
└── README.md
/Citra AddOn/citra.addon:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jakedowns/reshade-shaders/HEAD/Citra AddOn/citra.addon
--------------------------------------------------------------------------------
/Citra AddOn/citra32.addon:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jakedowns/reshade-shaders/HEAD/Citra AddOn/citra32.addon
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: jakedowns
4 | patreon: jakedowns
5 | ko_fi: jakedowns
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Citra Looking Glass Portrait Support / Citra ReShade Support
2 |
3 | 
4 |
5 | ### Method 1. Citra Addon + Citra Effect for ReShade:
6 | https://github.com/jakedowns/reshade-shaders/tree/main/Citra%20AddOn
7 |
8 | - more... fluid looking, since you're actually rendering more views
9 | - doesn't cover all depth effects, some UI/Sprites/Transitions etc render oddly
10 | - minor glitches (still a work in progress)
11 |
12 | ### Method 2. Looking Glass Portrait Interlaced Shader for Citra (no ReShade required)
13 | https://github.com/jakedowns/reshade-shaders/tree/main/interlaced-shader
14 |
15 | - better visual fidelity due to it simply displaying Left/Right images exactly as-rendered by 3DS emulator
16 | - more eye strain since it's only 2 views and stereo views on a display that supports 100 is kind of silly (you kind of have to hold your head in a "Sweet-spot" but, it does look good when you're in the sweet-spot)
17 | - option to have 1 sweet-spot (low cross talk, less forgiving for your neck) or repeating sweet-spots (more cross talk, more viable viewing angles)
18 |
--------------------------------------------------------------------------------
/mpv/looking-glass-mpv.glsl:
--------------------------------------------------------------------------------
1 | //!HOOK OUTPUT
2 | //!BIND HOOKED
3 | //!DESC LookingGlass
4 | // ! WIDTH 1536
5 | // ! HEIGHT 2048
6 |
7 | // this is especially made for looking glass portrait.
8 | // TODO: make it more generic / able to support other variations
9 | const float width = 1536.0f;
10 | const float height = 2048.0f;
11 | const float dpi = 324.0f;
12 |
13 |
14 |
15 | // Calibration values
16 | // via: https://jakedowns.github.io/looking-glass-calibration.html
17 | const float slope = -7.083540916442871;
18 | const float center = 0.8167931437492371;
19 | const float pitch = 52.59267044067383;
20 | const float tilt = height / (width * slope);
21 |
22 | const float pitch_adjusted = pitch * width / dpi * cos(atan(1.0f, slope));
23 |
24 | const float subp = 1.0f / (3.0f * width) * pitch_adjusted;
25 | const float repeat = 100.0f/3.0f;
26 |
27 | vec4 my_sample(float alpha){
28 | const vec2 pos = vec2(HOOKED_pos);
29 | const float halfX = pos.x / 2.0f;
30 |
31 | // return HOOKED_texOff(vec2(-alpha,0.0));
32 |
33 | if(fract(alpha) < .5){
34 | // if(mod(fract(alpha)*100.0f,repeat) < repeat*0.5){
35 | // if(pos.x < 0.5){
36 | // right eye (right half of SBS);
37 | // return vec4(0.0,0.0,1.0,1.0);
38 | return HOOKED_tex(vec2(0.5 + halfX, pos.y));
39 | }
40 | // left eye (left half of SBS)
41 | // return vec4(0.0);
42 | // return vec4(0.0,0.0,1.0,1.0);
43 | return HOOKED_tex(vec2(halfX, pos.y));
44 | }
45 |
46 | vec4 hook(){
47 |
48 | vec4 myColor = vec4(0.0,0.0,0.0,0.1);//HOOKED_tex(HOOKED_pos);
49 |
50 | // float alpha = (HOOKED_pos.x + (1.0-HOOKED_pos.y) * tilt) * pitch_adjusted - center;
51 | float alpha = (HOOKED_pos.x + (1.0-HOOKED_pos.y) * slope) * pitch_adjusted - center;
52 |
53 | // This makes a perfect red/cyan filter somehow
54 | // float alpha = gl_FragCoord.x; // + gl_FragCoord.y;
55 |
56 | // we sample 3 times since the r,g,b subpixels for each "original" pixel needs to be additionally shifted by one extra "subpixel" amount per channel to match the unique sub-pixel layout of the LKGP display
57 | // myColor = my_sample(alpha);
58 | myColor.r = my_sample(alpha).r;
59 | myColor.g = my_sample(alpha + subp).g;
60 | myColor.b = my_sample(alpha + 2.0f * subp).b;
61 |
62 | return myColor;
63 | }
64 |
--------------------------------------------------------------------------------
/interlaced-shader/lookingglass.glsl:
--------------------------------------------------------------------------------
1 | // Citra 3DS Looking Glass Portrait Interlacing Shader
2 | // September 24, 2022
3 | // Created by Holophone3D and Jake Downs
4 | // inspired by Ian Reese & Yann Vernier (lonetech)
5 | // original source https://github.com/jakedowns/reshade-shaders/tree/main/interlaced-shader/
6 |
7 | // Calibration values
8 | // via: https://jakedowns.github.io/looking-glass-calibration.html
9 | float slope = -7.083540916442871;
10 | float center = 0.8167931437492371;
11 | float pitch = 52.59267044067383;
12 |
13 | // this is especially made for looking glass portrait.
14 | // TODO: make it more generic / able to support other variations
15 | float width = o_resolution.y;
16 | float height = o_resolution.x;
17 | float dpi = 324.0f;
18 |
19 | float tilt = height / (width * slope);
20 | float pitch_adjusted = pitch * width / dpi * cos(atan(1.0, slope));
21 | float subp = 1.0 / (3.0f * width) * pitch_adjusted;
22 | float repeat = 100/2;
23 |
24 | vec4 my_sample(float alpha){
25 | // sample right eye
26 |
27 | // one-shot mode
28 | if(fract(alpha) > 0.145){
29 | // if(alpha < -100){
30 | // repeated mode
31 | //if(mod(fract(alpha)*100,repeat) >= repeat*0.5){
32 | // return vec4(1,0,0,1); // debug color
33 | return texture(color_texture_r, frag_tex_coord);
34 | }
35 | // sample left eye
36 | // return vec4(0,0,1,1); // debug color
37 | return texture(color_texture, frag_tex_coord);
38 | }
39 |
40 | void main() {
41 | // if(screen == 1){
42 | // // bottom screen
43 | // // straight texture read, nothing special to do
44 | // color = texture(color_texture, frag_tex_coord);
45 | // }else{
46 | // top screen (3d interlaced for lenticular display)
47 |
48 | // alpha is... the offset for the subpixels for the current uv coordinate in the final interlaced texture,
49 | // which we assume is being drawn full screen, so 1536px wide
50 |
51 | // x and y are intentionally swapped here cause, 3DS LCDs are rotated, and the emulator maintains that
52 | // generate using our normalized uv
53 | float alpha = ( frag_tex_coord.y + frag_tex_coord.x * tilt ) * pitch_adjusted - center;
54 |
55 | // we sample 3 times since the r,g,b subpixels for each "original" pixel needs to be additionally shifted by one extra "subpixel" amount per channel to match the unique sub-pixel layout of the LKGP display
56 | color.r = my_sample(alpha).r;
57 | color.g = my_sample(alpha + subp).g;
58 | color.b = my_sample(alpha + 2.0f * subp).b;
59 | // }
60 | }
61 |
--------------------------------------------------------------------------------
/interlaced-shader/README.md:
--------------------------------------------------------------------------------
1 | ## Looking Glass Portrait interlaced shader for Citra 3DS Emulator
2 |
3 | ### Basic Setup
4 |
5 | 1. In Citra; File > Open Citra Folder...
6 |
7 | 2. Go to Shaders directory and right-click to create a new text file called `lookingglass.glsl`
8 |
9 | 3. copy the contents of [lookingglass.glsl](./lookingglass.glsl) from this repo
10 |
11 | 4. Next, you need your calibration info.
12 |
13 | > Each LKGP is uniquely calibrated, and those values need to be specified in the shader to get the correct output. In future versions, I might automate this step.
14 | > Go to this URL and copy your Looking Glass Portrait's Calibration Data: https://jakedowns.github.io/looking-glass-calibration.html
15 | > NOTE: you can manually grab the values if you connect to your LKGP via USB and open it's internal storage in the explorer and navigate to `LKG_calibration/visual.json`
16 |
17 | 5. in `lookingglass.glsl` replace my calibration values with yours
18 |
19 | > **Note** you'll need at least Citra Nightly 1788 (2022-09-25) for this interlaced shader to load correctly.
20 | > If you're using a mainline or nightly older than 2022-09-25, you'll need to perform the following extra step to allow the shader to load:
21 | >
22 | > copy your shader to /anaglyph/lookingglass.glsl subfolder within the shaders folder
23 | > there's a bug where the shader file needs to be in two places to load properly citra-emu/citra#6133
24 |
25 | 6. Go to Emulation > Configure > Graphics
26 |
27 | 7. Set Stereoscopy > 3D Mode to Interlaced
28 |
29 | 8. Select Post-Processing Filter: lookingglass
30 |
31 | 9. enjoy! Let me know how it goes! https://twitter.com/jakedowns/status/1573681327005573120
32 |
33 | ### Advanced
34 |
35 | if you want to try dialing in the sweet spot yourself,
36 | you can try altering the shader, you can press F9 or F10 or F11 in citra to reload the shader without restarting.
37 |
38 | if it crashes, you've introduced a syntax error into the shader. undo and try again.
39 |
40 | The main values to be tweaked are:
41 | `float repeat = 100/3;`
42 | > // the number of times the LL/RR stereo pair will be repeated across the full viewing angle.
43 | > More repeats (100/4, 100/5) = more cross-talk, but more flexibility in viewing angles.
44 | > Lower divisions (100/1, 100/2) = less cross-talk, but there's only 1 or two "sweet spots"
45 |
46 | `< repeat*0.5`
47 | you can change this `0.5` to kind of... shift or rotate the sweet spot left or right
48 |
49 | ### Misc.
50 |
51 | If the looking-glass-calibration url above doesn't work, here's a fallback version on codesandbox:
52 |
53 | https://codesandbox.io/s/floral-flower-w2sjmx?file=/index.html
54 |
55 | If that doesn't work, try restarting Holoplay Service
56 |
57 | ### Related
58 |
59 | If you want to try something a little more advanced, check out my [Citra addon for ReShade, ReGlass & Refract](https://github.com/jakedowns/reshade-shaders/tree/main/Citra%20AddOn)
60 |
--------------------------------------------------------------------------------
/Citra AddOn/README.md:
--------------------------------------------------------------------------------
1 | ## Citra Add-On + Citra.fx Effect for ReShade
2 |
3 | ### 🚧 \*\* **Work in Progress** \*\*
4 |
5 | **Easier Alternative for Beginners** if ReShade is a little too advanced for you, try starting with the shader-only setup. No ReShade needed, just Citra + a custom [Looking Glass Interlacing Shader for Citra](https://github.com/jakedowns/reshade-shaders/tree/main/interlaced-shader)
6 |
7 | ### Features:
8 | - normalizes Citra's depth buffers to be usable by other existing ReShade effects
9 | - rotates depth buffer so x,y uv coordinates are correct
10 | - inverts depth values so bright values are near and dark values are far
11 | - supports setting a fixed depth for the bottom screen when in split-screen mode
12 |
13 | ### How:
14 | - basically a clone of the Generic Depth addon, that remaps all calls from other effects/addons looking to access the `DEPTH` texture to instead access a pre-processed depth texture, specially modified to normalize it from Citra's emulation-specific values to something more standardly consumable by other shader effects pipelines
15 |
16 | ### Installation:
17 |
18 | > **Note** This addon requires you to install the version of reShade with full add on support
19 | 
20 |
21 | > **Note** in order to test this, you'll need a custom build of Citra. You can build it yourself from this [tagged commit](https://github.com/jakedowns/citra-fix-custom-interlaced-shader-path/tree/reshade-left-eye-optional), or [download the citra-qt.exe from the release page here](https://github.com/jakedowns/citra-fix-custom-interlaced-shader-path/releases/tag/reshade-left-eye-optional)
22 | >
23 | > Hopefully the pull request to fix this gets merged: https://github.com/citra-emu/citra/pull/6140
24 | > or, i'll find another way to capture the left-eye's depth buffer to negate the need for this patch
25 |
26 |
27 | 1. download [`citra.addon`](./citra.addon) (64-bit) OR [`citra32.addon`](./citra32.addon) (32-bit) into the same directory as `ReShade.ini` and `citra-qt.exe`
28 | 2. place `Citra.fx` in `./reshade-shaders/Shaders/` sub-directory within the Citra executable folder
29 | 4. start citra. (if it crashes or reshade doesn't launch, disable generic depth, edit ReShade.ini:
30 | ```
31 | [ADDON]
32 | DisabledAddons=Generic Depth
33 | ```
34 | 5. start citra. press `home` to bring up ReShade **Important** Disable the `Generic Depth` add-on
35 | 6. make sure `Citra` is enabled in the add-ons tab
36 | 7. make sure the `Citra` effect is enabled in the home tab, and that it's at the top of the effects list
37 |
38 |
39 |
40 |
41 | ### Recommended / Tested Effects:
42 |
43 | - *Looking Glass Portrait Support:*
44 | - [ReGlass](https://github.com/jbienz/ReGlass) aka [LookingGlass.fx](https://github.com/jbienz/ReGlass/blob/main/Shaders/LookingGlass.fx)
45 | - with [Refract](https://github.com/SolerSoft/Refract)
46 |
47 | - [MXAO.fx](https://github.com/cyrie/Stormshade/blob/master/reshade-shaders/Shaders/MXAO.fx)
48 | - [CinematicDOF.fx](https://github.com/FransBouma/OtisFX/blob/master/Shaders/CinematicDOF.fx)
49 | - mcflypg / Pascal Gilcher's ReShade Ray Tracing shader (RTGI) - https://www.patreon.com/mcflypg
50 |
51 | ### Known Issues
52 |
53 | - currently, if you have too many, or too intense fx enabled, or resolution too high, you might see flickering.
54 |
55 | this is due to the fact that many games share a single depth buffer. i'm still working on a solution to prevent this.
56 |
57 | - currently the "left, right, small" side-by-side modes are unfinished and require manual offset adjustments
58 |
59 | - scaling adjustments and aspect adjustments need re-enabled
60 |
61 | - certain window dimensions require a lot of manual offset adjustments, i'm working on automating these
62 |
63 | ### Credits
64 | - [crosire](https://github.com/crosire/reshade) - Big thanks for giving us the guidance to get this add-on working
65 | - [Edgarska](https://www.reddit.com/r/Citra/comments/i4o5i1/reshade_depth_buffer_access_fix/) - their reddit post in /r/ReShade, [Reshade depth buffer access fix.](https://www.reddit.com/r/Citra/comments/i4o5i1/reshade_depth_buffer_access_fix/) gave us a lot of the basics we needed to get this working. I simply packaged them up.
66 | - Holophone3D - thanks for the motivation and inspiration to get this working. also thanks for getting the ball rolling with the basics for the shader / fx code.
67 | - [jbienz](https://github.com/jbienz) / [SolerSoft](https://github.com/SolerSoft) - thanks for your enthusiasm and encouragement, and for your great work on ReGlass & Refract
68 | - emufan4568 - thanks for your help on the Citra discord #development channel
69 | - jakedowns - follow me on twitter.com/jakedowns for future updates | support my dev work on patreon.com/jakedownsthings | follow my art work on instagram.com/jakedownsthings
70 |
71 | ### Video
72 |
73 |
74 |
75 | https://user-images.githubusercontent.com/1683122/196085074-107558b8-6426-4692-a38a-8b72487b69ec.mp4
76 |
77 |
78 |
79 | ### Images
80 | 
81 | 
82 | 
83 |
84 |
--------------------------------------------------------------------------------
/Citra AddOn/Citra.fx:
--------------------------------------------------------------------------------
1 | /*
2 | Citra by Holophone3D, Jake Downs, Jared Bienz.
3 | Recommended to be used with:
4 | https://github.com/jbienz/ReGlass & https://github.com/SolerSoft/Refract
5 |
6 | Preprocess depth map textures from Citra so they can be used by other add-ons
7 | - pre-swap (un-rotate) depth buffer xy coordinates
8 |
9 | */
10 |
11 | #include "ReShade.fxh"
12 |
13 | // -- Options --
14 |
15 | uniform float BUFFER_AR = BUFFER_WIDTH / BUFFER_HEIGHT;
16 |
17 | uniform int iUIBottomScreenPosition <
18 | ui_type = "combo";
19 | ui_label = "Bottom Screen Position";
20 | ui_category = "Bottom Screen";
21 | ui_items = "Bottom\0"
22 | "Top\0"
23 | "Left\0"
24 | "Right\0"
25 | "Disabled\0";
26 | > = 0;
27 |
28 | uniform float fUIBottomFocus <
29 | ui_type = "drag";
30 | ui_label = "Bottom Screen Focus";
31 | ui_category = "Bottom Screen";
32 | ui_tooltip = "Adjust bottom screen near/far focus.\n";
33 | ui_min = 0.0; ui_max = 1.0;
34 | ui_step = 0.05;
35 | > = 0.5;
36 |
37 | uniform bool bUIPreviewDepth <
38 | ui_category = "Preview Depth Buffer";
39 | ui_label = "Preview Depth Buffer";
40 | > = false;
41 |
42 | uniform float bUIPreviewAlpha <
43 | ui_type = "drag";
44 | ui_category = "Preview Depth Buffer";
45 | ui_label = "Preview Alpha";
46 | ui_min = 0.0; ui_max = 1.0;
47 | > = 1.0;
48 |
49 | uniform float fUINearPlane <
50 | ui_type = "drag";
51 | ui_label = "Near Plane";
52 | ui_category = "Depth";
53 | ui_tooltip = "RESHADE_DEPTH_LINEARIZATION_FAR_PLANE=";
54 | ui_min = 0.0; ui_max = 1000.0;
55 | ui_step = 0.01;
56 | > = 0.0;
57 |
58 | uniform float fUIFarPlane <
59 | ui_type = "drag";
60 | ui_label = "Far Plane";
61 | ui_category = "Depth";
62 | ui_tooltip = "RESHADE_DEPTH_LINEARIZATION_FAR_PLANE=";
63 | ui_min = 0.0; ui_max = 1000.0;
64 | ui_step = 0.001;
65 | > = 0.01;
66 |
67 | uniform float fUIDepthMultiplier <
68 | ui_type = "drag";
69 | ui_label = "Multiplier";
70 | ui_category = "Depth";
71 | ui_tooltip = "RESHADE_DEPTH_MULTIPLIER=";
72 | ui_min = 0.0; ui_max = 1000.0;
73 | ui_step = 0.001;
74 | > = 1.0;
75 |
76 | // -- Aspect Options --
77 |
78 | uniform bool bUIUseCustomAspectRatio <
79 | ui_label = "Use Custom Aspect Ratio";
80 | ui_category = "Aspect ratio";
81 | > = false;
82 |
83 | uniform float AspectRatio <
84 | ui_type = "drag";
85 | ui_label = "Correct proportions";
86 | ui_category = "Aspect ratio";
87 | ui_min = -4.0; ui_max = 4.0;
88 | ui_step = 0.01;
89 | > = 1.0;
90 |
91 | uniform float ScaleX <
92 | ui_type = "drag";
93 | ui_label = "Scale image X";
94 | ui_category = "Aspect ratio";
95 | ui_min = 0.0; ui_max = 4.0;
96 | ui_step = 0.001;
97 | > = 1.0;
98 |
99 | uniform float ScaleY <
100 | ui_type = "drag";
101 | ui_label = "Scale image Y";
102 | ui_category = "Aspect ratio";
103 | ui_min = 0.0; ui_max = 4.0;
104 | ui_step = 0.001;
105 | > = 1.0;
106 |
107 | uniform float fUIDepthXOffset <
108 | ui_label = "Offset X Relative";
109 | ui_category = "Offsets";
110 | ui_type = "drag";
111 | ui_step = 0.001;
112 | > = 0.0;
113 |
114 | // uniform float fUIDepthXPxOffset <
115 | // ui_label = "Offset X in pixels";
116 | // ui_category = "Offsets";
117 | // ui_type = "drag";
118 | // > = 0.0;
119 |
120 | uniform float fUIDepthYOffset <
121 | ui_label = "Offset Y Relative";
122 | ui_category = "Offsets";
123 | ui_type = "drag";
124 | ui_step = 0.001;
125 | > = 0.0;
126 |
127 | // uniform float fUIDepthYPxOffset <
128 | // ui_label = "Offset Y in pixels";
129 | // ui_category = "Offsets";
130 | // ui_type = "drag";
131 | // > = 0.0;
132 |
133 | uniform bool FitScreen <
134 | ui_label = "Scale image to borders";
135 | ui_category = "Aspect ratio";
136 | > = true;
137 |
138 | uniform float4 Color <
139 | ui_label = "Background color";
140 | ui_category = "Aspect ratio";
141 | ui_type = "color";
142 | > = float4(0.027, 0.027, 0.027, 0.17);
143 |
144 | // uniform int iUIPresentType <
145 | // ui_type = "combo";
146 | // ui_label = "Present type";
147 | // ui_items = "Depth map\0"
148 | // "Normal map\0"
149 | // "Show both (Vertical 50/50)\0";
150 | // > = 2;
151 |
152 | texture OrigDepthTex : ORIG_DEPTH;
153 | sampler OrigDepth{ Texture = OrigDepthTex; };
154 |
155 | texture ModifiedDepthTex{ Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R32F; };
156 |
157 | // float3 AspectRatioPS(
158 | // float4 pos : SV_Position,
159 | // float2 texcoord : TEXCOORD,
160 | // float aspect,
161 | // float zoom,
162 | // bool fitscreen,
163 | // int depth
164 | // ) : SV_Target
165 | // {
166 | // bool mask = false;
167 |
168 | // // Center coordinates
169 | // float2 coord = texcoord-0.5;
170 |
171 | // // if (Zoom != 1.0) coord /= Zoom;
172 | // if (zoom != 1.0) coord /= clamp(zoom, 1.0, 1.5); // Anti-cheat
173 |
174 | // // Squeeze horizontally
175 | // if (aspect<0)
176 | // {
177 | // coord.x *= abs(aspect)+1.0; // Apply distortion
178 |
179 | // // Scale to borders
180 | // if (fitscreen) coord /= abs(aspect)+1.0;
181 | // else // mask image borders
182 | // mask = abs(coord.x)>0.5;
183 | // }
184 | // // Squeeze vertically
185 | // else if (aspect>0)
186 | // {
187 | // coord.y *= aspect+1.0; // Apply distortion
188 |
189 | // // Scale to borders
190 | // if (fitscreen) coord /= abs(aspect)+1.0;
191 | // else // mask image borders
192 | // mask = abs(coord.y)>0.5;
193 | // }
194 |
195 | // // Coordinates back to the corner
196 | // coord += 0.5;
197 |
198 | // // Sample display image and return
199 | // if(mask){
200 | // return Color.rgb;
201 | // }
202 |
203 | // return depth ? tex2D(OrigDepth, coord).rgb : tex2D(ReShade::BackBuffer, coord).rgb;
204 | // }
205 |
206 | bool isBottomScreenPx(float2 input_tex) {
207 |
208 | // disabled? return false
209 | if (iUIBottomScreenPosition == 4) {
210 | return false;
211 | }
212 |
213 | bool isBottomScreenPixel = false;
214 | if (iUIBottomScreenPosition == 0) {
215 | // bottom
216 | isBottomScreenPixel = input_tex.x > 0.5;
217 | } else if (iUIBottomScreenPosition == 1) {
218 | // top
219 | isBottomScreenPixel = input_tex.x < 0.5;
220 | } else if (iUIBottomScreenPosition == 2) {
221 | // left
222 | isBottomScreenPixel = input_tex.y > 0.55546875;
223 | } else if (iUIBottomScreenPosition == 3) {
224 | // right
225 | isBottomScreenPixel = input_tex.y < 1.0 - 0.55546875;
226 | }
227 | // else if (iUIBottomScreenPosition == 4) {
228 | // // right > small
229 | // isBottomScreenPixel = input_tex.y > 0.83333333;
230 | // }
231 |
232 | // return false;
233 | return isBottomScreenPixel;
234 | }
235 |
236 | float2 scaleCoordinates(float2 mytexcoord){
237 |
238 | int2 depthSize = tex2Dsize(OrigDepth).yx;
239 | int2 bufferSize = int2(BUFFER_WIDTH, BUFFER_HEIGHT);
240 |
241 | float scaled_width, scaled_height, max_scaled_width, max_scaled_height;
242 | if(
243 | iUIBottomScreenPosition == 0
244 | || iUIBottomScreenPosition == 1
245 | ){
246 | // over/under top screen scaled relative dimensions
247 | // 400 * 240 (really 400 x 480)
248 | // 400 scaled width
249 | // --- x ---
250 | // 240 scaled height (BUFFER_HEIGHT/2)
251 | scaled_width = BUFFER_WIDTH < BUFFER_HEIGHT ? BUFFER_WIDTH : (BUFFER_HEIGHT/2) * 400 / 240;
252 | scaled_height = BUFFER_HEIGHT <= BUFFER_WIDTH ? BUFFER_HEIGHT/2 : BUFFER_WIDTH * 240 / 400;
253 | }
254 | else if(
255 | iUIBottomScreenPosition == 2
256 | || iUIBottomScreenPosition == 3
257 | ){
258 | // sbs 400+320 by 240
259 | // 720 scaled width
260 | // --- x ---
261 | // 240 scaled height
262 | max_scaled_height = BUFFER_WIDTH * 240 / 720;
263 | scaled_height = BUFFER_HEIGHT < max_scaled_height ? BUFFER_HEIGHT : max_scaled_height;
264 | max_scaled_width = BUFFER_HEIGHT < max_scaled_height ? BUFFER_HEIGHT * 720 / 240 : BUFFER_WIDTH;
265 | scaled_width = max_scaled_width * 0.5556; // 400 / 720
266 | }
267 | else if(
268 | iUIBottomScreenPosition == 4
269 | ){
270 | max_scaled_width = BUFFER_HEIGHT * 400 / 240;
271 | scaled_width = BUFFER_WIDTH > max_scaled_width ? max_scaled_width : BUFFER_WIDTH;
272 |
273 | max_scaled_height = BUFFER_WIDTH * 240 / 400;
274 | scaled_height = BUFFER_HEIGHT > max_scaled_height ? max_scaled_height : BUFFER_HEIGHT;
275 | }
276 |
277 | // map FULL BUFFER coordinate system down to just where the top screen is, relatively within the buffer
278 | // so that when the depth map is sampled it's contents align to where the rgb top screen is rendered within the output buffer
279 |
280 | if(iUIBottomScreenPosition == 0){
281 | // top screen is on top, bottom screen is on bottom
282 |
283 | // horizontal is still controlled by "y" here even tho i created this coord as .yx
284 | mytexcoord.y -= .5;
285 | mytexcoord.y /= scaled_width / BUFFER_WIDTH;
286 | mytexcoord.y += .5;
287 |
288 | // vert
289 | mytexcoord.x -= 0.5;
290 | mytexcoord.x /= scaled_height / BUFFER_HEIGHT;
291 | }
292 | else if(iUIBottomScreenPosition == 1){
293 | // top screen is below bottom screen
294 |
295 | // horizontal is still controlled by "y" here even tho i created this coord as .yx
296 | mytexcoord.y -= .5;
297 | mytexcoord.y /= scaled_width / BUFFER_WIDTH;
298 | mytexcoord.y += .5;
299 |
300 | // vert
301 | mytexcoord.x -= 0.5;
302 | mytexcoord.x /= scaled_height / BUFFER_HEIGHT;
303 | mytexcoord.x += 1.0;
304 |
305 | }
306 | else if(iUIBottomScreenPosition == 2){
307 | // top screen is left of bottom screen
308 | mytexcoord.y -= .5;
309 | mytexcoord.y /= scaled_width / BUFFER_WIDTH;
310 | mytexcoord.y += .1;
311 |
312 | mytexcoord.x -= .5;
313 | mytexcoord.x /= max_scaled_height / BUFFER_HEIGHT;
314 | mytexcoord.x += .5;
315 | }
316 | else if(iUIBottomScreenPosition == 3){
317 | // top screen is right of bottom screen
318 |
319 | mytexcoord.y -= .5;
320 | mytexcoord.y /= scaled_width / BUFFER_WIDTH;
321 | mytexcoord.y += .9;
322 |
323 | mytexcoord.x -= .5;
324 | mytexcoord.x /= max_scaled_height / BUFFER_HEIGHT;
325 | mytexcoord.x += .5;
326 | }
327 | // else if(iUIBottomScreenPosition == 4){
328 | // // // right small
329 | // // texcoord.y = texcoord.y * 1.22;
330 | // // texcoord.y = texcoord.y - (1.0 - 0.83333333);
331 | // }
332 | else {
333 | // fullscreen
334 |
335 | mytexcoord.y -= .5;
336 | mytexcoord.y /= scaled_width / BUFFER_WIDTH;
337 | mytexcoord.y += .5;
338 |
339 |
340 | mytexcoord.x -= .5;
341 | mytexcoord.x /= scaled_height / BUFFER_HEIGHT;
342 | mytexcoord.x += .5;
343 |
344 | }
345 |
346 | // global scaling
347 | // texcoord.x = texcoord.x / ScaleY;
348 | // texcoord.y = texcoord.y / ScaleX;
349 |
350 | // if(bUIUseCustomAspectRatio){
351 | // texcoord.x = texcoord.x * AspectRatio;
352 | // texcoord.y = texcoord.y * AspectRatio;
353 | // }
354 | // else
355 | // texcoord.y = texcoord.y * BUFFER_AR;
356 |
357 | return mytexcoord;
358 | }
359 |
360 | float GetModDepth(float2 tex : TEXCOORD) {
361 | float2 input_tex = float2(tex.y, tex.x);
362 | float2 mytex = 1.0 - float2(tex.y, tex.x);
363 |
364 | if (isBottomScreenPx(input_tex)){
365 | return fUIBottomFocus;
366 | }
367 |
368 | mytex = scaleCoordinates(mytex);
369 |
370 | // if(bUIDepthIsUpsideDown){
371 | // tex.y = 1.0 - tex.y;
372 | // }
373 |
374 | // mytex.x /= fUIDepthXScale;
375 | // mytex.y /= fUIDepthYScale;
376 |
377 | // if(fUIDepthXOffset){
378 | // mytex.y += fUIDepthXOffset / 2.000000001;
379 | // }
380 | // // else if(fUIDepthXPxOffset){
381 | // // mytex.x -= fUIDepthXPxOffset * BUFFER_RCP_WIDTH;
382 | // // }
383 |
384 | // if(fUIDepthYOffset){
385 | // mytex.x -= fUIDepthYOffset / 2.000000001;
386 | // }
387 | // // else if(fUIDepthYPxOffset){
388 | // // mytex.y += fUIDepthYPxOffset * BUFFER_RCP_HEIGHT;
389 | // // }
390 |
391 |
392 | float depth = tex2Dlod(OrigDepth, float4(mytex, 0, 0)).x * fUIDepthMultiplier;
393 |
394 | // if(bUIDepthIsLog){
395 | // const float C = 0.01;
396 | // depth = (exp(depth * log(C + 1.0)) - 1.0) / C;
397 | // }
398 |
399 | // invert by default for citra
400 | depth = 1.0 - depth;
401 | // if(!bUIDepthIsReversed){
402 | // depth = 1.0 - depth;
403 | // }
404 |
405 | const float N = 1.0;
406 | depth /= fUIFarPlane - depth * (fUIFarPlane - N);
407 |
408 | return depth;
409 | }
410 |
411 | float4 MyPS(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET {
412 | float depth = GetModDepth(tex);
413 | return float4(depth.xxx,1.0);
414 | }
415 |
416 | float4 PreviewDepth(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET {
417 | if(bUIPreviewDepth){
418 | float depth = GetModDepth(tex);
419 | return lerp(tex2D(ReShade::BackBuffer, tex), float4(depth.xxx,1.0), bUIPreviewAlpha);
420 | }
421 | return tex2D(ReShade::BackBuffer, tex);
422 | }
423 |
424 |
425 | // FullscreenVS
426 | technique Citra {
427 | pass {
428 | VertexShader = PostProcessVS;
429 | PixelShader = MyPS;
430 | RenderTarget = ModifiedDepthTex;
431 | }
432 | pass {
433 | VertexShader = PostProcessVS;
434 | PixelShader = PreviewDepth;
435 | }
436 | }
437 |
--------------------------------------------------------------------------------
/Citra AddOn/citra.cpp:
--------------------------------------------------------------------------------
1 | /*
2 | * 2022 Jake Downs
3 | */
4 |
5 | /*
6 | *
7 | * Based on generic_depth
8 | * Copyright (C) 2021 Patrick Mours
9 | * SPDX-License-Identifier: BSD-3-Clause
10 | */
11 |
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 | #include
19 | #include
20 |
21 | using namespace reshade::api;
22 |
23 | static std::shared_mutex s_mutex;
24 |
25 | static bool s_disable_intz = false;
26 | // Enable or disable the creation of backup copies at clear operations on the selected depth-stencil
27 | static unsigned int s_preserve_depth_buffers = 0;
28 | // Enable or disable the aspect ratio check from 'check_aspect_ratio' in the detection heuristic
29 | static unsigned int s_use_aspect_ratio_heuristics = 0;
30 |
31 | enum class clear_op
32 | {
33 | clear_depth_stencil_view,
34 | fullscreen_draw,
35 | unbind_depth_stencil_view,
36 | };
37 |
38 | struct draw_stats
39 | {
40 | uint32_t vertices = 0;
41 | uint32_t drawcalls = 0;
42 | uint32_t drawcalls_indirect = 0;
43 | viewport last_viewport = {};
44 | };
45 | struct clear_stats : public draw_stats
46 | {
47 | clear_op clear_op = clear_op::clear_depth_stencil_view;
48 | bool copied_during_frame = false;
49 | };
50 |
51 | struct depth_stencil_info
52 | {
53 | draw_stats total_stats;
54 | draw_stats current_stats; // Stats since last clear operation
55 | std::vector clears;
56 | bool copied_during_frame = false;
57 | };
58 |
59 | struct depth_stencil_hash
60 | {
61 | inline size_t operator()(resource value) const
62 | {
63 | // Simply use the handle (which is usually a pointer) as hash value (with some bits shaved off due to pointer alignment)
64 | return static_cast(value.handle >> 4);
65 | }
66 | };
67 |
68 | struct __declspec(uuid("ad059cc1-c3ad-4cef-a4a9-401f672c6c37")) state_tracking
69 | {
70 | viewport current_viewport = {};
71 | resource current_depth_stencil = { 0 };
72 | std::unordered_map counters_per_used_depth_stencil;
73 | bool first_draw_since_bind = true;
74 | draw_stats best_copy_stats;
75 |
76 | state_tracking()
77 | {
78 | // Reserve some space upfront to avoid rehashing during command recording
79 | counters_per_used_depth_stencil.reserve(32);
80 | }
81 |
82 | void reset()
83 | {
84 | reset_on_present();
85 | current_depth_stencil = { 0 };
86 | }
87 | void reset_on_present()
88 | {
89 | best_copy_stats = { 0, 0 };
90 | counters_per_used_depth_stencil.clear();
91 | }
92 |
93 | void merge(const state_tracking &source)
94 | {
95 | // Executing a command list in a different command list inherits state
96 | current_depth_stencil = source.current_depth_stencil;
97 |
98 | if (source.best_copy_stats.vertices >= best_copy_stats.vertices)
99 | best_copy_stats = source.best_copy_stats;
100 |
101 | if (source.counters_per_used_depth_stencil.empty())
102 | return;
103 |
104 | counters_per_used_depth_stencil.reserve(source.counters_per_used_depth_stencil.size());
105 | for (const auto &[depth_stencil_handle, snapshot] : source.counters_per_used_depth_stencil)
106 | {
107 | depth_stencil_info &target_snapshot = counters_per_used_depth_stencil[depth_stencil_handle];
108 | target_snapshot.total_stats.vertices += snapshot.total_stats.vertices;
109 | target_snapshot.total_stats.drawcalls += snapshot.total_stats.drawcalls;
110 | target_snapshot.total_stats.drawcalls_indirect += snapshot.total_stats.drawcalls_indirect;
111 | target_snapshot.current_stats.vertices += snapshot.current_stats.vertices;
112 | target_snapshot.current_stats.drawcalls += snapshot.current_stats.drawcalls;
113 | target_snapshot.current_stats.drawcalls_indirect += snapshot.current_stats.drawcalls_indirect;
114 |
115 | target_snapshot.clears.insert(target_snapshot.clears.end(), snapshot.clears.begin(), snapshot.clears.end());
116 |
117 | target_snapshot.copied_during_frame |= snapshot.copied_during_frame;
118 | }
119 | }
120 | };
121 |
122 | struct __declspec(uuid("7c6363c7-f94e-437a-9160-141782c44a98")) generic_depth_data
123 | {
124 | // The depth-stencil resource that is currently selected as being the main depth target
125 | resource selected_depth_stencil = { 0 };
126 |
127 | // Resource used to override automatic depth-stencil selection
128 | resource override_depth_stencil = { 0 };
129 |
130 | // The current depth shader resource view bound to shaders
131 | // This can be created from either the selected depth-stencil resource (if it supports shader access) or from a backup resource
132 | resource_view selected_shader_resource = { 0 };
133 |
134 | // True when the shader resource view was created from the backup resource, false when it was created from the original depth-stencil
135 | bool using_backup_texture = false;
136 |
137 | std::unordered_map display_count_per_depth_stencil;
138 | };
139 |
140 | struct depth_stencil_backup
141 | {
142 | // The number of effect runtimes referencing this backup
143 | size_t references = 1;
144 |
145 | // A resource used as target for a backup copy of this depth-stencil
146 | resource backup_texture = { 0 };
147 |
148 | // The depth-stencil that should be copied from
149 | resource depth_stencil_resource = { 0 };
150 |
151 | // Set to zero for automatic detection, otherwise will use the clear operation at the specific index within a frame
152 | size_t force_clear_index = 0;
153 |
154 | // Frame dimensions of the last effect runtime this backup was used with
155 | uint32_t frame_width = 0;
156 | uint32_t frame_height = 0;
157 | };
158 |
159 | struct __declspec(uuid("e006e162-33ac-4b9f-b10f-0e15335c7bdb")) generic_depth_device_data
160 | {
161 | // List of queues created for this device
162 | std::vector queues;
163 |
164 | // List of resources that were deleted this frame
165 | std::vector destroyed_resources;
166 |
167 | // List of resources that are enqueued for delayed destruction in the future
168 | std::vector> delayed_destroy_resources;
169 |
170 | // List of all encountered depth-stencils of the last frame
171 | std::vector> current_depth_stencil_list;
172 |
173 | // List of depth-stencils that should be tracked throughout each frame and potentially be backed up during clear operations
174 | std::vector depth_stencil_backups;
175 |
176 | depth_stencil_backup *find_depth_stencil_backup(resource resource)
177 | {
178 | for (depth_stencil_backup &backup : depth_stencil_backups)
179 | if (backup.depth_stencil_resource == resource)
180 | return &backup;
181 | return nullptr;
182 | }
183 |
184 | depth_stencil_backup *track_depth_stencil_for_backup(device *device, resource resource, resource_desc desc)
185 | {
186 | const auto it = std::find_if(depth_stencil_backups.begin(), depth_stencil_backups.end(),
187 | [resource](const depth_stencil_backup &existing) { return existing.depth_stencil_resource == resource; });
188 | if (it != depth_stencil_backups.end())
189 | {
190 | it->references++;
191 | return &(*it);
192 | }
193 |
194 | depth_stencil_backup &backup = depth_stencil_backups.emplace_back();
195 | backup.depth_stencil_resource = resource;
196 |
197 | desc.type = resource_type::texture_2d;
198 | desc.heap = memory_heap::gpu_only;
199 | desc.usage = resource_usage::shader_resource | resource_usage::copy_dest;
200 |
201 | if (device->get_api() == device_api::d3d9)
202 | desc.texture.format = format::r32_float; // D3DFMT_R32F, since INTZ does not support D3DUSAGE_RENDERTARGET which is required for copying
203 | // Use depth format as-is in OpenGL and Vulkan, since those are valid for shader resource views there
204 | else if (device->get_api() != device_api::opengl && device->get_api() != device_api::vulkan)
205 | desc.texture.format = format_to_typeless(desc.texture.format);
206 |
207 | // First try to revive a backup resource that was previously enqueued for delayed destruction
208 | for (auto delayed_destroy_it = delayed_destroy_resources.begin(); delayed_destroy_it != delayed_destroy_resources.end(); ++delayed_destroy_it)
209 | {
210 | const resource_desc delayed_destroy_desc = device->get_resource_desc(delayed_destroy_it->first);
211 |
212 | if (desc.texture.width == delayed_destroy_desc.texture.width && desc.texture.height == delayed_destroy_desc.texture.height && desc.texture.format == delayed_destroy_desc.texture.format)
213 | {
214 | backup.backup_texture = delayed_destroy_it->first;
215 | delayed_destroy_resources.erase(delayed_destroy_it);
216 | return &backup;
217 | }
218 | }
219 |
220 | if (device->create_resource(desc, nullptr, resource_usage::copy_dest, &backup.backup_texture))
221 | device->set_resource_name(backup.backup_texture, "ReShade depth backup texture");
222 | else
223 | reshade::log_message(1, "Failed to create backup depth-stencil texture!");
224 |
225 | return &backup;
226 | }
227 |
228 | void untrack_depth_stencil(resource resource)
229 | {
230 | const auto it = std::find_if(depth_stencil_backups.begin(), depth_stencil_backups.end(),
231 | [resource](const depth_stencil_backup &existing) { return existing.depth_stencil_resource == resource; });
232 | if (it == depth_stencil_backups.end() || --it->references != 0)
233 | return;
234 |
235 | depth_stencil_backup &backup = *it;
236 |
237 | if (backup.backup_texture != 0)
238 | {
239 | // Do not destroy backup texture immediately since it may still be referenced by a command list that is in flight or was prerecorded
240 | // Instead enqueue it for delayed destruction in the future
241 | delayed_destroy_resources.emplace_back(backup.backup_texture, 50); // Destroy after 50 frames
242 | }
243 |
244 | depth_stencil_backups.erase(it);
245 | }
246 | };
247 |
248 | // Checks whether the aspect ratio of the two sets of dimensions is similar or not
249 | static bool check_aspect_ratio(float width_to_check, float height_to_check, uint32_t width, uint32_t height)
250 | {
251 | if (width_to_check == 0.0f || height_to_check == 0.0f)
252 | return true;
253 |
254 | const float w = static_cast(width);
255 | float w_ratio = w / width_to_check;
256 | const float h = static_cast(height);
257 | float h_ratio = h / height_to_check;
258 | const float aspect_ratio = (w / h) - (static_cast(width_to_check) / height_to_check);
259 |
260 | // Accept if dimensions are similar in value or almost exact multiples
261 | return std::fabs(aspect_ratio) <= 0.1f && ((w_ratio <= 1.85f && w_ratio >= 0.5f && h_ratio <= 1.85f && h_ratio >= 0.5f) || (s_use_aspect_ratio_heuristics == 2 && std::modf(w_ratio, &w_ratio) <= 0.02f && std::modf(h_ratio, &h_ratio) <= 0.02f));
262 | }
263 |
264 | static void on_clear_depth_impl(command_list *cmd_list, state_tracking &state, resource depth_stencil, clear_op op)
265 | {
266 | if (depth_stencil == 0)
267 | return;
268 |
269 | device *const device = cmd_list->get_device();
270 |
271 | depth_stencil_backup *const depth_stencil_backup = device->get_private_data().find_depth_stencil_backup(depth_stencil);
272 | if (depth_stencil_backup == nullptr || depth_stencil_backup->backup_texture == 0)
273 | return;
274 |
275 | bool do_copy = true;
276 | depth_stencil_info &counters = state.counters_per_used_depth_stencil[depth_stencil];
277 |
278 | // Ignore clears when there was no meaningful workload (e.g. at the start of a frame)
279 | if (counters.current_stats.drawcalls == 0)
280 | return;
281 |
282 | // Ignore clears when the last viewport rendered to only affected a small subset of the depth-stencil (fixes flickering in some games)
283 | switch (op)
284 | {
285 | case clear_op::clear_depth_stencil_view:
286 | // Mirror's Edge and Portal occasionally render something into a small viewport (16x16 in Mirror's Edge, 512x512 in Portal to render underwater geometry)
287 | do_copy = counters.current_stats.last_viewport.width > 1024 || (counters.current_stats.last_viewport.width == 0 || depth_stencil_backup->frame_width <= 1024);
288 | break;
289 | case clear_op::fullscreen_draw:
290 | // Mass Effect 3 in Mass Effect Legendary Edition sometimes uses a larger common depth buffer for shadow map and scene rendering, where the former uses a 1024x1024 viewport and the latter uses a viewport matching the render resolution
291 | do_copy = check_aspect_ratio(counters.current_stats.last_viewport.width, counters.current_stats.last_viewport.height, depth_stencil_backup->frame_width, depth_stencil_backup->frame_height);
292 | break;
293 | case clear_op::unbind_depth_stencil_view:
294 | break;
295 | }
296 |
297 | if (do_copy)
298 | {
299 | if (op != clear_op::unbind_depth_stencil_view)
300 | {
301 | // If clear index override is set to zero, always copy any suitable buffers
302 | if (depth_stencil_backup->force_clear_index == 0)
303 | {
304 | // Use greater equals operator here to handle case where the same scene is first rendered into a shadow map and then for real (e.g. Mirror's Edge main menu)
305 | do_copy = counters.current_stats.vertices >= state.best_copy_stats.vertices || (op == clear_op::fullscreen_draw && counters.current_stats.drawcalls >= state.best_copy_stats.drawcalls);
306 | }
307 | else if (std::numeric_limits::max() == depth_stencil_backup->force_clear_index)
308 | {
309 | // Special case for Garry's Mod which chooses the last clear operation that has a high workload
310 | do_copy = counters.current_stats.vertices >= 5000;
311 | }
312 | else
313 | {
314 | // This is not really correct, since clears may accumulate over multiple command lists, but it's unlikely that the same depth-stencil is used in more than one
315 | do_copy = counters.clears.size() == (depth_stencil_backup->force_clear_index - 1);
316 | }
317 |
318 | counters.clears.push_back({ counters.current_stats, op, do_copy });
319 | }
320 |
321 | // Make a backup copy of the depth texture before it is cleared
322 | if (do_copy)
323 | {
324 | state.best_copy_stats = counters.current_stats;
325 |
326 | // A resource has to be in this state for a clear operation, so can assume it here
327 | cmd_list->barrier(depth_stencil, resource_usage::depth_stencil_write, resource_usage::copy_source);
328 | cmd_list->copy_resource(depth_stencil, depth_stencil_backup->backup_texture);
329 | cmd_list->barrier(depth_stencil, resource_usage::copy_source, resource_usage::depth_stencil_write);
330 |
331 | counters.copied_during_frame = true;
332 | }
333 | }
334 |
335 | // Reset draw call stats for clears
336 | counters.current_stats = { 0, 0 };
337 | }
338 |
339 | static void update_effect_runtime(effect_runtime *runtime)
340 | {
341 | const generic_depth_data &instance = runtime->get_private_data();
342 |
343 | runtime->update_texture_bindings("ORIG_DEPTH", instance.selected_shader_resource);
344 |
345 | runtime->enumerate_uniform_variables(nullptr, [&instance](effect_runtime *runtime, auto variable) {
346 | char source[32] = "";
347 | if (runtime->get_annotation_string_from_uniform_variable(variable, "source", source) && std::strcmp(source, "bufready_depth") == 0)
348 | runtime->set_uniform_value_bool(variable, instance.selected_shader_resource != 0);
349 | });
350 |
351 | resource_view srv, srv_srgb;
352 |
353 | effect_texture_variable ModifiedDepthTex_handle = runtime->find_texture_variable("Citra.fx", "ModifiedDepthTex");
354 | runtime->get_texture_binding(ModifiedDepthTex_handle, &srv, &srv_srgb);
355 |
356 | runtime->update_texture_bindings("DEPTH", srv, srv_srgb);
357 | }
358 |
359 | static void on_init_device(device *device)
360 | {
361 | device->create_private_data();
362 |
363 | reshade::config_get_value(nullptr, "DEPTH", "DisableINTZ", s_disable_intz);
364 | reshade::config_get_value(nullptr, "DEPTH", "DepthCopyBeforeClears", s_preserve_depth_buffers);
365 | reshade::config_get_value(nullptr, "DEPTH", "UseAspectRatioHeuristics", s_use_aspect_ratio_heuristics);
366 | }
367 | static void on_init_command_list(command_list *cmd_list)
368 | {
369 | cmd_list->create_private_data();
370 | }
371 | static void on_init_command_queue(command_queue *cmd_queue)
372 | {
373 | cmd_queue->create_private_data();
374 |
375 | if ((cmd_queue->get_type() & command_queue_type::graphics) == 0)
376 | return;
377 |
378 | auto &device_data = cmd_queue->get_device()->get_private_data();
379 | device_data.queues.push_back(cmd_queue);
380 | }
381 | static void on_init_effect_runtime(effect_runtime *runtime)
382 | {
383 | runtime->create_private_data();
384 | }
385 | static void on_destroy_device(device *device)
386 | {
387 | auto &device_data = device->get_private_data();
388 |
389 | // Destroy any remaining resources
390 | for (const auto &[resource, _] : device_data.delayed_destroy_resources)
391 | {
392 | device->destroy_resource(resource);
393 | }
394 |
395 | for (depth_stencil_backup &depth_stencil_backup : device_data.depth_stencil_backups)
396 | {
397 | if (depth_stencil_backup.backup_texture != 0)
398 | device->destroy_resource(depth_stencil_backup.backup_texture);
399 | }
400 |
401 | device->destroy_private_data();
402 | }
403 | static void on_destroy_command_list(command_list *cmd_list)
404 | {
405 | cmd_list->destroy_private_data();
406 | }
407 | static void on_destroy_command_queue(command_queue *cmd_queue)
408 | {
409 | cmd_queue->destroy_private_data();
410 |
411 | auto &device_data = cmd_queue->get_device()->get_private_data();
412 | device_data.queues.erase(std::remove(device_data.queues.begin(), device_data.queues.end(), cmd_queue), device_data.queues.end());
413 | }
414 | static void on_destroy_effect_runtime(effect_runtime *runtime)
415 | {
416 | device *const device = runtime->get_device();
417 | generic_depth_data &data = runtime->get_private_data();
418 |
419 | if (data.selected_shader_resource != 0)
420 | device->destroy_resource_view(data.selected_shader_resource);
421 |
422 | runtime->destroy_private_data();
423 | }
424 |
425 | static bool on_create_resource(device *device, resource_desc &desc, subresource_data *, resource_usage)
426 | {
427 | if (desc.type != resource_type::surface && desc.type != resource_type::texture_2d)
428 | return false; // Skip resources that are not 2D textures
429 | if (desc.texture.samples != 1 || (desc.usage & resource_usage::depth_stencil) == 0 || desc.texture.format == format::s8_uint)
430 | return false; // Skip MSAA textures and resources that are not used as depth buffers
431 |
432 | switch (device->get_api())
433 | {
434 | case device_api::d3d9:
435 | if (s_disable_intz)
436 | return false;
437 | // Skip textures that are sampled as PCF shadow maps (see https://aras-p.info/texts/D3D9GPUHacks.html#shadowmap) using hardware support, since changing format would break that
438 | if (desc.type == resource_type::texture_2d && (desc.texture.format == format::d16_unorm || desc.texture.format == format::d24_unorm_x8_uint || desc.texture.format == format::d24_unorm_s8_uint))
439 | return false;
440 | // Skip small textures that are likely just shadow maps too (fixes a hang in Dragon's Dogma: Dark Arisen when changing areas)
441 | if (desc.texture.width <= 512)
442 | return false;
443 | // Replace texture format with special format that supports normal sampling (see https://aras-p.info/texts/D3D9GPUHacks.html#depth)
444 | desc.texture.format = format::intz;
445 | desc.usage |= resource_usage::shader_resource;
446 | break;
447 | case device_api::d3d10:
448 | case device_api::d3d11:
449 | // Allow shader access to images that are used as depth-stencil attachments
450 | desc.texture.format = format_to_typeless(desc.texture.format);
451 | desc.usage |= resource_usage::shader_resource;
452 | break;
453 | case device_api::d3d12:
454 | case device_api::vulkan:
455 | // D3D12 and Vulkan always use backup texture, but need to be able to copy to it
456 | desc.usage |= resource_usage::copy_source;
457 | break;
458 | case device_api::opengl:
459 | // No need to change anything in OpenGL
460 | return false;
461 | }
462 |
463 | return true;
464 | }
465 | static bool on_create_resource_view(device *device, resource resource, resource_usage usage_type, resource_view_desc &desc)
466 | {
467 | // A view cannot be created with a typeless format (which was set in 'on_create_resource' above), so fix it in case defaults are used
468 | if ((device->get_api() != device_api::d3d10 && device->get_api() != device_api::d3d11) || desc.format != format::unknown)
469 | return false;
470 |
471 | const resource_desc texture_desc = device->get_resource_desc(resource);
472 | // Only non-MSAA textures where modified, so skip all others
473 | if (texture_desc.texture.samples != 1 || (texture_desc.usage & resource_usage::depth_stencil) == 0)
474 | return false;
475 |
476 | switch (usage_type)
477 | {
478 | case resource_usage::depth_stencil:
479 | desc.format = format_to_depth_stencil_typed(texture_desc.texture.format);
480 | break;
481 | case resource_usage::shader_resource:
482 | desc.format = format_to_default_typed(texture_desc.texture.format);
483 | break;
484 | }
485 |
486 | // Only need to set the rest of the fields if the application did not pass in a valid description already
487 | if (desc.type == resource_view_type::unknown)
488 | {
489 | desc.type = texture_desc.texture.depth_or_layers > 1 ? resource_view_type::texture_2d_array : resource_view_type::texture_2d;
490 | desc.texture.first_level = 0;
491 | desc.texture.level_count = (usage_type == resource_usage::shader_resource) ? UINT32_MAX : 1;
492 | desc.texture.first_layer = 0;
493 | desc.texture.layer_count = (usage_type == resource_usage::shader_resource) ? UINT32_MAX : 1;
494 | }
495 |
496 | return true;
497 | }
498 | static void on_destroy_resource(device *device, resource resource)
499 | {
500 | auto &device_data = device->get_private_data();
501 |
502 | // In some cases the 'destroy_device' event may be called before all resources have been destroyed
503 | // The state tracking context would have been destroyed already in that case, so return early if it does not exist
504 | if (std::addressof(device_data) == nullptr)
505 | return;
506 |
507 | std::unique_lock lock(s_mutex);
508 |
509 | device_data.destroyed_resources.push_back(resource);
510 |
511 | // Remove this destroyed resource from the list of tracked depth-stencil resources
512 | const auto it = std::find_if(device_data.current_depth_stencil_list.begin(), device_data.current_depth_stencil_list.end(),
513 | [resource](const auto ¤t) { return current.first == resource; });
514 | if (it != device_data.current_depth_stencil_list.end())
515 | {
516 | const bool copied_during_frame = it->second.copied_during_frame;
517 |
518 | device_data.current_depth_stencil_list.erase(it);
519 |
520 | lock.unlock();
521 |
522 | // This is bad ... the resource may still be in use by an effect on the GPU and destroying it would crash it
523 | // Try to mitigate that somehow by delaying this thread a little to hopefully give the GPU enough time to catch up before the resource memory is deallocated
524 | if (device->get_api() == device_api::d3d12 || device->get_api() == device_api::vulkan)
525 | {
526 | reshade::log_message(2, "A depth-stencil resource was destroyed while still being tracked.");
527 |
528 | if (!copied_during_frame)
529 | Sleep(250);
530 | }
531 | }
532 | }
533 |
534 | static bool on_draw(command_list *cmd_list, uint32_t vertices, uint32_t instances, uint32_t, uint32_t)
535 | {
536 | auto &state = cmd_list->get_private_data();
537 | if (state.current_depth_stencil == 0)
538 | return false; // This is a draw call with no depth-stencil bound
539 |
540 | // Check if this draw call likely represets a fullscreen rectangle (two triangles), which would clear the depth-stencil
541 | const bool fullscreen_draw = vertices == 6 && instances == 1;
542 | if (fullscreen_draw &&
543 | s_preserve_depth_buffers == 2 &&
544 | state.first_draw_since_bind &&
545 | // But ignore that in Vulkan (since it is invalid to copy a resource inside an active render pass)
546 | cmd_list->get_device()->get_api() != device_api::vulkan)
547 | on_clear_depth_impl(cmd_list, state, state.current_depth_stencil, clear_op::fullscreen_draw);
548 |
549 | state.first_draw_since_bind = false;
550 |
551 | depth_stencil_info &counters = state.counters_per_used_depth_stencil[state.current_depth_stencil];
552 | counters.total_stats.vertices += vertices * instances;
553 | counters.total_stats.drawcalls += 1;
554 | counters.current_stats.vertices += vertices * instances;
555 | counters.current_stats.drawcalls += 1;
556 |
557 | // Skip updating last viewport for fullscreen draw calls, to prevent a clear operation in Prince of Persia: The Sands of Time from getting filtered out
558 | if (!fullscreen_draw)
559 | counters.current_stats.last_viewport = state.current_viewport;
560 |
561 | return false;
562 | }
563 | static bool on_draw_indexed(command_list *cmd_list, uint32_t indices, uint32_t instances, uint32_t, int32_t, uint32_t)
564 | {
565 | on_draw(cmd_list, indices, instances, 0, 0);
566 |
567 | return false;
568 | }
569 | static bool on_draw_indirect(command_list *cmd_list, indirect_command type, resource, uint64_t, uint32_t draw_count, uint32_t)
570 | {
571 | if (type == indirect_command::dispatch)
572 | return false;
573 |
574 | auto &state = cmd_list->get_private_data();
575 | if (state.current_depth_stencil == 0)
576 | return false; // This is a draw call with no depth-stencil bound
577 |
578 | depth_stencil_info &counters = state.counters_per_used_depth_stencil[state.current_depth_stencil];
579 | counters.total_stats.drawcalls += draw_count;
580 | counters.total_stats.drawcalls_indirect += draw_count;
581 | counters.current_stats.drawcalls += draw_count;
582 | counters.current_stats.drawcalls_indirect += draw_count;
583 | counters.current_stats.last_viewport = state.current_viewport;
584 |
585 | return false;
586 | }
587 |
588 | static void on_bind_viewport(command_list *cmd_list, uint32_t first, uint32_t count, const viewport *viewport)
589 | {
590 | if (first != 0 || count == 0)
591 | return; // Only interested in the main viewport
592 |
593 | auto &state = cmd_list->get_private_data();
594 | state.current_viewport = viewport[0];
595 | }
596 | static void on_bind_depth_stencil(command_list *cmd_list, uint32_t, const resource_view *, resource_view depth_stencil_view)
597 | {
598 | auto &state = cmd_list->get_private_data();
599 |
600 | const resource depth_stencil = (depth_stencil_view != 0) ? cmd_list->get_device()->get_resource_from_view(depth_stencil_view) : resource{ 0 };
601 |
602 | if (depth_stencil != state.current_depth_stencil)
603 | {
604 | if (depth_stencil != 0)
605 | state.first_draw_since_bind = true;
606 |
607 | // Make a backup of the depth texture before it is used differently, since in D3D12 or Vulkan the underlying memory may be aliased to a different resource, so cannot just access it at the end of the frame
608 | if (s_preserve_depth_buffers == 2 &&
609 | state.current_depth_stencil != 0 && depth_stencil == 0 && (
610 | cmd_list->get_device()->get_api() == device_api::d3d12 || cmd_list->get_device()->get_api() == device_api::vulkan))
611 | on_clear_depth_impl(cmd_list, state, state.current_depth_stencil, clear_op::unbind_depth_stencil_view);
612 | }
613 |
614 | state.current_depth_stencil = depth_stencil;
615 | }
616 | static bool on_clear_depth_stencil(command_list *cmd_list, resource_view dsv, const float *depth, const uint8_t *, uint32_t, const rect *)
617 | {
618 | // Ignore clears that do not affect the depth buffer (stencil clears)
619 | if (depth != nullptr && s_preserve_depth_buffers)
620 | {
621 | auto &state = cmd_list->get_private_data();
622 |
623 | const resource depth_stencil = cmd_list->get_device()->get_resource_from_view(dsv);
624 |
625 | // Note: This does not work when called from 'vkCmdClearAttachments', since it is invalid to copy a resource inside an active render pass
626 | on_clear_depth_impl(cmd_list, state, depth_stencil, clear_op::clear_depth_stencil_view);
627 | }
628 |
629 | return false;
630 | }
631 | static void on_begin_render_pass_with_depth_stencil(command_list *cmd_list, uint32_t, const render_pass_render_target_desc *, const render_pass_depth_stencil_desc *depth_stencil_desc)
632 | {
633 | if (depth_stencil_desc != nullptr && depth_stencil_desc->depth_load_op == render_pass_load_op::clear)
634 | {
635 | on_clear_depth_stencil(cmd_list, depth_stencil_desc->view, &depth_stencil_desc->clear_depth, nullptr, 0, nullptr);
636 |
637 | // Prevent 'on_bind_depth_stencil' from copying depth buffer again
638 | auto &state = cmd_list->get_private_data();
639 | state.current_depth_stencil = { 0 };
640 | }
641 |
642 | // If render pass has depth store operation set to 'discard', any copy performed after the render pass will likely contain broken data, so can only hope that the depth buffer can be copied before that ...
643 |
644 | on_bind_depth_stencil(cmd_list, 0, nullptr, depth_stencil_desc != nullptr ? depth_stencil_desc->view : resource_view{});
645 | }
646 |
647 | static void on_reset(command_list *cmd_list)
648 | {
649 | auto &target_state = cmd_list->get_private_data();
650 | target_state.reset();
651 | }
652 | static void on_execute_primary(command_queue *queue, command_list *cmd_list)
653 | {
654 | auto &target_state = queue->get_private_data();
655 | const auto &source_state = cmd_list->get_private_data();
656 |
657 | // Skip merging state when this execution event is just the immediate command list getting flushed
658 | if (std::addressof(target_state) != std::addressof(source_state))
659 | {
660 | target_state.merge(source_state);
661 | }
662 | }
663 | static void on_execute_secondary(command_list *cmd_list, command_list *secondary_cmd_list)
664 | {
665 | auto &target_state = cmd_list->get_private_data();
666 | const auto &source_state = secondary_cmd_list->get_private_data();
667 |
668 | // If this is a secondary command list that was recorded without a depth-stencil binding, but is now executed using a depth-stencil binding, handle it as if an indirect draw call was performed to ensure the depth-stencil is tracked
669 | if (target_state.current_depth_stencil != 0 && source_state.current_depth_stencil == 0 && source_state.counters_per_used_depth_stencil.empty())
670 | {
671 | target_state.current_viewport = source_state.current_viewport;
672 |
673 | on_draw_indirect(cmd_list, indirect_command::draw, { 0 }, 0, 1, 0);
674 | }
675 | else
676 | {
677 | target_state.merge(source_state);
678 | }
679 | }
680 |
681 | static void on_present(command_queue *, swapchain *swapchain, const rect *, const rect *, uint32_t, const rect *)
682 | {
683 | device *const device = swapchain->get_device();
684 | generic_depth_device_data &device_data = device->get_private_data();
685 |
686 | const std::unique_lock lock(s_mutex);
687 |
688 | // Merge state from all graphics queues
689 | state_tracking queue_state;
690 | for (command_queue *const queue : device_data.queues)
691 | queue_state.merge(queue->get_private_data());
692 |
693 | // Only update device list if there are any depth-stencils, otherwise this may be a second present call (at which point 'reset_on_present' already cleared out the queue list in the first present call)
694 | if (queue_state.counters_per_used_depth_stencil.empty())
695 | return;
696 |
697 | // Also skip update when there has been very little activity (special case for emulators like PCSX2 which may present more often than they render a frame)
698 | if (queue_state.counters_per_used_depth_stencil.size() == 1 && queue_state.counters_per_used_depth_stencil.begin()->second.total_stats.drawcalls <= 8)
699 | return;
700 |
701 | device_data.current_depth_stencil_list.clear();
702 | device_data.current_depth_stencil_list.reserve(queue_state.counters_per_used_depth_stencil.size());
703 |
704 | for (const auto &[resource, snapshot] : queue_state.counters_per_used_depth_stencil)
705 | {
706 | if (snapshot.total_stats.drawcalls == 0)
707 | continue; // Skip unused
708 |
709 | if (std::find(device_data.destroyed_resources.begin(), device_data.destroyed_resources.end(), resource) != device_data.destroyed_resources.end())
710 | continue; // Skip resources that were destroyed by the application
711 |
712 | // Save to current list of depth-stencils on the device, so that it can be displayed in the GUI
713 | device_data.current_depth_stencil_list.emplace_back(resource, snapshot);
714 | }
715 |
716 | for (command_queue *const queue : device_data.queues)
717 | queue->get_private_data().reset_on_present();
718 |
719 | device_data.destroyed_resources.clear();
720 |
721 | // Destroy resources that were enqueued for delayed destruction and have reached the targeted number of passed frames
722 | for (auto it = device_data.delayed_destroy_resources.begin(); it != device_data.delayed_destroy_resources.end();)
723 | {
724 | if (--it->second == 0)
725 | {
726 | device->destroy_resource(it->first);
727 |
728 | it = device_data.delayed_destroy_resources.erase(it);
729 | }
730 | else
731 | {
732 | ++it;
733 | }
734 | }
735 | }
736 |
737 | static void on_begin_render_effects(effect_runtime *runtime, command_list *cmd_list, resource_view, resource_view)
738 | {
739 | device *const device = runtime->get_device();
740 | generic_depth_data &data = runtime->get_private_data();
741 | generic_depth_device_data &device_data = device->get_private_data();
742 |
743 | resource best_match = { 0 };
744 | resource_desc best_match_desc;
745 | const depth_stencil_info *best_snapshot = nullptr;
746 |
747 | uint32_t frame_width, frame_height;
748 | runtime->get_screenshot_width_and_height(&frame_width, &frame_height);
749 |
750 | std::shared_lock lock(s_mutex);
751 | const auto current_depth_stencil_list = device_data.current_depth_stencil_list;
752 | // Unlock while calling into device below, since device may hold a lock itself and that then can deadlock another thread that calls into 'on_destroy_resource' from the device holding that lock
753 | lock.unlock();
754 |
755 | for (auto &[resource, snapshot] : current_depth_stencil_list)
756 | {
757 | const resource_desc desc = device->get_resource_desc(resource);
758 | if (desc.texture.samples > 1)
759 | continue; // Ignore MSAA textures, since they would need to be resolved first
760 |
761 | if (s_use_aspect_ratio_heuristics && !check_aspect_ratio(static_cast(desc.texture.width), static_cast(desc.texture.height), frame_width, frame_height))
762 | continue; // Not a good fit
763 |
764 | if (best_snapshot == nullptr || (snapshot.total_stats.drawcalls_indirect < (snapshot.total_stats.drawcalls / 3) ?
765 | // Choose snapshot with the most vertices, since that is likely to contain the main scene
766 | snapshot.total_stats.vertices > best_snapshot->total_stats.vertices :
767 | // Or check draw calls, since vertices may not be accurate if application is using indirect draw calls
768 | snapshot.total_stats.drawcalls > best_snapshot->total_stats.drawcalls))
769 | {
770 | best_match = resource;
771 | best_match_desc = desc;
772 | best_snapshot = &snapshot;
773 | }
774 | }
775 |
776 | if (data.override_depth_stencil != 0)
777 | {
778 | const auto it = std::find_if(current_depth_stencil_list.begin(), current_depth_stencil_list.end(),
779 | [resource = data.override_depth_stencil](const auto ¤t) { return current.first == resource; });
780 | if (it != current_depth_stencil_list.end())
781 | {
782 | best_match = it->first;
783 | best_match_desc = device->get_resource_desc(it->first);
784 | best_snapshot = &it->second;
785 | }
786 | }
787 |
788 | if (best_match != 0)
789 | {
790 | const device_api api = device->get_api();
791 |
792 | depth_stencil_backup *depth_stencil_backup = device_data.find_depth_stencil_backup(best_match);
793 |
794 | if (best_match != data.selected_depth_stencil || data.selected_shader_resource == 0 || (s_preserve_depth_buffers && depth_stencil_backup == nullptr))
795 | {
796 | // Destroy previous resource view, since the underlying resource has changed
797 | if (data.selected_shader_resource != 0)
798 | {
799 | runtime->get_command_queue()->wait_idle(); // Ensure resource view is no longer in-use before destroying it
800 | device->destroy_resource_view(data.selected_shader_resource);
801 |
802 | device_data.untrack_depth_stencil(data.selected_depth_stencil);
803 | }
804 |
805 | data.using_backup_texture = false;
806 | data.selected_depth_stencil = best_match;
807 | data.selected_shader_resource = { 0 };
808 |
809 | // Create two-dimensional resource view to the first level and layer of the depth-stencil resource
810 | resource_view_desc srv_desc(api != device_api::opengl && api != device_api::vulkan ? format_to_default_typed(best_match_desc.texture.format) : best_match_desc.texture.format);
811 |
812 | // Need to create backup texture only if doing backup copies or original resource does not support shader access (which is necessary for binding it to effects)
813 | // Also always create a backup texture in D3D12 or Vulkan to circument problems in case application makes use of resource aliasing
814 | if (s_preserve_depth_buffers || (best_match_desc.usage & resource_usage::shader_resource) == 0 || (api == device_api::d3d12 || api == device_api::vulkan))
815 | {
816 | depth_stencil_backup = device_data.track_depth_stencil_for_backup(device, best_match, best_match_desc);
817 |
818 | // Abort in case backup texture creation failed
819 | if (depth_stencil_backup->backup_texture == 0)
820 | return;
821 |
822 | depth_stencil_backup->frame_width = frame_width;
823 | depth_stencil_backup->frame_height = frame_height;
824 |
825 | if (s_preserve_depth_buffers)
826 | reshade::config_get_value(nullptr, "DEPTH", "DepthCopyAtClearIndex", depth_stencil_backup->force_clear_index);
827 | else
828 | depth_stencil_backup->force_clear_index = 0;
829 |
830 | if (api == device_api::d3d9)
831 | srv_desc.format = format::r32_float; // Same format as backup texture, as set in 'track_depth_stencil_for_backup'
832 |
833 | if (!device->create_resource_view(depth_stencil_backup->backup_texture, resource_usage::shader_resource, srv_desc, &data.selected_shader_resource))
834 | return;
835 |
836 | data.using_backup_texture = true;
837 | }
838 | else
839 | {
840 | if (!device->create_resource_view(best_match, resource_usage::shader_resource, srv_desc, &data.selected_shader_resource))
841 | return;
842 | }
843 |
844 | update_effect_runtime(runtime);
845 | }
846 |
847 | if (data.using_backup_texture)
848 | {
849 | assert(depth_stencil_backup != nullptr && depth_stencil_backup->backup_texture != 0 && best_snapshot != nullptr);
850 | const resource backup_texture = depth_stencil_backup->backup_texture;
851 |
852 | // Copy to backup texture unless already copied during the current frame
853 | if (!best_snapshot->copied_during_frame && (best_match_desc.usage & resource_usage::copy_source) != 0)
854 | {
855 | // Ensure barriers are not created with 'D3D12_RESOURCE_STATE_[...]_SHADER_RESOURCE' when resource has 'D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE' flag set
856 | const resource_usage old_state = best_match_desc.usage & (resource_usage::depth_stencil | resource_usage::shader_resource);
857 |
858 | lock.lock();
859 | const auto it = std::find_if(device_data.current_depth_stencil_list.begin(), device_data.current_depth_stencil_list.end(),
860 | [best_match](const auto ¤t) { return current.first == best_match; });
861 | // Indicate that the copy is now being done, so it is not repeated in case effects are rendered by another runtime (e.g. when there are multiple present calls in a frame)
862 | if (it != device_data.current_depth_stencil_list.end())
863 | it->second.copied_during_frame = true;
864 | else
865 | // Resource disappeared from the current depth-stencil list between earlier in this function and now, which indicates that it was destroyed in the meantime
866 | return;
867 | lock.unlock();
868 |
869 | cmd_list->barrier(best_match, old_state, resource_usage::copy_source);
870 | cmd_list->copy_resource(best_match, backup_texture);
871 | cmd_list->barrier(best_match, resource_usage::copy_source, old_state);
872 | }
873 |
874 | cmd_list->barrier(backup_texture, resource_usage::copy_dest, resource_usage::shader_resource);
875 | }
876 | else
877 | {
878 | // Unset current depth-stencil view, in case it is bound to an effect as a shader resource (which will fail if it is still bound on output)
879 | if (api <= device_api::d3d11)
880 | cmd_list->bind_render_targets_and_depth_stencil(0, nullptr);
881 |
882 | cmd_list->barrier(best_match, resource_usage::depth_stencil | resource_usage::shader_resource, resource_usage::shader_resource);
883 | }
884 | }
885 | else
886 | {
887 | // Unset any existing depth-stencil selected in previous frames
888 | if (data.selected_depth_stencil != 0)
889 | {
890 | if (data.selected_shader_resource != 0)
891 | {
892 | runtime->get_command_queue()->wait_idle(); // Ensure resource view is no longer in-use before destroying it
893 | device->destroy_resource_view(data.selected_shader_resource);
894 |
895 | device_data.untrack_depth_stencil(data.selected_depth_stencil);
896 | }
897 |
898 | data.using_backup_texture = false;
899 | data.selected_depth_stencil = { 0 };
900 | data.selected_shader_resource = { 0 };
901 |
902 | update_effect_runtime(runtime);
903 | }
904 | }
905 | }
906 | static void on_finish_render_effects(effect_runtime *runtime, command_list *cmd_list, resource_view, resource_view)
907 | {
908 | const generic_depth_data &data = runtime->get_private_data();
909 |
910 | if (data.selected_shader_resource != 0)
911 | {
912 | if (data.using_backup_texture)
913 | {
914 | const resource backup_texture = runtime->get_device()->get_resource_from_view(data.selected_shader_resource);
915 | cmd_list->barrier(backup_texture, resource_usage::shader_resource, resource_usage::copy_dest);
916 | }
917 | else
918 | {
919 | cmd_list->barrier(data.selected_depth_stencil, resource_usage::shader_resource, resource_usage::depth_stencil | resource_usage::shader_resource);
920 | }
921 | }
922 | }
923 |
924 | static inline const char *format_to_string(format format) {
925 | switch (format)
926 | {
927 | case format::d16_unorm:
928 | case format::r16_typeless:
929 | return "D16 ";
930 | case format::d16_unorm_s8_uint:
931 | return "D16S8";
932 | case format::d24_unorm_x8_uint:
933 | return "D24X8";
934 | case format::d24_unorm_s8_uint:
935 | case format::r24_g8_typeless:
936 | return "D24S8";
937 | case format::d32_float:
938 | case format::r32_float:
939 | case format::r32_typeless:
940 | return "D32 ";
941 | case format::d32_float_s8_uint:
942 | case format::r32_g8_typeless:
943 | return "D32S8";
944 | case format::intz:
945 | return "INTZ ";
946 | default:
947 | return " ";
948 | }
949 | }
950 |
951 | static void draw_settings_overlay(effect_runtime *runtime)
952 | {
953 | device *const device = runtime->get_device();
954 | generic_depth_data &data = runtime->get_private_data();
955 | generic_depth_device_data &device_data = device->get_private_data();
956 |
957 | bool force_reset = false;
958 |
959 | if (bool use_aspect_ratio_heuristics = s_use_aspect_ratio_heuristics != 0;
960 | ImGui::Checkbox("Use aspect ratio heuristics", &use_aspect_ratio_heuristics))
961 | {
962 | s_use_aspect_ratio_heuristics = use_aspect_ratio_heuristics ? 1 : 0;
963 | reshade::config_set_value(nullptr, "DEPTH", "UseAspectRatioHeuristics", s_use_aspect_ratio_heuristics);
964 | force_reset = true;
965 | }
966 |
967 | if (s_use_aspect_ratio_heuristics)
968 | {
969 | if (bool use_aspect_ratio_heuristics_ex = s_use_aspect_ratio_heuristics == 2;
970 | ImGui::Checkbox("Use extended aspect ratio heuristics (for DLSS or resolution scaling)", &use_aspect_ratio_heuristics_ex))
971 | {
972 | s_use_aspect_ratio_heuristics = use_aspect_ratio_heuristics_ex ? 2 : 1;
973 | reshade::config_set_value(nullptr, "DEPTH", "UseAspectRatioHeuristics", s_use_aspect_ratio_heuristics);
974 | force_reset = true;
975 | }
976 | }
977 |
978 | if (bool copy_before_clear_operations = s_preserve_depth_buffers != 0;
979 | ImGui::Checkbox("Copy depth buffer before clear operations", ©_before_clear_operations))
980 | {
981 | s_preserve_depth_buffers = copy_before_clear_operations ? 1 : 0;
982 | reshade::config_set_value(nullptr, "DEPTH", "DepthCopyBeforeClears", s_preserve_depth_buffers);
983 | force_reset = true;
984 | }
985 |
986 | const bool is_d3d12_or_vulkan = device->get_api() == device_api::d3d12 || device->get_api() == device_api::vulkan;
987 |
988 | if (s_preserve_depth_buffers || is_d3d12_or_vulkan)
989 | {
990 | if (bool copy_before_fullscreen_draws = s_preserve_depth_buffers == 2;
991 | ImGui::Checkbox(is_d3d12_or_vulkan ? "Copy depth buffer during frame to prevent artifacts" : "Copy depth buffer before fullscreen draw calls", ©_before_fullscreen_draws))
992 | {
993 | s_preserve_depth_buffers = copy_before_fullscreen_draws ? 2 : 1;
994 | reshade::config_set_value(nullptr, "DEPTH", "DepthCopyBeforeClears", s_preserve_depth_buffers);
995 | }
996 | }
997 |
998 | ImGui::Spacing();
999 | ImGui::Separator();
1000 | ImGui::Spacing();
1001 |
1002 | std::shared_lock lock(s_mutex);
1003 |
1004 | if (device_data.current_depth_stencil_list.empty())
1005 | {
1006 | ImGui::TextUnformatted("No depth buffers found.");
1007 | return;
1008 | }
1009 |
1010 | // Sort pointer list so that added/removed items do not change the GUI much
1011 | struct depth_stencil_item
1012 | {
1013 | unsigned int display_count;
1014 | resource resource;
1015 | depth_stencil_info snapshot;
1016 | resource_desc desc;
1017 | };
1018 |
1019 | std::vector sorted_item_list;
1020 | sorted_item_list.reserve(device_data.current_depth_stencil_list.size());
1021 |
1022 | for (const auto &[resource, snapshot] : device_data.current_depth_stencil_list)
1023 | {
1024 | if (auto it = data.display_count_per_depth_stencil.find(resource);
1025 | it == data.display_count_per_depth_stencil.end())
1026 | {
1027 | sorted_item_list.push_back({ 1u, resource, snapshot, device->get_resource_desc(resource) });
1028 | }
1029 | else
1030 | {
1031 | sorted_item_list.push_back({ it->second + 1u, resource, snapshot, device->get_resource_desc(resource) });
1032 | }
1033 | }
1034 |
1035 | lock.unlock();
1036 |
1037 | std::sort(sorted_item_list.begin(), sorted_item_list.end(), [](const depth_stencil_item &a, const depth_stencil_item &b) {
1038 | return (a.display_count > b.display_count) ||
1039 | (a.display_count == b.display_count && ((a.desc.texture.width > b.desc.texture.width || (a.desc.texture.width == b.desc.texture.width && a.desc.texture.height > b.desc.texture.height)) ||
1040 | (a.desc.texture.width == b.desc.texture.width && a.desc.texture.height == b.desc.texture.height && a.resource < b.resource)));
1041 | });
1042 |
1043 | bool has_msaa_depth_stencil = false;
1044 | bool has_no_clear_operations = false;
1045 |
1046 | data.display_count_per_depth_stencil.clear();
1047 | for (const depth_stencil_item &item : sorted_item_list)
1048 | {
1049 | data.display_count_per_depth_stencil[item.resource] = item.display_count;
1050 |
1051 | char label[512] = "";
1052 | sprintf_s(label, "%c 0x%016llx", (item.resource == data.selected_depth_stencil ? '>' : ' '), item.resource.handle);
1053 |
1054 | if (item.desc.texture.samples > 1) // Disable widget for MSAA textures
1055 | {
1056 | has_msaa_depth_stencil = true;
1057 |
1058 | ImGui::BeginDisabled();
1059 | ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]);
1060 | }
1061 |
1062 | if (bool value = (item.resource == data.override_depth_stencil);
1063 | ImGui::Checkbox(label, &value))
1064 | {
1065 | data.override_depth_stencil = value ? item.resource : resource{ 0 };
1066 | force_reset = true;
1067 | }
1068 |
1069 | ImGui::SameLine();
1070 | ImGui::Text("| %4ux%-4u | %s | %5u draw calls (%5u indirect) ==> %8u vertices |%s",
1071 | item.desc.texture.width,
1072 | item.desc.texture.height,
1073 | format_to_string(item.desc.texture.format),
1074 | item.snapshot.total_stats.drawcalls,
1075 | item.snapshot.total_stats.drawcalls_indirect,
1076 | item.snapshot.total_stats.vertices,
1077 | (item.desc.texture.samples > 1 ? " MSAA" : ""));
1078 |
1079 | if (item.desc.texture.samples > 1)
1080 | {
1081 | ImGui::PopStyleColor();
1082 | ImGui::EndDisabled();
1083 | }
1084 |
1085 | if (s_preserve_depth_buffers && item.resource == data.selected_depth_stencil)
1086 | {
1087 | if (item.snapshot.clears.empty())
1088 | {
1089 | has_no_clear_operations = !is_d3d12_or_vulkan;
1090 | continue;
1091 | }
1092 |
1093 | depth_stencil_backup *const depth_stencil_backup = device_data.find_depth_stencil_backup(item.resource);
1094 | if (depth_stencil_backup == nullptr || depth_stencil_backup->backup_texture == 0)
1095 | continue;
1096 |
1097 | for (size_t clear_index = 1; clear_index <= item.snapshot.clears.size(); ++clear_index)
1098 | {
1099 | const auto &clear_stats = item.snapshot.clears[clear_index - 1];
1100 |
1101 | sprintf_s(label, "%c CLEAR %2zu", clear_stats.copied_during_frame ? '>' : ' ', clear_index);
1102 |
1103 | if (bool value = (depth_stencil_backup->force_clear_index == clear_index);
1104 | ImGui::Checkbox(label, &value))
1105 | {
1106 | depth_stencil_backup->force_clear_index = value ? clear_index : 0;
1107 | reshade::config_set_value(nullptr, "DEPTH", "DepthCopyAtClearIndex", depth_stencil_backup->force_clear_index);
1108 | }
1109 |
1110 | ImGui::SameLine();
1111 | ImGui::Text(" | | | %5u draw calls (%5u indirect) ==> %8u vertices |%s",
1112 | clear_stats.drawcalls,
1113 | clear_stats.drawcalls_indirect,
1114 | clear_stats.vertices,
1115 | clear_stats.clear_op == clear_op::fullscreen_draw ? " Fullscreen draw call" : "");
1116 | }
1117 |
1118 | if (sorted_item_list.size() == 1 && !is_d3d12_or_vulkan)
1119 | {
1120 | if (bool value = (depth_stencil_backup->force_clear_index == std::numeric_limits::max());
1121 | ImGui::Checkbox(" Choose last clear operation with high number of draw calls", &value))
1122 | {
1123 | depth_stencil_backup->force_clear_index = value ? std::numeric_limits::max() : 0;
1124 | reshade::config_set_value(nullptr, "DEPTH", "DepthCopyAtClearIndex", depth_stencil_backup->force_clear_index);
1125 | }
1126 | }
1127 | }
1128 | }
1129 |
1130 | if (has_msaa_depth_stencil || has_no_clear_operations)
1131 | {
1132 | ImGui::Spacing();
1133 | ImGui::Separator();
1134 | ImGui::Spacing();
1135 |
1136 | ImGui::PushTextWrapPos();
1137 | if (has_msaa_depth_stencil)
1138 | ImGui::TextUnformatted("Not all depth buffers are available.\nYou may have to disable MSAA in the game settings for depth buffer detection to work!");
1139 | if (has_no_clear_operations)
1140 | ImGui::Text("No clear operations were found for the selected depth buffer.\n%s",
1141 | s_preserve_depth_buffers != 2 ? "Try enabling \"Copy depth buffer before fullscreen draw calls\" or disable \"Copy depth buffer before clear operations\"!" : "Disable \"Copy depth buffer before clear operations\" or select a different depth buffer!");
1142 | ImGui::PopTextWrapPos();
1143 | }
1144 |
1145 | if (force_reset)
1146 | {
1147 | // Reset selected depth-stencil to force re-creation of resources next frame (like the backup texture)
1148 | if (data.selected_shader_resource != 0)
1149 | {
1150 | command_queue *const queue = runtime->get_command_queue();
1151 |
1152 | queue->wait_idle(); // Ensure resource view is no longer in-use before destroying it
1153 | device->destroy_resource_view(data.selected_shader_resource);
1154 |
1155 | device_data.untrack_depth_stencil(data.selected_depth_stencil);
1156 | }
1157 |
1158 | data.using_backup_texture = false;
1159 | data.selected_depth_stencil = { 0 };
1160 | data.selected_shader_resource = { 0 };
1161 |
1162 | update_effect_runtime(runtime);
1163 | }
1164 | }
1165 |
1166 | void register_addon_depth()
1167 | {
1168 | reshade::register_overlay(nullptr, draw_settings_overlay);
1169 |
1170 | reshade::register_event(on_init_device);
1171 | reshade::register_event(on_init_command_list);
1172 | reshade::register_event(on_init_command_queue);
1173 | reshade::register_event(on_init_effect_runtime);
1174 | reshade::register_event(on_destroy_device);
1175 | reshade::register_event(on_destroy_command_list);
1176 | reshade::register_event(on_destroy_command_queue);
1177 | reshade::register_event(on_destroy_effect_runtime);
1178 |
1179 | reshade::register_event(on_create_resource);
1180 | reshade::register_event(on_create_resource_view);
1181 | reshade::register_event(on_destroy_resource);
1182 |
1183 | reshade::register_event(on_draw);
1184 | reshade::register_event(on_draw_indexed);
1185 | reshade::register_event(on_draw_indirect);
1186 | reshade::register_event(on_bind_viewport);
1187 | reshade::register_event(on_begin_render_pass_with_depth_stencil);
1188 | reshade::register_event(on_bind_depth_stencil);
1189 | reshade::register_event(on_clear_depth_stencil);
1190 |
1191 | reshade::register_event(on_reset);
1192 | reshade::register_event(on_execute_primary);
1193 | reshade::register_event(on_execute_secondary);
1194 |
1195 | reshade::register_event(on_present);
1196 |
1197 | reshade::register_event(on_begin_render_effects);
1198 | reshade::register_event(on_finish_render_effects);
1199 | // Need to set texture binding again after reloading
1200 | reshade::register_event(update_effect_runtime);
1201 | }
1202 | void unregister_addon_depth()
1203 | {
1204 | reshade::unregister_event(on_init_device);
1205 | reshade::unregister_event(on_init_command_list);
1206 | reshade::unregister_event(on_init_command_queue);
1207 | reshade::unregister_event(on_init_effect_runtime);
1208 | reshade::unregister_event(on_destroy_device);
1209 | reshade::unregister_event(on_destroy_command_list);
1210 | reshade::unregister_event(on_destroy_command_queue);
1211 | reshade::unregister_event(on_destroy_effect_runtime);
1212 |
1213 | reshade::unregister_event(on_create_resource);
1214 | reshade::unregister_event(on_create_resource_view);
1215 | reshade::unregister_event(on_destroy_resource);
1216 |
1217 | reshade::unregister_event(on_draw);
1218 | reshade::unregister_event(on_draw_indexed);
1219 | reshade::unregister_event(on_draw_indirect);
1220 | reshade::unregister_event(on_bind_viewport);
1221 | reshade::unregister_event(on_begin_render_pass_with_depth_stencil);
1222 | reshade::unregister_event(on_bind_depth_stencil);
1223 | reshade::unregister_event(on_clear_depth_stencil);
1224 |
1225 | reshade::unregister_event(on_reset);
1226 | reshade::unregister_event(on_execute_primary);
1227 | reshade::unregister_event(on_execute_secondary);
1228 |
1229 | reshade::unregister_event(on_present);
1230 |
1231 | reshade::unregister_event(on_begin_render_effects);
1232 | reshade::unregister_event(on_finish_render_effects);
1233 | reshade::unregister_event(update_effect_runtime);
1234 | }
1235 |
1236 | extern "C" __declspec(dllexport) const char *NAME = "Citra";
1237 | extern "C" __declspec(dllexport) const char *DESCRIPTION = "add-on that pre-processes depth buffer from Citra to be standardized / aligned for other add-ons to consume it.";
1238 |
1239 | BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
1240 | {
1241 | switch (fdwReason)
1242 | {
1243 | case DLL_PROCESS_ATTACH:
1244 | if (!reshade::register_addon(hinstDLL))
1245 | return FALSE;
1246 | register_addon_depth();
1247 | break;
1248 | case DLL_PROCESS_DETACH:
1249 | unregister_addon_depth();
1250 | reshade::unregister_addon(hinstDLL);
1251 | break;
1252 | }
1253 | return TRUE;
1254 | }
1255 |
--------------------------------------------------------------------------------