Skip to content

Commit ab3a652

Browse files
committed
Add GLSL SMAA 1x three-pass shaders
Introduce a three-pass GLSL SMAA implementation (edge, weights, blend) and associated lookup textures/assets to replace the old ARB fragment programs and eliminate an OpenGL feedback-loop that could produce a black viewport. Adds new shader files under openq4/glprogs and .install, updates postprocess_openq4.mtr to use glslProgram + shaderTexture/shaderParm bindings, and removes the legacy .vfp blends. Registers intrinsic SMAA lookup images (_smaaArea, _smaaSearch) and includes AreaTex/SearchTex headers; Image and Material code are adjusted to expose image filter/repeat and to honor explicit texture filter/repeat from materials. Documentation and packaging checks were updated to reflect the new assets, and a small engine-side singleplayer flashlight workaround (cvar + input remap) was added in Session.cpp.
1 parent 11e7a8c commit ab3a652

31 files changed

+15894
-156
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
uniform sampler2D ColorTex;
2+
uniform sampler2D BlendTex;
3+
uniform vec2 invTexSize;
4+
5+
void main() {
6+
vec2 texcoord = gl_TexCoord[0].st;
7+
8+
vec4 a;
9+
a.x = texture2D( BlendTex, texcoord + vec2( invTexSize.x, 0.0 ) ).a;
10+
a.y = texture2D( BlendTex, texcoord + vec2( 0.0, invTexSize.y ) ).g;
11+
a.wz = texture2D( BlendTex, texcoord ).xz;
12+
13+
if ( dot( a, vec4( 1.0, 1.0, 1.0, 1.0 ) ) < 0.00001 ) {
14+
gl_FragColor = texture2D( ColorTex, texcoord );
15+
return;
16+
}
17+
18+
bool horizontal = max( a.x, a.z ) > max( a.y, a.w );
19+
vec4 blendingOffset = vec4( 0.0, a.y, 0.0, a.w );
20+
vec2 blendingWeight = a.yw;
21+
22+
if ( horizontal ) {
23+
blendingOffset = vec4( a.x, 0.0, a.z, 0.0 );
24+
blendingWeight = a.xz;
25+
}
26+
27+
blendingWeight /= max( dot( blendingWeight, vec2( 1.0, 1.0 ) ), 0.00001 );
28+
29+
vec4 blendingCoord = blendingOffset * vec4( invTexSize.xy, -invTexSize.xy ) + texcoord.xyxy;
30+
vec4 color = blendingWeight.x * texture2D( ColorTex, blendingCoord.xy );
31+
color += blendingWeight.y * texture2D( ColorTex, blendingCoord.zw );
32+
33+
gl_FragColor = color;
34+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
void main() {
2+
gl_Position = ftransform();
3+
gl_TexCoord[0] = gl_MultiTexCoord0;
4+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
uniform sampler2D ColorTex;
2+
uniform vec2 invTexSize;
3+
4+
const float kThreshold = 0.1;
5+
const float kLocalContrastAdaptationFactor = 2.0;
6+
const vec3 kLumaWeights = vec3( 0.2126, 0.7152, 0.0722 );
7+
8+
float SampleLuma( vec2 uv ) {
9+
return dot( texture2D( ColorTex, uv ).rgb, kLumaWeights );
10+
}
11+
12+
void main() {
13+
vec2 texcoord = gl_TexCoord[0].st;
14+
vec4 offset0 = invTexSize.xyxy * vec4( -1.0, 0.0, 0.0, -1.0 ) + texcoord.xyxy;
15+
vec4 offset1 = invTexSize.xyxy * vec4( 1.0, 0.0, 0.0, 1.0 ) + texcoord.xyxy;
16+
vec4 offset2 = invTexSize.xyxy * vec4( -2.0, 0.0, 0.0, -2.0 ) + texcoord.xyxy;
17+
18+
float luma = SampleLuma( texcoord );
19+
float lumaLeft = SampleLuma( offset0.xy );
20+
float lumaTop = SampleLuma( offset0.zw );
21+
22+
vec4 delta;
23+
delta.xy = abs( luma - vec2( lumaLeft, lumaTop ) );
24+
25+
vec2 edges = step( vec2( kThreshold, kThreshold ), delta.xy );
26+
if ( dot( edges, vec2( 1.0, 1.0 ) ) == 0.0 ) {
27+
discard;
28+
}
29+
30+
float lumaRight = SampleLuma( offset1.xy );
31+
float lumaBottom = SampleLuma( offset1.zw );
32+
delta.zw = abs( luma - vec2( lumaRight, lumaBottom ) );
33+
34+
vec2 maxDelta = max( delta.xy, delta.zw );
35+
36+
float lumaLeftLeft = SampleLuma( offset2.xy );
37+
float lumaTopTop = SampleLuma( offset2.zw );
38+
delta.zw = abs( vec2( lumaLeft, lumaTop ) - vec2( lumaLeftLeft, lumaTopTop ) );
39+
40+
maxDelta = max( maxDelta, delta.zw );
41+
float finalDelta = max( maxDelta.x, maxDelta.y );
42+
43+
edges *= step( vec2( finalDelta, finalDelta ), vec2( kLocalContrastAdaptationFactor, kLocalContrastAdaptationFactor ) * delta.xy );
44+
gl_FragColor = vec4( edges, 0.0, 0.0 );
45+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
void main() {
2+
gl_Position = ftransform();
3+
gl_TexCoord[0] = gl_MultiTexCoord0;
4+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
uniform sampler2D EdgesTex;
2+
uniform sampler2D AreaTex;
3+
uniform sampler2D SearchTex;
4+
uniform vec2 invTexSize;
5+
6+
const float kMaxSearchSteps = 8.0;
7+
const float kAreaMaxDistance = 16.0;
8+
const vec2 kAreaTexPixelSize = vec2( 1.0 / 160.0, 1.0 / 560.0 );
9+
const float kAreaTexSubtexSize = 1.0 / 7.0;
10+
const vec2 kSearchTexSize = vec2( 66.0, 33.0 );
11+
const vec2 kSearchTexPackedSize = vec2( 64.0, 16.0 );
12+
13+
vec2 RoundVec2( vec2 value ) {
14+
return floor( value + vec2( 0.5, 0.5 ) );
15+
}
16+
17+
float SampleSearchLength( vec2 e, float offsetValue ) {
18+
vec2 scale = kSearchTexSize * vec2( 0.5, -1.0 );
19+
vec2 bias = kSearchTexSize * vec2( offsetValue, 1.0 );
20+
21+
scale += vec2( -1.0, 1.0 );
22+
bias += vec2( 0.5, -0.5 );
23+
24+
scale /= kSearchTexPackedSize;
25+
bias /= kSearchTexPackedSize;
26+
27+
return texture2D( SearchTex, scale * e + bias ).r;
28+
}
29+
30+
float SearchXLeft( vec2 texcoord, float endValue ) {
31+
vec2 e = vec2( 0.0, 1.0 );
32+
while ( texcoord.x > endValue && e.g > 0.8281 && e.r == 0.0 ) {
33+
e = texture2D( EdgesTex, texcoord ).rg;
34+
texcoord -= vec2( 2.0 * invTexSize.x, 0.0 );
35+
}
36+
37+
float offsetValue = -( 255.0 / 127.0 ) * SampleSearchLength( e, 0.0 ) + 3.25;
38+
return invTexSize.x * offsetValue + texcoord.x;
39+
}
40+
41+
float SearchXRight( vec2 texcoord, float endValue ) {
42+
vec2 e = vec2( 0.0, 1.0 );
43+
while ( texcoord.x < endValue && e.g > 0.8281 && e.r == 0.0 ) {
44+
e = texture2D( EdgesTex, texcoord ).rg;
45+
texcoord += vec2( 2.0 * invTexSize.x, 0.0 );
46+
}
47+
48+
float offsetValue = -( 255.0 / 127.0 ) * SampleSearchLength( e, 0.5 ) + 3.25;
49+
return -invTexSize.x * offsetValue + texcoord.x;
50+
}
51+
52+
float SearchYUp( vec2 texcoord, float endValue ) {
53+
vec2 e = vec2( 1.0, 0.0 );
54+
while ( texcoord.y > endValue && e.r > 0.8281 && e.g == 0.0 ) {
55+
e = texture2D( EdgesTex, texcoord ).rg;
56+
texcoord -= vec2( 0.0, 2.0 * invTexSize.y );
57+
}
58+
59+
float offsetValue = -( 255.0 / 127.0 ) * SampleSearchLength( e.gr, 0.0 ) + 3.25;
60+
return invTexSize.y * offsetValue + texcoord.y;
61+
}
62+
63+
float SearchYDown( vec2 texcoord, float endValue ) {
64+
vec2 e = vec2( 1.0, 0.0 );
65+
while ( texcoord.y < endValue && e.r > 0.8281 && e.g == 0.0 ) {
66+
e = texture2D( EdgesTex, texcoord ).rg;
67+
texcoord += vec2( 0.0, 2.0 * invTexSize.y );
68+
}
69+
70+
float offsetValue = -( 255.0 / 127.0 ) * SampleSearchLength( e.gr, 0.5 ) + 3.25;
71+
return -invTexSize.y * offsetValue + texcoord.y;
72+
}
73+
74+
vec2 SampleArea( vec2 dist, float e1, float e2, float offsetValue ) {
75+
vec2 texcoord = vec2( kAreaMaxDistance, kAreaMaxDistance ) * RoundVec2( 4.0 * vec2( e1, e2 ) ) + dist;
76+
texcoord = kAreaTexPixelSize * texcoord + 0.5 * kAreaTexPixelSize;
77+
texcoord.y = kAreaTexSubtexSize * offsetValue + texcoord.y;
78+
return texture2D( AreaTex, texcoord ).rg;
79+
}
80+
81+
void main() {
82+
vec2 texcoord = gl_TexCoord[0].st;
83+
vec2 rtSize = 1.0 / max( invTexSize, vec2( 0.000001, 0.000001 ) );
84+
vec2 pixcoord = texcoord * rtSize;
85+
86+
vec4 offset0 = invTexSize.xyxy * vec4( -0.25, -0.125, 1.25, -0.125 ) + texcoord.xyxy;
87+
vec4 offset1 = invTexSize.xyxy * vec4( -0.125, -0.25, -0.125, 1.25 ) + texcoord.xyxy;
88+
vec4 offset2 = vec4( invTexSize.x, invTexSize.x, invTexSize.y, invTexSize.y ) *
89+
( vec4( -2.0, 2.0, -2.0, 2.0 ) * kMaxSearchSteps ) +
90+
vec4( offset0.x, offset0.z, offset1.y, offset1.w );
91+
92+
vec4 weights = vec4( 0.0, 0.0, 0.0, 0.0 );
93+
vec2 e = texture2D( EdgesTex, texcoord ).rg;
94+
95+
if ( e.g > 0.0 ) {
96+
vec3 coords;
97+
coords.x = SearchXLeft( offset0.xy, offset2.x );
98+
coords.y = offset1.y;
99+
float leftEdge = texture2D( EdgesTex, coords.xy ).r;
100+
101+
coords.z = SearchXRight( offset0.zw, offset2.y );
102+
vec2 d = abs( RoundVec2( rtSize.xx * vec2( coords.x, coords.z ) - pixcoord.xx ) );
103+
vec2 sqrtD = sqrt( d );
104+
float rightEdge = texture2D( EdgesTex, coords.zy + vec2( invTexSize.x, 0.0 ) ).r;
105+
106+
weights.rg = SampleArea( sqrtD, leftEdge, rightEdge, 0.0 );
107+
}
108+
109+
if ( e.r > 0.0 ) {
110+
vec3 coords;
111+
coords.y = SearchYUp( offset1.xy, offset2.z );
112+
coords.x = offset0.x;
113+
float topEdge = texture2D( EdgesTex, coords.xy ).g;
114+
115+
coords.z = SearchYDown( offset1.zw, offset2.w );
116+
vec2 d = abs( RoundVec2( rtSize.yy * vec2( coords.y, coords.z ) - pixcoord.yy ) );
117+
vec2 sqrtD = sqrt( d );
118+
float bottomEdge = texture2D( EdgesTex, coords.xz + vec2( 0.0, invTexSize.y ) ).g;
119+
120+
weights.ba = SampleArea( sqrtD, topEdge, bottomEdge, 0.0 );
121+
}
122+
123+
gl_FragColor = weights;
124+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
void main() {
2+
gl_Position = ftransform();
3+
gl_TexCoord[0] = gl_MultiTexCoord0;
4+
}

.install/openq4/materials/postprocess_openq4.mtr

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,23 @@ postprocess/openq4_smaa_edge
4747
noShadows
4848
{
4949
blend gl_one, gl_zero
50-
program openq4_smaa_edge.vfp
51-
fragmentMap 0 clamp _postProcessAlbedo0
50+
glslProgram openq4_smaa_edge.fs
51+
shaderParm invTexSize 1.0 / VideoWidth, 1.0 / VideoHeight
52+
shaderTexture ColorTex nearest clamp _forwardRenderResolvedAlbedo
53+
}
54+
}
55+
56+
postprocess/openq4_smaa_weights
57+
{
58+
sort postProcess
59+
noShadows
60+
{
61+
blend gl_one, gl_zero
62+
glslProgram openq4_smaa_weights.fs
63+
shaderParm invTexSize 1.0 / VideoWidth, 1.0 / VideoHeight
64+
shaderTexture EdgesTex linear clamp _postProcessAlbedo1
65+
shaderTexture AreaTex linear clamp _smaaArea
66+
shaderTexture SearchTex linear clamp _smaaSearch
5267
}
5368
}
5469

@@ -58,9 +73,10 @@ postprocess/openq4_smaa_blend
5873
noShadows
5974
{
6075
blend gl_one, gl_zero
61-
program openq4_smaa_blend.vfp
62-
fragmentMap 0 clamp _currentRender
63-
fragmentMap 1 clamp _postProcessAlbedo1
76+
glslProgram openq4_smaa_blend.fs
77+
shaderParm invTexSize 1.0 / VideoWidth, 1.0 / VideoHeight
78+
shaderTexture ColorTex linear clamp _forwardRenderResolvedAlbedo
79+
shaderTexture BlendTex linear clamp _postProcessAlbedo0
6480
}
6581
}
6682

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ To play OpenQ4, you need:
7373
- **CRT Emulation**: Optional CRT post-process with scanlines, mask, curvature, and chromatic offset controls
7474
- **Shadow Mapping Pipeline**: Experimental shadow-map support for projected and point lights, with cascaded shadow maps (CSM) actively under development
7575
- **Resolution Scaling and Supersample Controls**: Screen-fraction rendering supports lower-resolution upscale modes and menu-exposed supersample-style presets for image-quality tuning
76-
- **Modern AA and Upscaling**: MSAA, SMAA, and high-quality resolution-scaling paths for cleaner output across a wide range of hardware
76+
- **Modern AA and Upscaling**: MSAA, official SMAA 1x (medium preset), and high-quality resolution-scaling paths for cleaner output across a wide range of hardware
7777

7878
### Display and UX Improvements
7979
- **Automatic Aspect-Ratio Management**: UI, FOV, zoom behavior, and view-weapon framing adapt from live render size instead of legacy manual aspect toggles
@@ -555,6 +555,7 @@ OpenQ4 builds upon the work of many talented developers and projects:
555555
- **GLEW Team** - Nigel Stewart, Milan Ikits, Marcelo E. Magallon, Lev Povalahev
556556
- **OpenAL Soft Contributors** - 3D audio implementation
557557
- **SDL Team** - Cross-platform framework
558+
- **Jorge Jimenez, Jose I. Echevarria, Belen Masia, Fernando Navarro, Diego Gutierrez** - [SMAA](https://www.iryoku.com/smaa/) reference implementation and lookup textures
558559

559560
### Special Thanks
560561
- The Quake and id Tech community for continued support and enthusiasm

docs-dev/renderdoc-workflow.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ This document covers the OpenQ4 RenderDoc status and the March 2026 black-viewpo
44

55
## Root Cause
66

7-
- The black viewport reports were not caused by missing SMAA assets in the current release package.
8-
- The actual failure was an undefined OpenGL feedback loop in the SMAA neighborhood-blend pass:
9-
- `OpenQ4-GameLibs/src/game/Game_render.cpp` already copies the resolved scene into `_currentRender` before the SMAA passes.
7+
- The black viewport reports were not caused by missing SMAA assets in the release package.
8+
- The older two-pass SMAA placeholder path had an undefined OpenGL feedback loop in the neighborhood-blend pass:
9+
- `OpenQ4-GameLibs/src/game/Game_render.cpp` already copied the resolved scene into `_currentRender` before the SMAA passes.
1010
- [`openq4/materials/postprocess_openq4.mtr`](../openq4/materials/postprocess_openq4.mtr) previously drew `postprocess/openq4_smaa_blend` into `_postProcessAlbedo0` while also sampling `_postProcessAlbedo0`.
1111
- Some drivers preserved the previous texture contents and appeared to work. Others returned black or undefined data. That is why the issue only reproduced for some users and clustered around `r_postAA 1`.
12+
- Current builds no longer use that path. `r_postAA 1` now runs a three-pass GLSL SMAA 1x implementation: edge detection, blending-weight calculation, and neighborhood blending.
1213

1314
## Current RenderDoc Limitation
1415

@@ -49,23 +50,40 @@ On the current renderer, the launch is still expected to fail before first frame
4950

5051
## What To Inspect
5152

52-
When reviewing an older or future capture in RenderDoc, find the draw using `postprocess/openq4_smaa_blend`.
53+
When reviewing an older or future capture in RenderDoc, inspect the three SMAA passes in order:
5354

54-
Expected bindings on a fixed build:
55+
- `postprocess/openq4_smaa_edge`
56+
- `postprocess/openq4_smaa_weights`
57+
- `postprocess/openq4_smaa_blend`
5558

56-
- Render target: `_postProcessAlbedo0`
57-
- Texture slot 0: `_currentRender`
58-
- Texture slot 1: `_postProcessAlbedo1`
59+
Expected bindings on a fixed build:
5960

60-
If texture slot 0 matches the render target, the build still contains the broken feedback loop.
61+
- `postprocess/openq4_smaa_edge`
62+
- Render target: `_postProcessAlbedo1`
63+
- Texture slot 0: `_currentRender`
64+
- `postprocess/openq4_smaa_weights`
65+
- Render target: `_postProcessAlbedo0`
66+
- Texture slot 0: `_postProcessAlbedo1`
67+
- Texture slot 1: `_smaaArea`
68+
- Texture slot 2: `_smaaSearch`
69+
- `postprocess/openq4_smaa_blend`
70+
- Render target: `_postProcessAlbedo1`
71+
- Texture slot 0: `_currentRender`
72+
- Texture slot 1: `_postProcessAlbedo0`
73+
74+
If the blend pass ever samples the same texture it is rendering to, the build still contains a broken feedback loop.
6175

6276
## Release Package Audit
6377

6478
`tools/build/package_nightly.py` now fails packaging if `pak0.pk4` is missing any of these required runtime files:
6579

6680
- `materials/postprocess_openq4.mtr`
67-
- `glprogs/openq4_smaa_edge.vfp`
68-
- `glprogs/openq4_smaa_blend.vfp`
81+
- `glprogs/openq4_smaa_edge.vs`
82+
- `glprogs/openq4_smaa_edge.fs`
83+
- `glprogs/openq4_smaa_weights.vs`
84+
- `glprogs/openq4_smaa_weights.fs`
85+
- `glprogs/openq4_smaa_blend.vs`
86+
- `glprogs/openq4_smaa_blend.fs`
6987

7088
This check is meant to catch packaging regressions in the postprocess stack before release artifacts ship.
7189

docs-user/display-settings.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ This guide covers OpenQ4 display/window settings for end users, including multi-
3131
| Setting | Default | What it does |
3232
|---|---:|---|
3333
| `r_multiSamples` | `0` | MSAA sample count for the main scene render target (`0`, `2`, `4`, `8`, `16`; `0` = off). |
34-
| `r_postAA` | `0` | Post AA mode (`0` = off, `1` = SMAA 1x). |
34+
| `r_postAA` | `0` | Post AA mode (`0` = off, `1` = official SMAA 1x using the medium preset). |
3535
| `r_msaaAlphaToCoverage` | `1` | Enables alpha-to-coverage for perforated/alpha-tested materials when MSAA is active. Helps foliage/fences look cleaner. |
3636
| `r_msaaResolveDepth` | `0` | Also resolves depth during MSAA resolve. Usually leave this off unless debugging a depth-dependent edge case. |
3737

@@ -191,4 +191,4 @@ vid_restart
191191
- If UI appears too centered/boxed on wide displays, set `ui_aspectCorrection 0`.
192192
- If the window opens off-screen after a monitor change, set `r_screen` explicitly to the target monitor and restart video; OpenQ4 will also attempt to recover automatically.
193193
- If AA settings seem unchanged, check values with `r_multiSamples`, `r_postAA`, and `r_msaaAlphaToCoverage`, then run `vid_restart`.
194-
- If enabling `r_postAA 1` turns the 3D viewport black on an older build, set `r_postAA 0`, run `vid_restart`, and attach `openq4.log` plus the output of `gfxInfo`. RenderDoc capture is not yet supported on the current OpenQ4 renderer.
194+
- If enabling `r_postAA 1` turns the 3D viewport black on an older build, set `r_postAA 0`, run `vid_restart`, and attach `openq4.log` plus the output of `gfxInfo`. Current builds use a three-pass GLSL SMAA path and should no longer hit the old feedback-loop failure. RenderDoc capture is not yet supported on the current OpenQ4 renderer.

0 commit comments

Comments
 (0)