diff --git a/.gitignore b/.gitignore
index a4003d396..c46e6716b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -142,3 +142,7 @@ vcpkg/
# Compiled SPIR-V shaders (built from source by glslangValidator)
*.spv
+
+# Vulkan SPIR-V is regenerated by CMake (glslangValidator required)
+Resources/Shaders/Vulkan/**/*.spv
+Resources/Shaders/Vulkan/*.spv
diff --git a/Resources/Shaders/Vulkan/BasicMap.vert b/Resources/Shaders/Vulkan/BasicMap.vert
index e27d27c43..af4eabfbe 100644
--- a/Resources/Shaders/Vulkan/BasicMap.vert
+++ b/Resources/Shaders/Vulkan/BasicMap.vert
@@ -41,6 +41,7 @@ layout(location = 0) in uvec3 positionAttribute;
layout(location = 1) in uvec2 aoCoordAttribute;
layout(location = 2) in uvec3 colorAttribute; // colorRed, colorGreen, colorBlue
layout(location = 3) in ivec3 normalAttribute;
+layout(location = 4) in ivec3 fixedPositionAttribute; // face-center * 2 (chunk-local)
layout(location = 0) out vec4 color; // xyz = linearized vertex color, w = sun lambert
layout(location = 1) out vec3 ambientLight; // ambient lighting component (hemisphere fallback)
@@ -90,7 +91,12 @@ void main() {
// mapShadowCoord.y -= mapShadowCoord.z (diagonal sun projection)
// mapShadowCoord.z /= 255.0 (normalize height)
// mapShadowCoord.xy /= 512.0 (normalize to texture coords)
- vec3 wPos = worldPos.xyz;
+ // Use the face-center "fixed position" (matches GL fixedPositionAttribute)
+ // instead of the raw vertex position. Vertices lie exactly on voxel
+ // boundaries; sampling the map-shadow texture there picks the neighboring
+ // column and leaks a lit sliver along face edges (e.g. top of north faces
+ // seen from the south). The face center is safely inside the voxel.
+ vec3 wPos = vec3(fixedPositionAttribute) * 0.5 + pushConstants.modelOrigin;
shadowCoord = vec3(wPos.x / 512.0, (wPos.y - wPos.z) / 512.0, wPos.z / 255.0);
// Fog density based on horizontal distance (matching SW/GL implementation)
@@ -102,7 +108,7 @@ void main() {
// AO 3D-texture coords. World position with z+1 (the 0-th slice is the
// "below ground" guard plane), divided by texture extent. Map dimensions
// are 512x512x64 in this game, so the texture is 512x512x65.
- aoCoord = (worldPos.xyz + vec3(0.0, 0.0, 1.0)) / vec3(512.0, 512.0, 65.0);
+ aoCoord = (wPos + vec3(0.0, 0.0, 1.0)) / vec3(512.0, 512.0, 65.0);
// 2D AO atlas coords (matches GL BasicBlock.vs). The atlas is 256x256 with
// 16x16 precomputed AO tiles; aoCoordAttribute holds tile_x*16 + corner
@@ -110,7 +116,7 @@ void main() {
ambientOcclusionCoord = (vec2(aoCoordAttribute) + 0.5) * (1.0 / 256.0);
// Radiosity 3D-texture coords (matches GL MapRadiosity.vs).
- radiosityTextureCoord = worldPos.xyz / vec3(512.0, 512.0, 64.0);
+ radiosityTextureCoord = wPos / vec3(512.0, 512.0, 64.0);
normalVarying = normalFloat;
// Per-cascade light-clip coords for model-shadow sampling. Ortho => w == 1,
diff --git a/Resources/Shaders/Vulkan/BasicMapPhys.vert b/Resources/Shaders/Vulkan/BasicMapPhys.vert
index c47f8e787..0d62cad39 100644
--- a/Resources/Shaders/Vulkan/BasicMapPhys.vert
+++ b/Resources/Shaders/Vulkan/BasicMapPhys.vert
@@ -37,6 +37,7 @@ layout(location = 0) in uvec3 positionAttribute;
layout(location = 1) in uvec2 aoCoordAttribute;
layout(location = 2) in uvec3 colorAttribute;
layout(location = 3) in ivec3 normalAttribute;
+layout(location = 4) in ivec3 fixedPositionAttribute; // face-center * 2 (chunk-local)
layout(location = 0) out vec4 color; // xyz = linearized vertex color, w = sun lambert (may be negative)
layout(location = 1) out vec3 ambientLight; // hemisphere ambient fallback
@@ -76,8 +77,10 @@ void main() {
color = vec4(vertexColor, lambert);
- // Shadow coordinates
- vec3 wPos = worldPos.xyz;
+ // Shadow coordinates — sample at the face center ("fixed position",
+ // matches GL fixedPositionAttribute) so voxel-boundary vertices don't
+ // leak the neighboring column's shadow value (lit sliver on face edges).
+ vec3 wPos = vec3(fixedPositionAttribute) * 0.5 + pushConstants.modelOrigin;
shadowCoord = vec3(wPos.x / 512.0, (wPos.y - wPos.z) / 512.0, wPos.z / 255.0);
// Fog
@@ -91,9 +94,9 @@ void main() {
viewSpaceNormal = normalize((pushConstants.viewMatrix * vec4(normalFloat, 0.0)).xyz);
reflectionDir = reflect(worldPos.xyz - pushConstants.viewOrigin, normalFloat);
- aoCoord = (worldPos.xyz + vec3(0.0, 0.0, 1.0)) / vec3(512.0, 512.0, 65.0);
+ aoCoord = (wPos + vec3(0.0, 0.0, 1.0)) / vec3(512.0, 512.0, 65.0);
// Radiosity 3D-texture coords (matches GL MapRadiosity.vs).
- radiosityTextureCoord = worldPos.xyz / vec3(512.0, 512.0, 64.0);
+ radiosityTextureCoord = wPos / vec3(512.0, 512.0, 64.0);
normalVarying = normalFloat;
}
diff --git a/Resources/Shaders/Vulkan/BasicModelVertexColor.frag b/Resources/Shaders/Vulkan/BasicModelVertexColor.frag
index e7b25e219..cc28b3972 100644
--- a/Resources/Shaders/Vulkan/BasicModelVertexColor.frag
+++ b/Resources/Shaders/Vulkan/BasicModelVertexColor.frag
@@ -33,6 +33,14 @@ layout(set = 0, binding = 4) uniform sampler3D radiosityTextureY;
layout(set = 0, binding = 5) uniform sampler3D radiosityTextureZ;
layout(set = 0, binding = 6) uniform sampler2D ambientOcclusionAtlas; // Gfx/AmbientOcclusion.png
+layout(set = 1, binding = 0) uniform ShadowSampling {
+ mat4 cascadeMatrix[3];
+ int enabled;
+} shadowSampling;
+layout(set = 1, binding = 1) uniform sampler2D modelShadowMap0;
+layout(set = 1, binding = 2) uniform sampler2D modelShadowMap1;
+layout(set = 1, binding = 3) uniform sampler2D modelShadowMap2;
+
layout(location = 0) in vec4 color; // xyz = vertexColor, w = sun lambert
layout(location = 1) in vec3 ambientLight; // hemisphere ambient fallback (kept for VS↔FS compat)
layout(location = 2) in vec3 customColor;
@@ -44,9 +52,32 @@ layout(location = 7) in vec3 radiosityTextureCoord;
layout(location = 8) in vec3 normalVarying;
layout(location = 9) in vec2 ambientOcclusionCoord; // 2D coords into AO atlas
layout(location = 10) in float waterClip; // <0 = below the reflection plane
+layout(location = 11) in vec3 modelShadowCoord0; // light-clip coords per cascade
+layout(location = 12) in vec3 modelShadowCoord1;
+layout(location = 13) in vec3 modelShadowCoord2;
layout(location = 0) out vec4 fragColor;
+// Same cascade sampling as BasicMap.frag. Local Z 0 = sun side; a smaller
+// stored depth means an occluder nearer the sun. Bias avoids self-shadow acne
+// (models render into these cascades themselves).
+float SampleModelCascade(sampler2D tex, vec3 c) {
+ vec2 uv = c.xy * 0.5 + 0.5;
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 || c.z < 0.0 || c.z > 1.0)
+ return -1.0;
+ float stored = texture(tex, uv).r;
+ return (stored < c.z - 0.0015) ? 0.0 : 1.0;
+}
+
+float EvaluteModelShadow() {
+ if (shadowSampling.enabled == 0)
+ return 1.0;
+ float v = SampleModelCascade(modelShadowMap0, modelShadowCoord0);
+ if (v < 0.0) v = SampleModelCascade(modelShadowMap1, modelShadowCoord1);
+ if (v < 0.0) v = SampleModelCascade(modelShadowMap2, modelShadowCoord2);
+ return (v < 0.0) ? 1.0 : v;
+}
+
vec3 DecodeRadiosityValue(vec3 val) {
val *= 1023.0 / 1022.0;
val = (val * 2.0) - 1.0;
@@ -62,6 +93,7 @@ void main() {
// Evaluate map shadow (matching OpenGL Map.fs: EvaluateMapShadow)
float shadowVal = texture(mapShadowTexture, shadowCoord.xy).w;
float shadow = (shadowVal < shadowCoord.z - 0.0001) ? 0.0 : 1.0;
+ shadow *= EvaluteModelShadow(); // model-on-model cascades (set 1)
vec3 vertexColor = color.xyz;
diff --git a/Resources/Shaders/Vulkan/BasicModelVertexColor.vert b/Resources/Shaders/Vulkan/BasicModelVertexColor.vert
index a32a25f6b..ddb7396d1 100644
--- a/Resources/Shaders/Vulkan/BasicModelVertexColor.vert
+++ b/Resources/Shaders/Vulkan/BasicModelVertexColor.vert
@@ -32,6 +32,11 @@ layout(push_constant) uniform PushConstants {
vec3 sunDirection; // points toward the sun (renderer GetSunDirection)
} pushConstants;
+layout(set = 1, binding = 0) uniform ShadowSampling {
+ mat4 cascadeMatrix[3];
+ int enabled;
+} shadowSampling;
+
layout(location = 0) in uvec3 positionAttribute;
layout(location = 1) in uvec3 colorAttribute; // RGB color stored in u,v as (R, G, B)
layout(location = 2) in ivec3 normalAttribute;
@@ -49,6 +54,9 @@ layout(location = 7) out vec3 radiosityTextureCoord; // 3D coords into radiosity
layout(location = 8) out vec3 normalVarying; // world-space surface normal
layout(location = 9) out vec2 ambientOcclusionCoord; // 2D coords into AO atlas
layout(location = 10) out float waterClip; // >=0 keep, <0 clip below the reflection plane
+layout(location = 11) out vec3 modelShadowCoord0; // light-clip coords per cascade
+layout(location = 12) out vec3 modelShadowCoord1;
+layout(location = 13) out vec3 modelShadowCoord2;
// Keep gl_Position bit-identical with ModelDynamicLit.vert so the additive
// dynamic-light pass (depth test EQUAL) matches this opaque pass's depth and
@@ -86,7 +94,9 @@ void main() {
customColorOut = pushConstants.customColor;
// Shadow map coordinates (sun projects diagonally along y-z)
- shadowCoord = vec3(worldPos.x / 512.0, (worldPos.y - worldPos.z) / 512.0, worldPos.z / 255.0);
+ // Sample slightly inside the surface to avoid shadow bleed at voxel boundaries
+ vec3 shadowPos = worldPos - normalFloat * 0.05;
+ shadowCoord = vec3(shadowPos.x / 512.0, (shadowPos.y - shadowPos.z) / 512.0, shadowPos.z / 255.0);
// Fog density pre-computed on CPU from model world position
fogDensityOut = vec3(pushConstants.fogDensity);
@@ -103,6 +113,12 @@ void main() {
// 256-pixel-space tile + corner offsets baked per-face in EmitFace.
ambientOcclusionCoord = (vec2(aoXAttribute, aoYAttribute) + 0.5) * (1.0 / 256.0);
+ // Model-on-model shadow cascades (same set-1 sampling as BasicMap).
+ vec4 worldPos4 = vec4(worldPos, 1.0);
+ modelShadowCoord0 = (shadowSampling.cascadeMatrix[0] * worldPos4).xyz;
+ modelShadowCoord1 = (shadowSampling.cascadeMatrix[1] * worldPos4).xyz;
+ modelShadowCoord2 = (shadowSampling.cascadeMatrix[2] * worldPos4).xyz;
+
// Reflection-pass water clip: negative for fragments below the water plane.
// In the normal scene mirrorClipZ is +inf, so this stays positive (no clip).
waterClip = pushConstants.mirrorClipZ - worldPos.z;
diff --git a/Resources/Shaders/Vulkan/BasicModelVertexColorPhys.vert b/Resources/Shaders/Vulkan/BasicModelVertexColorPhys.vert
index 5e5d779e4..027963308 100644
--- a/Resources/Shaders/Vulkan/BasicModelVertexColorPhys.vert
+++ b/Resources/Shaders/Vulkan/BasicModelVertexColorPhys.vert
@@ -90,7 +90,9 @@ void main() {
customColorOut = pushConstants.customColor;
// Shadow coordinates
- shadowCoord = vec3(worldPos.x / 512.0, (worldPos.y - worldPos.z) / 512.0, worldPos.z / 255.0);
+ // Sample slightly inside the surface to avoid shadow bleed at voxel boundaries
+ vec3 shadowPos = worldPos - worldNormal * 0.05;
+ shadowCoord = vec3(shadowPos.x / 512.0, (shadowPos.y - shadowPos.z) / 512.0, shadowPos.z / 255.0);
// Fog
fogDensityOut = vec3(pushConstants.fogDensity);
diff --git a/Resources/Shaders/Vulkan/PostFilters/BloomComposite.vk.fs b/Resources/Shaders/Vulkan/PostFilters/BloomComposite.vk.fs
index e86f8f927..450094561 100644
--- a/Resources/Shaders/Vulkan/PostFilters/BloomComposite.vk.fs
+++ b/Resources/Shaders/Vulkan/PostFilters/BloomComposite.vk.fs
@@ -20,18 +20,42 @@
#version 450
-// Bloom final composite pass.
-// Mirrors GLBloomFilter's GammaMix call with mix1=0.8, mix2=0.2 on a linear HDR framebuffer:
-// outColor = scene * 0.8 + bloom * 0.2
+// Bloom final composite — Vulkan port of GL's LensDust.fs (the real
+// r_bloom composite). Linear framebuffer, so GL's linearize/sqrt round
+// trip is dropped; the dust texture still gets squared to linearize it
+// (it is a plain sRGB-ish JPG).
-layout(binding = 0) uniform sampler2D sceneTexture;
-layout(binding = 1) uniform sampler2D bloomTexture;
+layout(binding = 0) uniform sampler2D inputTexture;
+layout(binding = 1) uniform sampler2D blurTexture1;
+layout(binding = 2) uniform sampler2D dustTexture;
+layout(binding = 3) uniform sampler2D noiseTexture;
+
+layout(push_constant) uniform LensDustPC {
+ vec4 noiseTexCoordFactor;
+} pc;
layout(location = 0) in vec2 texCoord;
layout(location = 0) out vec4 outColor;
void main() {
- vec3 scene = texture(sceneTexture, texCoord).rgb;
- vec3 bloom = texture(bloomTexture, texCoord).rgb;
- outColor = vec4(scene * 0.8 + bloom * 0.2, 1.0);
+ vec3 dust1 = texture(dustTexture, texCoord).xyz;
+ dust1 *= dust1; // linearize
+
+ vec3 blur1 = texture(blurTexture1, texCoord).xyz;
+
+ vec3 sum = dust1 * blur1;
+
+ vec3 final = texture(inputTexture, texCoord).xyz;
+
+ final *= 0.95;
+ final += sum * 2.0;
+
+ // film grain
+ vec4 noiseTexCoord = texCoord.xyxy * pc.noiseTexCoordFactor;
+ float grain = texture(noiseTexture, noiseTexCoord.xy).x;
+ grain += texture(noiseTexture, noiseTexCoord.zw).x;
+ grain = fract(grain) - 0.5;
+ final += grain * 0.003;
+
+ outColor = vec4(max(final, 0.0), 1.0);
}
diff --git a/Resources/Shaders/Vulkan/PostFilters/CameraBlur.vk.fs b/Resources/Shaders/Vulkan/PostFilters/CameraBlur.vk.fs
new file mode 100644
index 000000000..bc600574e
--- /dev/null
+++ b/Resources/Shaders/Vulkan/PostFilters/CameraBlur.vk.fs
@@ -0,0 +1,81 @@
+/*
+ Copyright (c) 2013 Fran6nd
+
+ This file is part of ZeroSpades, a fork of OpenSpades.
+
+ OpenSpades is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OpenSpades is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OpenSpades. If not, see .
+
+ */
+
+#version 450
+
+// Vulkan port of PostFilters/CameraBlur.fs.
+// Offscreen buffer is linear (LINEAR_FRAMEBUFFER), so no
+// linearize / sqrt round trip. Depth-weighted 5-tap smear along the
+// per-pixel motion vector; depth^2 weighting keeps the view weapon
+// (depth ~[0,0.1]) mostly unsmeared.
+
+layout(binding = 0) uniform sampler2D mainTexture;
+layout(binding = 1) uniform sampler2D depthTexture;
+
+layout(push_constant) uniform CameraBlurPC {
+ mat4 reverseMatrix;
+ float shutterTimeScale;
+} pc;
+
+layout(location = 0) in vec2 newCoord;
+layout(location = 1) in vec3 oldCoord;
+
+layout(location = 0) out vec4 fragColor;
+
+vec4 getSample(vec2 coord) {
+ vec3 color = texture(mainTexture, coord).xyz;
+ float depth = texture(depthTexture, coord).x;
+ float weight = depth * depth;
+ weight = min(weight, 1.0) + 0.0001;
+ return vec4(color * weight, weight);
+}
+
+void main() {
+ vec2 nextCoord = newCoord;
+ vec2 prevCoord = oldCoord.xy / oldCoord.z;
+ vec2 coord;
+
+ vec4 sum;
+
+ coord = mix(nextCoord, prevCoord, 0.0);
+ sum = getSample(coord);
+
+ // use latest sample's weight for camera blur strength
+ float allWeight = sum.w;
+ vec4 sum2;
+
+ sum /= sum.w;
+
+ coord = mix(nextCoord, prevCoord, pc.shutterTimeScale * 0.2);
+ sum2 = getSample(coord);
+
+ coord = mix(nextCoord, prevCoord, pc.shutterTimeScale * 0.4);
+ sum2 += getSample(coord);
+
+ coord = mix(nextCoord, prevCoord, pc.shutterTimeScale * 0.6);
+ sum2 += getSample(coord);
+
+ coord = mix(nextCoord, prevCoord, pc.shutterTimeScale * 0.8);
+ sum2 += getSample(coord);
+
+ sum += sum2 * allWeight;
+
+ fragColor = vec4(sum.xyz / sum.w, 1.0);
+}
diff --git a/Resources/Shaders/Vulkan/PostFilters/CameraBlur.vk.vs b/Resources/Shaders/Vulkan/PostFilters/CameraBlur.vk.vs
new file mode 100644
index 000000000..33d4e1948
--- /dev/null
+++ b/Resources/Shaders/Vulkan/PostFilters/CameraBlur.vk.vs
@@ -0,0 +1,46 @@
+/*
+ Copyright (c) 2013 Fran6nd
+
+ This file is part of ZeroSpades, a fork of OpenSpades.
+
+ OpenSpades is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OpenSpades is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OpenSpades. If not, see .
+
+ */
+
+#version 450
+
+// Vulkan port of PostFilters/CameraBlur.vs.
+// Fullscreen triangle; per-vertex reprojection of the current-frame
+// texcoord into last frame's frame via reverseMatrix.
+
+layout(push_constant) uniform CameraBlurPC {
+ mat4 reverseMatrix;
+ float shutterTimeScale;
+} pc;
+
+layout(location = 0) out vec2 newCoord;
+layout(location = 1) out vec3 oldCoord;
+
+void main() {
+ vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
+ gl_Position = vec4(uv * 2.0 - 1.0, 0.5, 1.0);
+
+ newCoord = uv;
+
+ // GL math runs in bottom-left-origin coords; our texcoords are
+ // top-left-origin, so flip Y going in and coming out.
+ vec4 cvt = vec4(uv.x - 0.5, 0.5 - uv.y, 0.0, 1.0);
+ vec3 o = (pc.reverseMatrix * cvt).xyz;
+ oldCoord = vec3(o.x + 0.5 * o.z, -o.y + 0.5 * o.z, o.z);
+}
diff --git a/Resources/Shaders/Vulkan/PostFilters/Fog.vk.fs b/Resources/Shaders/Vulkan/PostFilters/Fog.vk.fs
index 579829c1c..2c91169a3 100644
--- a/Resources/Shaders/Vulkan/PostFilters/Fog.vk.fs
+++ b/Resources/Shaders/Vulkan/PostFilters/Fog.vk.fs
@@ -92,7 +92,15 @@ void main() {
vec3 startPos = shadowOrigin;
vec3 dir = shadowRayDirection;
- if (length(dir.xy) < 0.0001) dir.xy = vec2(0.0001);
+
+ // Near-vertical rays: voxelDistanceFactor -> 0, so the fog integral is
+ // ~zero anyway. Snapping dir.xy (old GL-style guard) made adjacent
+ // fragments flip between the real and snapped direction, glitching the
+ // view straight down. Skip the march instead.
+ if (length(dir.xy) < 0.0001 * length(dir)) {
+ outColor = texture(colorTexture, texCoord);
+ return;
+ }
if (dir.x == 0.0) dir.x = 0.00001;
if (dir.y == 0.0) dir.y = 0.00001;
dir = normalize(dir);
diff --git a/Resources/Shaders/Vulkan/PostFilters/Gauss1DRGBA.vk.fs b/Resources/Shaders/Vulkan/PostFilters/Gauss1DRGBA.vk.fs
new file mode 100644
index 000000000..5fc6f1710
--- /dev/null
+++ b/Resources/Shaders/Vulkan/PostFilters/Gauss1DRGBA.vk.fs
@@ -0,0 +1,48 @@
+/*
+ Copyright (c) 2013 Fran6nd
+
+ This file is part of ZeroSpades, a fork of OpenSpades.
+
+ OpenSpades is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OpenSpades is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OpenSpades. If not, see .
+
+ */
+
+// RGBA variant of Gauss1D.vk.fs (which is single-channel, used by DoF CoC).
+// Separable 4-tap gaussian, kernel from the original 1dgaussGen.rb; used by
+// the bloom (LensDust) downsample chain for GL parity.
+// unitShift = (1/w, 0) for horizontal, (0, 1/h) for vertical.
+// Pair with PassThrough.vk.vs.
+
+#version 450
+
+layout(binding = 0) uniform sampler2D mainTexture;
+
+layout(push_constant) uniform Params {
+ vec2 unitShift;
+} pc;
+
+layout(location = 0) in vec2 texCoord;
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 s1 = pc.unitShift * 2.30654399138844;
+ vec2 s2 = pc.unitShift * 0.629455560633963;
+ const float w1 = 0.178704407070903;
+ const float w2 = 0.321295592929097;
+
+ outColor = texture(mainTexture, texCoord - s1) * w1;
+ outColor += texture(mainTexture, texCoord - s2) * w2;
+ outColor += texture(mainTexture, texCoord + s2) * w2;
+ outColor += texture(mainTexture, texCoord + s1) * w1;
+}
diff --git a/Resources/Shaders/Vulkan/PostFilters/LensFlareScanner.vk.fs b/Resources/Shaders/Vulkan/PostFilters/LensFlareScanner.vk.fs
index 5d370d890..4a8de81f4 100644
--- a/Resources/Shaders/Vulkan/PostFilters/LensFlareScanner.vk.fs
+++ b/Resources/Shaders/Vulkan/PostFilters/LensFlareScanner.vk.fs
@@ -22,14 +22,22 @@
//
// Port of Shaders/OpenGL/LensFlare/Scanner.fs.
//
-// Reads a sampler2DShadow comparing scanPos.z against the offscreen
-// depth texture. The bilinear-filtered comparison gives a soft
-// occlusion factor in [0, 1]. A radial mask trims the disc inside
-// circlePos (radius = 32 in NDC pixels).
+// Compares scanPos.z against the offscreen depth texture, done in-shader
+// rather than via a sampler2DShadow hardware compare: the depth texture
+// this samples is an R32F color image (CopySceneDepthForSampling / the
+// MSAA depth-resolve pass copy the D32 depth attachment into an R32F
+// color target, since MoltenVK maps GLSL sampler2D to Metal's
+// texture2d, which cannot read a D32/depth2d image — see
+// VulkanFramebufferManager::sceneDepthSampleImage). Hardware compare is
+// invalid on a color format regardless, so the compare must be manual.
+// Softness comes from the 3x Gauss blur applied afterwards.
+//
+// A radial mask trims the disc inside circlePos (radius = 32 in NDC
+// pixels).
#version 450
-layout(binding = 0) uniform sampler2DShadow depthTexture;
+layout(binding = 0) uniform sampler2D depthTexture;
layout(location = 0) in vec3 scanPos;
layout(location = 1) in vec2 circlePos;
@@ -37,7 +45,8 @@ layout(location = 1) in vec2 circlePos;
layout(location = 0) out vec4 outColor;
void main() {
- float val = texture(depthTexture, scanPos);
+ float depth = texture(depthTexture, scanPos.xy).x;
+ float val = step(scanPos.z, depth);
// Circle trim — matches the GL Scanner.fs `radius = 32` parameter.
float rad = length(circlePos) * 32.0;
diff --git a/Resources/Shaders/Vulkan/PostFilters/ResampleBicubic.vk.fs b/Resources/Shaders/Vulkan/PostFilters/ResampleBicubic.vk.fs
new file mode 100644
index 000000000..81af39050
--- /dev/null
+++ b/Resources/Shaders/Vulkan/PostFilters/ResampleBicubic.vk.fs
@@ -0,0 +1,94 @@
+/*
+ Copyright (c) 2019 yvt
+
+ This file is part of OpenSpades.
+
+ OpenSpades is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OpenSpades is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OpenSpades. If not, see .
+
+ */
+
+#version 450
+
+// Vulkan port of PostFilters/ResampleBicubic.fs (r_scaleFilter == 2).
+
+/*
+ * This bi-cubic spline interpolation code is based on
+ *
+ * http://www.dannyruijters.nl/cubicinterpolation/
+ * https://github.com/DannyRuijters/CubicInterpolationCUDA
+ *
+ * @license Copyright (c) 2008-2013, Danny Ruijters. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of the copyright holders nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ */
+
+layout(binding = 0) uniform sampler2D mainTexture;
+
+layout(push_constant) uniform ResamplePC {
+ vec2 inverseVP; // 1 / source size in pixels
+} pc;
+
+layout(location = 0) in vec2 texCoord;
+
+layout(location = 0) out vec4 fragColor;
+
+void bspline_weights(vec2 fraction, out vec2 w0, out vec2 w1, out vec2 w2, out vec2 w3) {
+ vec2 one_frac = 1.0 - fraction;
+ vec2 squared = fraction * fraction;
+ vec2 one_sqd = one_frac * one_frac;
+
+ w0 = 1.0 / 6.0 * one_sqd * one_frac;
+ w1 = 2.0 / 3.0 - 0.5 * squared * (2.0 - fraction);
+ w2 = 2.0 / 3.0 - 0.5 * one_sqd * (2.0 - one_frac);
+ w3 = 1.0 / 6.0 * squared * fraction;
+}
+
+vec3 cubicTex2D(sampler2D tex, vec2 coord, vec2 inverseTexSize) {
+ vec2 coord_grid = coord - 0.5;
+ vec2 index = floor(coord_grid);
+ vec2 fraction = coord_grid - index;
+ vec2 w0, w1, w2, w3;
+ bspline_weights(fraction, w0, w1, w2, w3);
+
+ vec2 g0 = w0 + w1;
+ vec2 g1 = w2 + w3;
+ vec2 h0 = (w1 / g0) - vec2(0.5) + index;
+ vec2 h1 = (w3 / g1) + vec2(1.5) + index;
+
+ // four hardware-filtered fetches replace 16 point fetches
+ vec3 tex00 = texture(tex, vec2(h0.x, h0.y) * inverseTexSize).xyz;
+ vec3 tex10 = texture(tex, vec2(h1.x, h0.y) * inverseTexSize).xyz;
+ vec3 tex01 = texture(tex, vec2(h0.x, h1.y) * inverseTexSize).xyz;
+ vec3 tex11 = texture(tex, vec2(h1.x, h1.y) * inverseTexSize).xyz;
+
+ tex00 = g0.y * tex00 + g1.y * tex01;
+ tex10 = g0.y * tex10 + g1.y * tex11;
+
+ return g0.x * tex00 + g1.x * tex10;
+}
+
+void main() {
+ // texCoord in [0,1] over the output; convert to source pixel coords
+ vec2 pixelCoord = texCoord / pc.inverseVP;
+ fragColor = vec4(cubicTex2D(mainTexture, pixelCoord, pc.inverseVP), 1.0);
+}
diff --git a/Resources/Shaders/Vulkan/Water.frag b/Resources/Shaders/Vulkan/Water.frag
deleted file mode 100644
index 2ab4946b9..000000000
--- a/Resources/Shaders/Vulkan/Water.frag
+++ /dev/null
@@ -1,202 +0,0 @@
-#version 450
-
-/*
- Copyright (c) 2013 Fran6nd
-
- This file is part of ZeroSpades, a fork of OpenSpades.
-
- OpenSpades is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- OpenSpades is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with OpenSpades. If not, see .
-
- */
-
-// Vulkan uses SRGB framebuffer
-#define LINEAR_FRAMEBUFFER 1
-
-layout(location = 0) in vec3 v_fogDensity;
-layout(location = 1) in vec3 v_screenPosition;
-layout(location = 2) in vec3 v_viewPosition;
-layout(location = 3) in vec3 v_worldPosition;
-
-layout(binding = 0) uniform sampler2D screenTexture;
-layout(binding = 1) uniform sampler2D depthTexture;
-layout(binding = 2) uniform sampler2D mainTexture;
-layout(binding = 3) uniform sampler2D waveTexture;
-
-layout(std140, binding = 4) uniform WaterUBO {
- vec4 fogColor; // xyz used
- vec4 skyColor; // xyz used
- vec2 zNearFar;
- vec2 _pad0;
- vec4 fovTan;
- vec4 waterPlane;
- vec4 viewOriginVector; // use .xyz
- vec2 displaceScale;
- vec2 _pad1;
-} waterUBO;
-
-layout(location = 0) out vec4 fragColor;
-
-// Sun direction from OpenGL reference: (0, -1, -1) normalized
-const vec3 sunDirection = normalize(vec3(0.0, -1.0, -1.0));
-
-// Sun lighting: matches OpenGL implementation
-// Returns vec3(0.6) for full sunlight (shadows not implemented yet)
-vec3 EvaluateSunLight() {
- return vec3(0.6); // Placeholder - should multiply by shadow visibility
-}
-
-// Ambient lighting: matches OpenGL implementation
-vec3 EvaluateAmbientLight(float detailAmbientOcclusion) {
- return vec3(0.3, 0.3, 0.35) * detailAmbientOcclusion;
-}
-
-float decodeDepth(float w, float near, float far) {
- return far * near / mix(far, near, w);
-}
-
-float depthAt(vec2 pt) {
- float w = texture(depthTexture, pt).x;
- return decodeDepth(w, waterUBO.zNearFar.x, waterUBO.zNearFar.y);
-}
-
-void main() {
- vec3 worldPositionFromOrigin = v_worldPosition - waterUBO.viewOriginVector.xyz;
- vec4 waveCoord = v_worldPosition.xyxy
- * vec4(vec2(0.08), vec2(0.15704))
- + vec4(0.0, 0.0, 0.754, 0.1315);
-
- vec2 waveCoord2 = v_worldPosition.xy * 0.02344 + vec2(0.154, 0.7315);
-
- // evaluate waveform
- vec3 wave = texture(waveTexture, waveCoord.xy).xyz;
- wave = mix(vec3(-1.0), vec3(1.0), wave);
- wave.xy *= 0.08 / 200.0;
-
- // detail (Far Cry seems to use this technique)
- vec2 wave2 = texture(waveTexture, waveCoord.zw).xy;
- wave2 = mix(vec2(-1.0), vec2(1.0), wave2);
- wave2.xy *= 0.15704 / 200.0;
- wave.xy += wave2;
-
- // rough
- wave2 = texture(waveTexture, waveCoord2.xy).xy;
- wave2 = mix(vec2(-1.0), vec2(1.0), wave2);
- wave2.xy *= 0.02344 / 200.0;
- wave.xy += wave2;
-
- wave.z = (1.0 / 128.0);
- wave.xyz = normalize(wave.xyz);
-
- vec2 origScrPos = v_screenPosition.xy / v_screenPosition.z;
- vec2 scrPos = origScrPos;
-
- float scale = 1.0 / v_viewPosition.z;
- vec2 disp = wave.xy * 0.1;
- scrPos += disp * scale * waterUBO.displaceScale * 4.0;
-
- // check envelope length.
- // if the displaced location points the out of the water,
- // reset to the original pos.
- float depth = depthAt(scrPos);
- // zNearFar is stored in waterUBO.zNearFar
-
-
- // convert to view coord
- vec3 sampledViewCoord = vec3(mix(waterUBO.fovTan.zw, waterUBO.fovTan.xy, scrPos), 1.0) * -depth;
- float planeDistance = dot(vec4(sampledViewCoord, 1.0), waterUBO.waterPlane);
- if (planeDistance > 0.0) {
- // reset!
- // original pos must be in the water.
- scrPos = origScrPos;
- depth = depthAt(scrPos);
- if (depth + v_viewPosition.z < 0.0) {
- // if the pixel is obscured by a object,
- // this fragment of water is not visible
- //discard; done by early-Z test
- }
- } else {
- depth = planeDistance / dot(waterUBO.waterPlane, vec4(0.0, 0.0, 1.0, 0.0));
- depth = abs(depth);
- depth -= v_viewPosition.z;
- }
-
- float envelope = clamp((depth + v_viewPosition.z), 0.0, 1.0);
- envelope = 1.0 - (1.0 - envelope) * (1.0 - envelope);
-
- // water color
- // TODO: correct integral
- vec2 waterCoord = v_worldPosition.xy;
- vec2 integralCoord = floor(waterCoord) + 0.5;
- vec2 blurDir = (worldPositionFromOrigin.xy);
- blurDir /= max(length(blurDir), 1.0);
- vec2 blurDirSign = mix(vec2(-1.0), vec2(1.0), step(0.0, blurDir));
- vec2 startPos = (waterCoord - integralCoord) * blurDirSign;
- vec2 diffPos = blurDir * envelope * blurDirSign * 0.5 /*limit blur*/;
- vec2 subCoord = 1.0 - clamp((vec2(0.5) - startPos) / diffPos, 0.0, 1.0);
- vec2 sampCoord = integralCoord + subCoord * blurDirSign;
- vec3 waterColor = texture(mainTexture, sampCoord / 512.0).xyz;
-
- // underwater object color
- fragColor = texture(screenTexture, scrPos);
-#if !LINEAR_FRAMEBUFFER
- fragColor.xyz *= fragColor.xyz; // screen color to linear
-#endif
-
- // apply fog color to water color now.
- // note that fog is already applied to underwater object.
- waterColor = mix(waterColor, waterUBO.fogColor.xyz, v_fogDensity);
-
- // blend water color with the underwater object's color.
- fragColor.xyz = mix(fragColor.xyz, waterColor, envelope);
-
- // attenuation factor for addition blendings below
- vec3 att = 1.0 - v_fogDensity;
-
- // reflectivity
- vec3 sunlight = EvaluateSunLight();
- vec3 ongoing = normalize(worldPositionFromOrigin);
- float reflective = dot(wave, ongoing);
- reflective = clamp(1.0 - reflective, 0.0, 1.0);
- reflective *= reflective;
- reflective *= reflective;
- reflective += 0.03;
-
- // fresnel refrection to sky
- fragColor.xyz = mix(fragColor.xyz,
- mix(waterUBO.skyColor.xyz * reflective * 0.6,
- waterUBO.fogColor.xyz, v_fogDensity), reflective);
-
- // specular reflection
- if (dot(sunlight, vec3(1.0)) > 0.0001) {
- vec3 refl = reflect(ongoing, wave);
- float spec = max(dot(refl, sunDirection), 0.0);
- spec *= spec; // ^2
- spec *= spec; // ^4
- spec *= spec; // ^16
- spec *= spec; // ^32
- spec *= spec; // ^64
- spec *= spec; // ^128
- spec *= spec; // ^256
- spec *= spec; // ^512
- spec *= spec; // ^1024
- spec *= reflective;
- fragColor.xyz += sunlight * spec * 1000.0 * att;
- }
-
-#if !LINEAR_FRAMEBUFFER
- fragColor.xyz = sqrt(fragColor.xyz);
-#endif
-
- fragColor.w = envelope;
-}
diff --git a/Resources/Shaders/Vulkan/Water.vert b/Resources/Shaders/Vulkan/Water.vert
deleted file mode 100644
index 66a9aa6b0..000000000
--- a/Resources/Shaders/Vulkan/Water.vert
+++ /dev/null
@@ -1,58 +0,0 @@
-#version 450
-
-/*
- Copyright (c) 2013 Fran6nd
-
- This file is part of ZeroSpades, a fork of OpenSpades.
-
- OpenSpades is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- OpenSpades is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with OpenSpades. If not, see .
-
- */
-
-layout(std140, binding = 5) uniform WaterMatricesUBO {
- mat4 projectionViewModelMatrix;
- mat4 modelMatrix;
- mat4 viewModelMatrix;
- vec4 viewOriginVector; // use .xyz
- float fogDistance;
- vec3 _pad0;
-} waterMat;
-
-// [x, y]
-layout(location = 0) in vec2 positionAttribute;
-
-layout(location = 0) out vec3 v_fogDensity;
-layout(location = 1) out vec3 v_screenPosition;
-layout(location = 2) out vec3 v_viewPosition;
-layout(location = 3) out vec3 v_worldPosition;
-
-// Fog computation from OpenGL Fog.vs
-vec4 ComputeFogDensity(float poweredLength) {
- return vec4(min(poweredLength / (waterMat.fogDistance * waterMat.fogDistance), 1.0));
-}
-
-void main() {
- vec4 vertexPos = vec4(positionAttribute.xy, 0.0, 1.0);
-
- v_worldPosition = (waterMat.modelMatrix * vertexPos).xyz;
- v_viewPosition = (waterMat.viewModelMatrix * vertexPos).xyz;
-
- gl_Position = waterMat.projectionViewModelMatrix * vertexPos;
- v_screenPosition = gl_Position.xyw;
- v_screenPosition.xy = (v_screenPosition.xy + v_screenPosition.z) * 0.5;
-
- vec2 horzRelativePos = v_worldPosition.xy - waterMat.viewOriginVector.xy;
- float horzDistance = dot(horzRelativePos, horzRelativePos);
- v_fogDensity = ComputeFogDensity(horzDistance).xyz;
-}
diff --git a/Sources/Draw/Vulkan/TODO.md b/Sources/Draw/Vulkan/TODO.md
index 31ba4c298..ed25844cc 100644
--- a/Sources/Draw/Vulkan/TODO.md
+++ b/Sources/Draw/Vulkan/TODO.md
@@ -13,24 +13,24 @@ capability). Remaining:
## Post-processing filters
Wired into [VulkanRenderer.cpp](VulkanRenderer.cpp) pp-chain:
-Fog → DoF → Bloom → FXAA → LensFlare → AutoExposure → ColorCorrection
+Fog → DoF → CameraBlur → Bloom → FXAA → LensFlare → AutoExposure → ColorCorrection
→ CavityOutline.
| GL filter | Vulkan equivalent | Status |
|---|---|---|
| `GLAutoExposureFilter` | `VulkanAutoExposureFilter` | wired (`r_hdr`) |
| `GLBloomFilter` | — | dead code in GL (instantiated nowhere) |
-| `GLLensDustFilter` (the real `r_bloom`) | `VulkanBloomFilter` | wired but simplified — no dust texture / noise overlay |
-| `GLCameraBlurFilter` | — | **missing** (`r_cameraBlur`) |
+| `GLLensDustFilter` (the real `r_bloom`) | `VulkanBloomFilter` | wired incl. dust texture + per-frame noise grain + Gauss1D H+V blur on every downsample level (`Gauss1DRGBA.vk.fs`), matching GL |
+| `GLCameraBlurFilter` | `VulkanCameraBlurFilter` | wired (`r_cameraBlur` + `sceneDef.radialBlur`), between DoF and Bloom like GL |
| `GLColorCorrectionFilter` | `VulkanColorCorrectionFilter` | wired (`r_colorCorrection`) |
| `GLDepthOfFieldFilter` | `VulkanDepthOfFieldFilter` | wired (`r_depthOfField`) |
| `GLFXAAFilter` | `VulkanFXAAFilter` | wired (`r_fxaa`) |
| `GLFogFilter` / `GLFogFilter2` | `VulkanFogFilter` | wired (`r_fogShadow`) — see follow-ups below |
| `GLLensFilter` | — | dead code in GL (unused) |
-| `GLLensFlareFilter` | `VulkanLensFlareFilter` | wired sun path (`r_lensFlare`); **`r_lensFlareDynamic` per-light flares missing** |
+| `GLLensFlareFilter` | `VulkanLensFlareFilter` | wired sun path (`r_lensFlare`) + per-light flares (`r_lensFlareDynamic`, capped at 8 per frame for descriptor budget) |
| `GLNonlinearizeFilter` | — | not needed — sRGB swapchain blit encodes for display |
-| `GLResampleBicubicFilter` | — | **missing** (`r_scaleFilter == 2`) |
-| `GLSSAOFilter` | — | **missing** (`r_ssao`) |
+| `GLResampleBicubicFilter` | `VulkanResampleBicubicFilter` | wired (`r_scaleFilter == 2`); `r_scaleFilter == 0` now also honored via nearest blit |
+| `GLSSAOFilter` | — | **missing** (`r_ssao`) — big: needs full map+model depth prepass, mid-frame depth resolve/pass split, and an SSAO sampler binding (= new set layout) in every lit map/model pipeline + shader |
| `GLTemporalAAFilter` | — | **missing** (see AA gap above) |
| (n/a — cavity is Vulkan-only) | `VulkanCavityOutlineFilter` | wired (`r_outlines`) |
@@ -40,11 +40,13 @@ Ground model shadows (player / grenade / other-players' weapons) are done:
models render into a models-only cascaded shadow map and the map lit shader
samples it (`BasicMap.frag` `EvaluteModelShadow()`). Remaining polish:
-- [ ] **Model self-shadowing** — wire the same cascade sampling into the
- shared `BasicModelVertexColor.vert/frag`. Note that vert/frag is used by
- the sunlight, prerender and both ghost pipelines, so all of them must
- bind the sampling set (set 1) or break. Low value: models already receive
- terrain shadows via `mapShadowTexture`; this only adds model-on-model.
+- [x] **Model self-shadowing** — `BasicModelVertexColor.vert/frag` now
+ declare the set-1 cascade sampling (same layout as `BasicMap`), the
+ shared model pipeline layout carries both sets, and the prerender +
+ sunlight passes bind the sampling set (covering the ghost pipelines,
+ which draw from the same passes; their frag variants simply don't
+ declare set 1, which Vulkan permits against the wider layout). The
+ UBO `enabled` flag gates sampling when no cascade was rendered.
- [ ] **Phys lit variants** — `BasicMapPhys`, `BasicModelVertexColorPhys`
(only active under `r_physicalLighting`).
@@ -58,20 +60,27 @@ push field) all read it, so changing that one method moves the sun + its shadows
+ ground/model lighting together. Still hardcoded `(0,-1,-1)`:
The **Phys** map lambert (`BasicMapPhys.vert/frag` via
-`MapSolidPushConstants.sunDirection`) now reads it too. Still hardcoded `(0,-1,-1)`:
+`MapSolidPushConstants.sunDirection`) now reads it too.
-- [ ] **Water** (`Water.frag`) and **Fog2** (`Fog2.vk.fs`) still hardcode the
- sun; point them at the same source.
+- [x] **Water + Fog2** — already wired: `Water/Water2/Water3.vk.fs` read
+ `WaterPushConstants.sunDirection` and `Fog2.vk.fs` reads the sun packed
+ into the scale `.w` slots, both fed from `GetSunDirection()`. The old
+ TODO pointed at `Water.frag`, which is a dead file (only `*.vk.fs` are
+ loaded via `Water*.vk.program`).
+- [x] Delete dead `Water.frag` / `Water.vert` — removed.
## Stubs
-- [ ] [VulkanMapRenderer.cpp:126](VulkanMapRenderer.cpp#L126)
- `PreloadShaders` is empty — first frame stutters as map pipelines build.
-- [ ] [VulkanOptimizedVoxelModel.cpp:46](VulkanOptimizedVoxelModel.cpp#L46)
- `PreloadShaders` is empty — same story for model pipelines.
-- [ ] [VulkanWaterRenderer.cpp:1067](VulkanWaterRenderer.cpp#L1067)
- `RenderDynamicLightPass` reuses the sunlight pipeline as a
- placeholder — water doesn't react to dynamic lights.
+- [x] Map/model `PreloadShaders` — no longer empty; both warm the SPIR-V
+ cache. TODO was stale.
+- [ ] `PreloadShaders` only preloads SPIR-V blobs, not `VkPipeline`s —
+ pipelines still compile lazily on first draw. If first-frame stutter
+ persists, pre-create the pipelines (needs render pass compat) or use
+ `VK_EXT_graphics_pipeline_library` / warm pipeline cache from disk.
+- [x] `VulkanWaterRenderer::RenderDynamicLightPass` — already an
+ intentional no-op in the code; GL water has no dynamic light pass
+ either, so "no reaction to dynamic lights" *is* parity. TODO was
+ stale (claimed it re-drew with the sunlight pipeline).
## Fog / sky parity follow-ups
@@ -80,32 +89,38 @@ remaining deltas vs GL.
### `BasicMap.frag` (non-physical lighting)
-- [ ] Missing terminal gamma encoding. Harmless under
- `A2B10G10R10_UNORM` (`r_highPrec=1`); if the offscreen format
- ever falls back to `R8G8B8A8_UNORM` the linear values would
- display ~2× too bright. Either branch on FB format or always
- render to a linear-precision FB.
+- [x] Missing terminal gamma encoding — sidestepped: the framebuffer
+ manager now prefers `A2B10G10R10_UNORM` whenever the hardware
+ supports it (attachment-blend + sampled), even with `r_highPrec=0`.
+ `R8G8B8A8_UNORM` remains only as a last-resort fallback on hardware
+ without 10-bit render targets.
### Other
- [ ] **Fog2 in-scatter dimmer than GL.** The flat `Sky.frag` fog-colour
fill is still drawn under Fog2 as a workaround. Drop once Fog2's
- push-constant scales / integration curve match GL.
-- [ ] **Fog filter view ray glitches looking straight down.** Likely
- degenerate `dir.xy` from the
- [Fog.vk.fs](../../../Resources/Shaders/Vulkan/PostFilters/Fog.vk.fs)
- / [Fog.vk.vs](../../../Resources/Shaders/Vulkan/PostFilters/Fog.vk.vs)
- `length(dir.xy) < 0.0001` guard.
-- [ ] **`VulkanMapShadowRenderer::Update` re-uploads the full 512×512
- bitmap on any change.** GL does a sub-rect upload. Perf cliff in
- build-heavy games.
-
-## Outline tuning (future work)
-
-The cavity threshold and edge strength in
-[VulkanCavityOutlineFilter.cpp](VulkanCavityOutlineFilter.cpp) are
-constants — promote to `r_outlinesDepthThreshold` /
-`r_outlinesStrength` once defaults are confirmed across maps.
+ push-constant scales / integration curve match GL. Possibly caused
+ by the same depth-read bug fixed for the lens flare scanner (see
+ `VulkanFramebufferManager::sceneDepthSampleImage`): Fog2 samples
+ the same depth texture, and a D32 depth image read through
+ `sampler2D` silently returns 0 on MoltenVK — worth re-checking
+ after that fix before spending more time here.
+- [x] **Fog filter view ray glitches looking straight down** — fixed:
+ degenerate near-vertical rays now early-out (fog integral is ~0
+ there anyway) instead of snapping `dir.xy`, which made adjacent
+ fragments flip between real and snapped directions.
+- [x] **`VulkanMapShadowRenderer::Update` sub-rect upload** — dirty 32-texel
+ words coalesce into per-row spans, packed into staging and copied via
+ one multi-region `vkCmdCopyBufferToImage`. Falls back to full upload
+ when >25% dirty or >256 spans.
+
+## Outline tuning
+
+Done: `r_outlinesDepthThreshold` (default 0.05, clamped 0.001–1) and
+`r_outlinesStrength` (default 1, clamped 0–4) are cvars defined in
+[VulkanCavityOutlineFilter.cpp](VulkanCavityOutlineFilter.cpp) and read
+every frame. Remaining: expose them in the setup-menu preferences UI
+once defaults are confirmed across maps.
## Performance / optimization
@@ -141,5 +156,6 @@ constants — promote to `r_outlinesDepthThreshold` /
## Build hygiene
-- [ ] **Committed `.spv` files drift from the GLSL.** CMake regenerates
- them on every build, so the checked-in copies become misleading.
+- [x] **Committed `.spv` files drift from the GLSL** — deleted from the
+ tree and gitignored; `glslangValidator` is a hard build requirement
+ so CMake always regenerates them.
diff --git a/Sources/Draw/Vulkan/VulkanBloomFilter.cpp b/Sources/Draw/Vulkan/VulkanBloomFilter.cpp
index 392444710..fc3aab333 100644
--- a/Sources/Draw/Vulkan/VulkanBloomFilter.cpp
+++ b/Sources/Draw/Vulkan/VulkanBloomFilter.cpp
@@ -19,6 +19,8 @@
*/
#include "VulkanBloomFilter.h"
+#include "VulkanBuffer.h"
+#include "VulkanImageWrapper.h"
#include "VulkanFramebufferManager.h"
#include "VulkanImage.h"
#include "VulkanRenderer.h"
@@ -27,6 +29,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -45,10 +48,13 @@ namespace spades {
ppRenderPass(VK_NULL_HANDLE),
singleSamplerDSL(VK_NULL_HANDLE),
dualSamplerDSL(VK_NULL_HANDLE),
+ quadSamplerDSL(VK_NULL_HANDLE),
downsampleLayout(VK_NULL_HANDLE),
+ gaussLayout(VK_NULL_HANDLE),
upsampleLayout(VK_NULL_HANDLE),
compositeLayout(VK_NULL_HANDLE),
downsamplePipeline(VK_NULL_HANDLE),
+ gaussPipeline(VK_NULL_HANDLE),
upsamplePipeline(VK_NULL_HANDLE),
compositePipeline(VK_NULL_HANDLE) {
SPADES_MARK_FUNCTION();
@@ -62,6 +68,20 @@ namespace spades {
InitDescriptorSetLayouts();
InitPipelines();
InitDescriptorPools();
+
+ // LensDust composite inputs (see GLLensDustFilter).
+ dustImage = r.RegisterImage("Textures/LensDustTexture.jpg");
+
+ noiseImage = Handle::New(
+ device, 128u, 128u, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL,
+ VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
+ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
+ noiseImage->CreateSampler(VK_FILTER_NEAREST, VK_FILTER_NEAREST,
+ VK_SAMPLER_ADDRESS_MODE_REPEAT);
+ noiseStaging = Handle::New(
+ device, (VkDeviceSize)(128 * 128 * 4), VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
+ noiseData.resize(128 * 128);
}
VulkanBloomFilter::~VulkanBloomFilter() {
@@ -78,12 +98,15 @@ namespace spades {
if (compositePipeline != VK_NULL_HANDLE) vkDestroyPipeline(dev, compositePipeline, nullptr);
if (upsamplePipeline != VK_NULL_HANDLE) vkDestroyPipeline(dev, upsamplePipeline, nullptr);
+ if (gaussPipeline != VK_NULL_HANDLE) vkDestroyPipeline(dev, gaussPipeline, nullptr);
if (downsamplePipeline != VK_NULL_HANDLE) vkDestroyPipeline(dev, downsamplePipeline, nullptr);
if (compositeLayout != VK_NULL_HANDLE) vkDestroyPipelineLayout(dev, compositeLayout, nullptr);
if (upsampleLayout != VK_NULL_HANDLE) vkDestroyPipelineLayout(dev, upsampleLayout, nullptr);
+ if (gaussLayout != VK_NULL_HANDLE) vkDestroyPipelineLayout(dev, gaussLayout, nullptr);
if (downsampleLayout != VK_NULL_HANDLE) vkDestroyPipelineLayout(dev, downsampleLayout, nullptr);
+ if (quadSamplerDSL != VK_NULL_HANDLE) vkDestroyDescriptorSetLayout(dev, quadSamplerDSL, nullptr);
if (dualSamplerDSL != VK_NULL_HANDLE) vkDestroyDescriptorSetLayout(dev, dualSamplerDSL, nullptr);
if (singleSamplerDSL != VK_NULL_HANDLE) vkDestroyDescriptorSetLayout(dev, singleSamplerDSL, nullptr);
@@ -135,7 +158,7 @@ namespace spades {
VkDevice dev = device->GetDevice();
auto MakeDSL = [&](uint32_t bindingCount) {
- VkDescriptorSetLayoutBinding bindings[2]{};
+ VkDescriptorSetLayoutBinding bindings[4]{};
for (uint32_t i = 0; i < bindingCount; ++i) {
bindings[i].binding = i;
bindings[i].descriptorCount = 1;
@@ -154,6 +177,7 @@ namespace spades {
singleSamplerDSL = MakeDSL(1);
dualSamplerDSL = MakeDSL(2);
+ quadSamplerDSL = MakeDSL(4);
}
VkShaderModule VulkanBloomFilter::LoadSPIRV(const char* path) {
@@ -178,6 +202,7 @@ namespace spades {
VkShaderModule vs = LoadSPIRV("Shaders/Vulkan/PostFilters/PassThrough.vk.vs.spv");
VkShaderModule passthroughFS = LoadSPIRV("Shaders/Vulkan/PostFilters/PassThrough.vk.fs.spv");
+ VkShaderModule gaussFS = LoadSPIRV("Shaders/Vulkan/PostFilters/Gauss1DRGBA.vk.fs.spv");
VkShaderModule upsampleFS = LoadSPIRV("Shaders/Vulkan/PostFilters/BloomUpsample.vk.fs.spv");
VkShaderModule compositeFS = LoadSPIRV("Shaders/Vulkan/PostFilters/BloomComposite.vk.fs.spv");
@@ -227,6 +252,17 @@ namespace spades {
if (vkCreatePipelineLayout(dev, &li, nullptr, &downsampleLayout) != VK_SUCCESS)
SPRaise("Failed to create downsample pipeline layout");
}
+ {
+ VkPushConstantRange pcr{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(float) * 2};
+ VkPipelineLayoutCreateInfo li{};
+ li.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
+ li.setLayoutCount = 1;
+ li.pSetLayouts = &singleSamplerDSL;
+ li.pushConstantRangeCount = 1;
+ li.pPushConstantRanges = &pcr;
+ if (vkCreatePipelineLayout(dev, &li, nullptr, &gaussLayout) != VK_SUCCESS)
+ SPRaise("Failed to create gauss pipeline layout");
+ }
{
VkPushConstantRange pcr{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(float)};
VkPipelineLayoutCreateInfo li{};
@@ -239,10 +275,13 @@ namespace spades {
SPRaise("Failed to create upsample pipeline layout");
}
{
+ VkPushConstantRange pcr{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(float) * 4};
VkPipelineLayoutCreateInfo li{};
- li.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
- li.setLayoutCount = 1;
- li.pSetLayouts = &dualSamplerDSL;
+ li.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
+ li.setLayoutCount = 1;
+ li.pSetLayouts = &quadSamplerDSL;
+ li.pushConstantRangeCount = 1;
+ li.pPushConstantRanges = &pcr;
if (vkCreatePipelineLayout(dev, &li, nullptr, &compositeLayout) != VK_SUCCESS)
SPRaise("Failed to create composite pipeline layout");
}
@@ -283,11 +322,13 @@ namespace spades {
};
downsamplePipeline = MakePipeline(passthroughFS, downsampleLayout);
+ gaussPipeline = MakePipeline(gaussFS, gaussLayout);
upsamplePipeline = MakePipeline(upsampleFS, upsampleLayout);
compositePipeline = MakePipeline(compositeFS, compositeLayout);
vkDestroyShaderModule(dev, vs, nullptr);
vkDestroyShaderModule(dev, passthroughFS,nullptr);
+ vkDestroyShaderModule(dev, gaussFS, nullptr);
vkDestroyShaderModule(dev, upsampleFS, nullptr);
vkDestroyShaderModule(dev, compositeFS, nullptr);
}
@@ -387,6 +428,49 @@ namespace spades {
return set;
}
+ VkDescriptorSet VulkanBloomFilter::BindTextures4(int frameSlot,
+ const VkImageView* views,
+ const VkSampler* samplers) {
+ VkDescriptorSetAllocateInfo ai{};
+ ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
+ ai.descriptorPool = perFrameDescPool[frameSlot];
+ ai.descriptorSetCount = 1;
+ ai.pSetLayouts = &quadSamplerDSL;
+ VkDescriptorSet set;
+ if (vkAllocateDescriptorSets(device->GetDevice(), &ai, &set) != VK_SUCCESS)
+ SPRaise("Failed to allocate bloom descriptor set");
+
+ VkDescriptorImageInfo imgs[4];
+ VkWriteDescriptorSet writes[4]{};
+ for (int i = 0; i < 4; ++i) {
+ imgs[i] = {samplers[i] != VK_NULL_HANDLE ? samplers[i] : linearSampler,
+ views[i], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
+ writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+ writes[i].dstSet = set;
+ writes[i].dstBinding = static_cast(i);
+ writes[i].descriptorCount = 1;
+ writes[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+ writes[i].pImageInfo = &imgs[i];
+ }
+ vkUpdateDescriptorSets(device->GetDevice(), 4, writes, 0, nullptr);
+ return set;
+ }
+
+ void VulkanBloomFilter::UpdateNoise(VkCommandBuffer cmd) {
+ // Fresh 128x128 random grain every frame, like GL's UpdateNoise.
+ for (size_t i = 0; i < noiseData.size(); i++)
+ noiseData[i] = static_cast(SampleRandom());
+ noiseStaging->UpdateData(noiseData.data(), noiseData.size() * sizeof(uint32_t));
+
+ noiseImage->TransitionLayout(cmd, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+ VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
+ VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+ noiseImage->CopyFromBuffer(cmd, noiseStaging->GetBuffer());
+ noiseImage->TransitionLayout(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+ VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
+ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+ }
+
void VulkanBloomFilter::DrawFullscreen(VkCommandBuffer cmd,
VkRenderPass rp,
VkFramebuffer fb,
@@ -440,6 +524,8 @@ namespace spades {
vkResetDescriptorPool(dev, perFrameDescPool[frameSlot], 0);
}
+ UpdateNoise(cmd);
+
VulkanTemporaryImagePool* pool = renderer.GetTemporaryImagePool();
// ── 1. Downsample 6 levels ────────────────────────────────
@@ -453,6 +539,10 @@ namespace spades {
uint32_t prevH = static_cast(input->GetHeight());
VkImageView srcView = input->GetImageView();
+ // Superseded gauss ping images; returned after recording (see below).
+ std::vector> gaussTrash;
+ gaussTrash.reserve(NUM_LEVELS);
+
for (int i = 0; i < NUM_LEVELS; ++i) {
uint32_t nw = (prevW + 1) / 2;
uint32_t nh = (prevH + 1) / 2;
@@ -464,6 +554,27 @@ namespace spades {
DrawFullscreen(cmd, ppRenderPass, fb, nw, nh,
downsamplePipeline, downsampleLayout, ds);
+ // GL parity: GLLensDustFilter runs a Gauss1D H+V blur on
+ // every downsample level; without it the bloom is noticeably
+ // harder-edged than GL.
+ for (int pass = 0; pass < 2; ++pass) {
+ float unitShift[2] = {pass == 0 ? 1.0f / (float)nw : 0.0f,
+ pass == 0 ? 0.0f : 1.0f / (float)nh};
+ vkCmdPushConstants(cmd, gaussLayout, VK_SHADER_STAGE_FRAGMENT_BIT,
+ 0, sizeof(unitShift), unitShift);
+
+ Handle blurred = pool->Acquire(nw, nh, colorFormat);
+ VkFramebuffer bfb = MakeFramebuffer(ppRenderPass,
+ blurred.GetPointerOrNull(), frameSlot);
+ VkDescriptorSet bds = BindTexture(frameSlot, singleSamplerDSL,
+ dst->GetImageView());
+ DrawFullscreen(cmd, ppRenderPass, bfb, nw, nh,
+ gaussPipeline, gaussLayout, bds);
+
+ gaussTrash.push_back(std::move(dst));
+ dst = std::move(blurred);
+ }
+
srcView = dst->GetImageView();
levels.push_back(std::move(dst));
prevW = nw; prevH = nh;
@@ -512,15 +623,37 @@ namespace spades {
uint32_t rw = static_cast(output->GetWidth());
uint32_t rh = static_cast(output->GetHeight());
+ VulkanImage* dust = nullptr;
+ if (auto* wrapper = dynamic_cast(dustImage.GetPointerOrNull()))
+ dust = wrapper->GetVulkanImage();
+
+ // GL falls back to plain input if anything is missing; here the
+ // dust texture ships with the game, so treat absence as fatal-ish
+ // and just skip the dust term by binding the bloom texture twice.
+ VkImageView views[4] = {
+ input->GetImageView(), levels[0]->GetImageView(),
+ dust ? dust->GetImageView() : levels[0]->GetImageView(),
+ noiseImage->GetImageView()};
+ VkSampler samplers[4] = {linearSampler, linearSampler,
+ dust ? dust->GetSampler() : linearSampler,
+ noiseImage->GetSampler()};
+
VkFramebuffer fb = MakeFramebuffer(ppRenderPass, output, frameSlot);
- VkDescriptorSet ds = BindTextures(frameSlot, dualSamplerDSL,
- input->GetImageView(),
- levels[0]->GetImageView());
+ VkDescriptorSet ds = BindTextures4(frameSlot, views, samplers);
+
+ // noiseTexCoordFactor, exactly as GLLensDustFilter computes it.
+ float facX = renderer.GetRenderWidth() / 128.0f;
+ float facY = renderer.GetRenderHeight() / 128.0f;
+ float noiseFactor[4] = {facX, facY, facX / 128.0f, facY / 128.0f};
+ vkCmdPushConstants(cmd, compositeLayout, VK_SHADER_STAGE_FRAGMENT_BIT,
+ 0, sizeof(noiseFactor), noiseFactor);
DrawFullscreen(cmd, ppRenderPass, fb, rw, rh,
compositePipeline, compositeLayout, ds);
// Return all temporary images (deferred to avoid pool reuse hazards).
+ for (auto& img : gaussTrash)
+ pool->Return(img.GetPointerOrNull());
for (auto& img : toReturn)
pool->Return(img.GetPointerOrNull());
for (auto& img : levels)
diff --git a/Sources/Draw/Vulkan/VulkanBloomFilter.h b/Sources/Draw/Vulkan/VulkanBloomFilter.h
index 4c7e9e275..f7f1d5544 100644
--- a/Sources/Draw/Vulkan/VulkanBloomFilter.h
+++ b/Sources/Draw/Vulkan/VulkanBloomFilter.h
@@ -22,18 +22,23 @@
#include
#include
+#include
#include "VulkanPostProcessFilter.h"
namespace spades {
namespace draw {
+ class VulkanBuffer;
- // Bloom filter.
+ // Bloom filter (r_bloom; mirrors GLLensDustFilter, the real GL
+ // r_bloom — GLBloomFilter is dead code there).
//
- // Algorithm (mirrors GLBloomFilter):
+ // Algorithm:
// 1. Downsample 6 levels via bilinear passthrough.
// 2. Composite levels back from smallest to second-largest using
// mix(large, small, alpha) where alpha = sqrt(cnt/(cnt+1)).
- // 3. Final composite: scene * 0.8 + bloom * 0.2 → output.
+ // 3. LensDust composite: scene * 0.95 + dust² * bloom * 2 + grain,
+ // with the dust overlay texture and a per-frame 128×128 noise
+ // texture for film grain, matching GL's LensDust.fs.
//
// Call Filter(cmd, input, output). input must be in
// SHADER_READ_ONLY_OPTIMAL; output ends up in SHADER_READ_ONLY_OPTIMAL.
@@ -49,12 +54,15 @@ namespace spades {
VkDescriptorSetLayout singleSamplerDSL;
VkDescriptorSetLayout dualSamplerDSL;
+ VkDescriptorSetLayout quadSamplerDSL;
VkPipelineLayout downsampleLayout; // singleSamplerDSL, no push constants
+ VkPipelineLayout gaussLayout; // singleSamplerDSL + push constant vec2 unitShift
VkPipelineLayout upsampleLayout; // dualSamplerDSL + push constant float alpha
- VkPipelineLayout compositeLayout; // dualSamplerDSL, no push constants
+ VkPipelineLayout compositeLayout; // quadSamplerDSL + push constant vec4 noise factor
VkPipeline downsamplePipeline;
+ VkPipeline gaussPipeline;
VkPipeline upsamplePipeline;
VkPipeline compositePipeline;
@@ -62,6 +70,14 @@ namespace spades {
VkDescriptorPool perFrameDescPool[MAX_FRAME_SLOTS];
std::vector perFrameFramebuffers[MAX_FRAME_SLOTS];
+ // LensDust composite inputs
+ Handle dustImage; // Textures/LensDustTexture.jpg
+ Handle noiseImage; // 128×128 RGBA8, refreshed per frame
+ Handle noiseStaging;
+ std::vector noiseData;
+
+ void UpdateNoise(VkCommandBuffer cmd);
+
void InitRenderPass();
void InitDescriptorSetLayouts();
void InitPipelines();
@@ -75,6 +91,8 @@ namespace spades {
VkImageView view);
VkDescriptorSet BindTextures(int frameSlot, VkDescriptorSetLayout dsl,
VkImageView view0, VkImageView view1);
+ VkDescriptorSet BindTextures4(int frameSlot, const VkImageView* views,
+ const VkSampler* samplers);
void DrawFullscreen(VkCommandBuffer cmd, VkRenderPass rp, VkFramebuffer fb,
uint32_t width, uint32_t height,
diff --git a/Sources/Draw/Vulkan/VulkanCameraBlurFilter.cpp b/Sources/Draw/Vulkan/VulkanCameraBlurFilter.cpp
new file mode 100644
index 000000000..3ace3cff7
--- /dev/null
+++ b/Sources/Draw/Vulkan/VulkanCameraBlurFilter.cpp
@@ -0,0 +1,456 @@
+/*
+ Copyright (c) 2013 Fran6nd
+
+ This file is part of ZeroSpades, a fork of OpenSpades.
+
+ OpenSpades is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OpenSpades is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OpenSpades. If not, see .
+
+ */
+
+#include
+#include
+#include
+
+#include "VulkanCameraBlurFilter.h"
+#include "VulkanFramebufferManager.h"
+#include "VulkanImage.h"
+#include "VulkanRenderer.h"
+#include "VulkanRenderPassUtils.h"
+#include "VulkanTemporaryImagePool.h"
+#include
+#include
+#include
+#include
+
+namespace spades {
+ namespace draw {
+
+ namespace {
+ struct CameraBlurPC {
+ float reverseMatrix[16];
+ float shutterTimeScale;
+ };
+
+ // Rotation-only "reverse" of the view difference matrix; straight
+ // port of GLCameraBlurFilter's ReverseMatrix.
+#define M(r, c) (d.m[(r) + (c)*4])
+ Matrix4 ReverseMatrix(Matrix4 d) {
+ return Matrix4(
+ M(1, 2) * M(2, 1) - M(1, 1) * M(2, 2), M(1, 0) * M(2, 2) - M(1, 2) * M(2, 0),
+ M(1, 1) * M(2, 0) - M(1, 0) * M(2, 1), 0, M(0, 1) * M(2, 2) - M(0, 2) * M(2, 1),
+ M(0, 2) * M(2, 0) - M(0, 0) * M(2, 2), M(0, 0) * M(2, 1) - M(0, 1) * M(2, 0), 0,
+ 0, 0, 0, 0, M(0, 2) * M(1, 1) - M(0, 1) * M(1, 2),
+ M(0, 0) * M(1, 2) - M(0, 2) * M(1, 0), M(0, 1) * M(1, 0) - M(0, 0) * M(1, 1), 1);
+ }
+#undef M
+
+ float MyACos(float v) { return v >= 1.0f ? 0.0f : acosf(v); }
+ } // namespace
+
+ VulkanCameraBlurFilter::VulkanCameraBlurFilter(VulkanRenderer& r)
+ : VulkanPostProcessFilter(r),
+ colorFormat(VK_FORMAT_UNDEFINED),
+ linearSampler(VK_NULL_HANDLE),
+ ppRenderPass(VK_NULL_HANDLE),
+ dualSamplerDSL(VK_NULL_HANDLE),
+ blurLayout(VK_NULL_HANDLE),
+ blurPipeline(VK_NULL_HANDLE) {
+ SPADES_MARK_FUNCTION();
+
+ for (int i = 0; i < MAX_FRAME_SLOTS; ++i)
+ perFrameDescPool[i] = VK_NULL_HANDLE;
+
+ prevMatrix = Matrix4::Identity();
+ colorFormat = r.GetFramebufferManager()->GetMainColorFormat();
+
+ InitRenderPass();
+ InitDescriptorSetLayout();
+ InitPipeline();
+ InitDescriptorPools();
+ }
+
+ VulkanCameraBlurFilter::~VulkanCameraBlurFilter() {
+ SPADES_MARK_FUNCTION();
+
+ VkDevice dev = device->GetDevice();
+
+ for (int i = 0; i < MAX_FRAME_SLOTS; ++i) {
+ for (VkFramebuffer fb : perFrameFramebuffers[i])
+ vkDestroyFramebuffer(dev, fb, nullptr);
+ if (perFrameDescPool[i] != VK_NULL_HANDLE)
+ vkDestroyDescriptorPool(dev, perFrameDescPool[i], nullptr);
+ }
+
+ if (blurPipeline != VK_NULL_HANDLE) vkDestroyPipeline(dev, blurPipeline, nullptr);
+ if (blurLayout != VK_NULL_HANDLE) vkDestroyPipelineLayout(dev, blurLayout, nullptr);
+ if (dualSamplerDSL != VK_NULL_HANDLE)
+ vkDestroyDescriptorSetLayout(dev, dualSamplerDSL, nullptr);
+ if (linearSampler != VK_NULL_HANDLE) vkDestroySampler(dev, linearSampler, nullptr);
+ if (ppRenderPass != VK_NULL_HANDLE) vkDestroyRenderPass(dev, ppRenderPass, nullptr);
+ }
+
+ void VulkanCameraBlurFilter::InitRenderPass() {
+ VkDevice dev = device->GetDevice();
+
+ VkSubpassDependency dep{};
+ dep.srcSubpass = VK_SUBPASS_EXTERNAL;
+ dep.dstSubpass = 0;
+ dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+ dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
+ dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+ dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
+
+ ppRenderPass = CreateSimpleColorRenderPass(
+ dev, colorFormat,
+ VK_ATTACHMENT_LOAD_OP_DONT_CARE,
+ VK_IMAGE_LAYOUT_UNDEFINED,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+ &dep);
+
+ VkSamplerCreateInfo si{};
+ si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
+ si.magFilter = VK_FILTER_LINEAR;
+ si.minFilter = VK_FILTER_LINEAR;
+ si.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ si.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ si.maxAnisotropy = 1.0f;
+ si.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
+ si.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
+
+ if (vkCreateSampler(dev, &si, nullptr, &linearSampler) != VK_SUCCESS)
+ SPRaise("Failed to create camera blur sampler");
+ }
+
+ void VulkanCameraBlurFilter::InitDescriptorSetLayout() {
+ VkDescriptorSetLayoutBinding b[2]{};
+ for (int i = 0; i < 2; ++i) {
+ b[i].binding = (uint32_t)i;
+ b[i].descriptorCount = 1;
+ b[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+ b[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
+ }
+
+ VkDescriptorSetLayoutCreateInfo info{};
+ info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
+ info.bindingCount = 2;
+ info.pBindings = b;
+
+ if (vkCreateDescriptorSetLayout(device->GetDevice(), &info, nullptr,
+ &dualSamplerDSL) != VK_SUCCESS)
+ SPRaise("Failed to create camera blur descriptor set layout");
+ }
+
+ VkShaderModule VulkanCameraBlurFilter::LoadSPIRV(const char* path) {
+ std::string data = FileManager::ReadAllBytes(path);
+ std::vector code(data.size() / sizeof(uint32_t));
+ std::memcpy(code.data(), data.data(), data.size());
+
+ VkShaderModuleCreateInfo info{};
+ info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
+ info.codeSize = data.size();
+ info.pCode = code.data();
+
+ VkShaderModule mod;
+ if (vkCreateShaderModule(device->GetDevice(), &info, nullptr, &mod) != VK_SUCCESS)
+ SPRaise("Failed to create shader module: %s", path);
+ return mod;
+ }
+
+ void VulkanCameraBlurFilter::InitPipeline() {
+ VkDevice dev = device->GetDevice();
+ VkPipelineCache cache = renderer.GetPipelineCache();
+
+ VkShaderModule vs = LoadSPIRV("Shaders/Vulkan/PostFilters/CameraBlur.vk.vs.spv");
+ VkShaderModule fs = LoadSPIRV("Shaders/Vulkan/PostFilters/CameraBlur.vk.fs.spv");
+
+ VkPipelineVertexInputStateCreateInfo vertexInput{};
+ vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
+
+ VkPipelineInputAssemblyStateCreateInfo ia{};
+ ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
+ ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
+
+ VkPipelineViewportStateCreateInfo vp{};
+ vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
+ vp.viewportCount = 1;
+ vp.scissorCount = 1;
+
+ VkPipelineRasterizationStateCreateInfo rs{};
+ rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
+ rs.polygonMode = VK_POLYGON_MODE_FILL;
+ rs.cullMode = VK_CULL_MODE_NONE;
+ rs.lineWidth = 1.0f;
+
+ VkPipelineMultisampleStateCreateInfo ms{};
+ ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
+ ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
+
+ VkPipelineDepthStencilStateCreateInfo ds{};
+ ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
+
+ VkDynamicState dynArr[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
+ VkPipelineDynamicStateCreateInfo dyn{};
+ dyn.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
+ dyn.dynamicStateCount = 2;
+ dyn.pDynamicStates = dynArr;
+
+ VkPipelineColorBlendAttachmentState noBlend{};
+ noBlend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
+ VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
+
+ VkPipelineColorBlendStateCreateInfo blend{};
+ blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
+ blend.attachmentCount = 1;
+ blend.pAttachments = &noBlend;
+
+ // One push-constant block shared by both stages (mat4 + float).
+ VkPushConstantRange pcr{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0,
+ sizeof(CameraBlurPC)};
+ VkPipelineLayoutCreateInfo li{};
+ li.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
+ li.setLayoutCount = 1;
+ li.pSetLayouts = &dualSamplerDSL;
+ li.pushConstantRangeCount = 1;
+ li.pPushConstantRanges = &pcr;
+ if (vkCreatePipelineLayout(dev, &li, nullptr, &blurLayout) != VK_SUCCESS)
+ SPRaise("Failed to create camera blur pipeline layout");
+
+ VkPipelineShaderStageCreateInfo stages[2]{};
+ stages[0] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0,
+ VK_SHADER_STAGE_VERTEX_BIT, vs, "main", nullptr};
+ stages[1] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0,
+ VK_SHADER_STAGE_FRAGMENT_BIT, fs, "main", nullptr};
+
+ VkGraphicsPipelineCreateInfo pi{};
+ pi.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
+ pi.stageCount = 2;
+ pi.pStages = stages;
+ pi.pVertexInputState = &vertexInput;
+ pi.pInputAssemblyState = &ia;
+ pi.pViewportState = &vp;
+ pi.pRasterizationState = &rs;
+ pi.pMultisampleState = &ms;
+ pi.pDepthStencilState = &ds;
+ pi.pColorBlendState = &blend;
+ pi.pDynamicState = &dyn;
+ pi.layout = blurLayout;
+ pi.renderPass = ppRenderPass;
+ pi.subpass = 0;
+
+ if (vkCreateGraphicsPipelines(dev, cache, 1, &pi, nullptr, &blurPipeline) != VK_SUCCESS)
+ SPRaise("Failed to create camera blur pipeline");
+
+ vkDestroyShaderModule(dev, vs, nullptr);
+ vkDestroyShaderModule(dev, fs, nullptr);
+ }
+
+ void VulkanCameraBlurFilter::InitDescriptorPools() {
+ // Up to ~8 blur iterations per frame; 2 samplers per set.
+ VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 32};
+ VkDescriptorPoolCreateInfo info{};
+ info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
+ info.poolSizeCount = 1;
+ info.pPoolSizes = &size;
+ info.maxSets = 16;
+
+ for (int i = 0; i < MAX_FRAME_SLOTS; ++i) {
+ if (vkCreateDescriptorPool(device->GetDevice(), &info, nullptr,
+ &perFrameDescPool[i]) != VK_SUCCESS)
+ SPRaise("Failed to create camera blur descriptor pool");
+ }
+ }
+
+ VkFramebuffer VulkanCameraBlurFilter::MakeFramebuffer(VulkanImage* image, int frameSlot) {
+ VkImageView view = image->GetImageView();
+ VkFramebufferCreateInfo fbInfo{};
+ fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
+ fbInfo.renderPass = ppRenderPass;
+ fbInfo.attachmentCount = 1;
+ fbInfo.pAttachments = &view;
+ fbInfo.width = image->GetWidth();
+ fbInfo.height = image->GetHeight();
+ fbInfo.layers = 1;
+
+ VkFramebuffer fb;
+ if (vkCreateFramebuffer(device->GetDevice(), &fbInfo, nullptr, &fb) != VK_SUCCESS)
+ SPRaise("Failed to create camera blur framebuffer");
+ perFrameFramebuffers[frameSlot].push_back(fb);
+ return fb;
+ }
+
+ VkDescriptorSet VulkanCameraBlurFilter::BindTextures(int frameSlot, VkImageView color,
+ VkImageView depth,
+ VkSampler depthSampler) {
+ VkDescriptorSetAllocateInfo ai{};
+ ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
+ ai.descriptorPool = perFrameDescPool[frameSlot];
+ ai.descriptorSetCount = 1;
+ ai.pSetLayouts = &dualSamplerDSL;
+ VkDescriptorSet set;
+ if (vkAllocateDescriptorSets(device->GetDevice(), &ai, &set) != VK_SUCCESS)
+ SPRaise("Failed to allocate camera blur descriptor set");
+
+ VkDescriptorImageInfo imgs[2] = {
+ {linearSampler, color, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL},
+ {depthSampler, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}};
+ VkWriteDescriptorSet w[2]{};
+ for (int i = 0; i < 2; ++i) {
+ w[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+ w[i].dstSet = set;
+ w[i].dstBinding = (uint32_t)i;
+ w[i].descriptorCount = 1;
+ w[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+ w[i].pImageInfo = &imgs[i];
+ }
+ vkUpdateDescriptorSets(device->GetDevice(), 2, w, 0, nullptr);
+ return set;
+ }
+
+ bool VulkanCameraBlurFilter::Apply(VkCommandBuffer cmd, VulkanImage* input,
+ VulkanImage* output, float intensity,
+ float radialBlur) {
+ SPADES_MARK_FUNCTION();
+
+ // CPU-side setup is a straight port of GLCameraBlurFilter::Filter.
+ if (radialBlur > 0.0f)
+ radialBlur = 1.0f - radialBlur;
+ else
+ radialBlur = 1.0f;
+
+ bool hasRadialBlur = radialBlur < 0.9999f;
+
+ const client::SceneDefinition& def = renderer.GetSceneDef();
+ Matrix4 newMatrix = Matrix4::Identity();
+ Vector3 axes[] = {def.viewAxis[0], def.viewAxis[1], def.viewAxis[2]};
+ axes[0] /= std::tan(def.fovX * 0.5f);
+ axes[1] /= std::tan(def.fovY * 0.5f);
+ newMatrix.m[0] = axes[0].x;
+ newMatrix.m[1] = axes[1].x;
+ newMatrix.m[2] = axes[2].x;
+ newMatrix.m[4] = axes[0].y;
+ newMatrix.m[5] = axes[1].y;
+ newMatrix.m[6] = axes[2].y;
+ newMatrix.m[8] = axes[0].z;
+ newMatrix.m[9] = axes[1].z;
+ newMatrix.m[10] = axes[2].z;
+
+ Matrix4 inverseNewMatrix = newMatrix.Inversed();
+ Matrix4 diffMatrix = prevMatrix * inverseNewMatrix;
+ prevMatrix = newMatrix;
+ Matrix4 reverseMatrix = ReverseMatrix(diffMatrix);
+
+ if (diffMatrix.m[0] < 0.3f || diffMatrix.m[5] < 0.3f || diffMatrix.m[10] < 0.3f) {
+ // camera cut; too much rotation to reproject
+ if (hasRadialBlur)
+ diffMatrix = Matrix4::Identity();
+ else
+ return false;
+ }
+
+ float movePixels = MyACos(diffMatrix.m[0]);
+ float shutterTimeScale = intensity;
+ movePixels = std::max(movePixels, MyACos(diffMatrix.m[5]));
+ movePixels = std::max(movePixels, MyACos(diffMatrix.m[10]));
+ movePixels = tanf(movePixels) / tanf(def.fovX * 0.5f);
+ movePixels *= (float)renderer.GetRenderWidth() * 0.5f;
+ movePixels *= shutterTimeScale;
+
+ movePixels =
+ std::max(movePixels, (1.0f - radialBlur) * renderer.GetRenderWidth() * 0.5f);
+
+ if (movePixels < 1.0f)
+ return false;
+
+ int levels = (int)ceilf(logf(movePixels) / logf(5.0f));
+ if (levels <= 0)
+ levels = 1;
+
+ if (hasRadialBlur)
+ radialBlur *= radialBlur;
+ reverseMatrix = Matrix4::Scale(radialBlur, radialBlur, 1.0f) * reverseMatrix;
+
+ int frameSlot = static_cast(renderer.GetCurrentFrameIndex());
+
+ {
+ VkDevice dev = device->GetDevice();
+ for (VkFramebuffer fb : perFrameFramebuffers[frameSlot])
+ vkDestroyFramebuffer(dev, fb, nullptr);
+ perFrameFramebuffers[frameSlot].clear();
+ vkResetDescriptorPool(dev, perFrameDescPool[frameSlot], 0);
+ }
+
+ Handle depthImg =
+ renderer.GetFramebufferManager()->GetResolvedDepthImage();
+ auto* pool = renderer.GetTemporaryImagePool();
+
+ CameraBlurPC pc{};
+ std::memcpy(pc.reverseMatrix, reverseMatrix.m, sizeof(pc.reverseMatrix));
+
+ uint32_t w = static_cast(output->GetWidth());
+ uint32_t h = static_cast(output->GetHeight());
+
+ // Iterated smear; each pass shrinks the shutter by 5x. Ping-pong
+ // through pool temporaries, final pass lands in `output`.
+ std::vector> keepAlive;
+ VulkanImage* src = input;
+ for (int i = 0; i < levels; i++) {
+ VulkanImage* dst;
+ if (i == levels - 1) {
+ dst = output;
+ } else {
+ Handle tmp = pool->Acquire(w, h, colorFormat);
+ dst = tmp.GetPointerOrNull();
+ keepAlive.push_back(std::move(tmp));
+ }
+
+ VkFramebuffer fb = MakeFramebuffer(dst, frameSlot);
+ VkDescriptorSet ds = BindTextures(frameSlot, src->GetImageView(),
+ depthImg->GetImageView(),
+ depthImg->GetSampler());
+
+ VkRenderPassBeginInfo rpBegin{};
+ rpBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
+ rpBegin.renderPass = ppRenderPass;
+ rpBegin.framebuffer = fb;
+ rpBegin.renderArea.extent = {w, h};
+
+ vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE);
+
+ VkViewport viewport{0.0f, 0.0f, (float)w, (float)h, 0.0f, 1.0f};
+ VkRect2D scissor{{0, 0}, {w, h}};
+ vkCmdSetViewport(cmd, 0, 1, &viewport);
+ vkCmdSetScissor(cmd, 0, 1, &scissor);
+
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, blurPipeline);
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, blurLayout, 0, 1,
+ &ds, 0, nullptr);
+
+ pc.shutterTimeScale = shutterTimeScale;
+ vkCmdPushConstants(cmd, blurLayout,
+ VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0,
+ sizeof(pc), &pc);
+
+ vkCmdDraw(cmd, 3, 1, 0, 0);
+ vkCmdEndRenderPass(cmd);
+
+ shutterTimeScale /= 5.0f;
+ src = dst;
+ }
+
+ return true;
+ }
+
+ } // namespace draw
+} // namespace spades
diff --git a/Sources/Draw/Vulkan/VulkanCameraBlurFilter.h b/Sources/Draw/Vulkan/VulkanCameraBlurFilter.h
new file mode 100644
index 000000000..c27478047
--- /dev/null
+++ b/Sources/Draw/Vulkan/VulkanCameraBlurFilter.h
@@ -0,0 +1,83 @@
+/*
+ Copyright (c) 2013 Fran6nd
+
+ This file is part of ZeroSpades, a fork of OpenSpades.
+
+ OpenSpades is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OpenSpades is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OpenSpades. If not, see .
+
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include "VulkanPostProcessFilter.h"
+
+namespace spades {
+ namespace draw {
+
+ // Camera motion blur (Vulkan port of GLCameraBlurFilter).
+ //
+ // Reprojects each pixel into last frame's view (rotation only) and
+ // smears along the motion vector, iterating log5(movePixels) passes
+ // with decreasing shutter scale. Also implements sceneDef.radialBlur.
+ //
+ // Call Apply(cmd, input, output, intensity, radialBlur); returns
+ // false when the blur was skipped (no motion / camera cut), in which
+ // case `output` was not written and the caller must not swap.
+
+ class VulkanCameraBlurFilter : public VulkanPostProcessFilter {
+
+ VkFormat colorFormat;
+ VkSampler linearSampler;
+
+ VkRenderPass ppRenderPass;
+ VkDescriptorSetLayout dualSamplerDSL;
+ VkPipelineLayout blurLayout;
+ VkPipeline blurPipeline;
+
+ static constexpr int MAX_FRAME_SLOTS = 2;
+ VkDescriptorPool perFrameDescPool[MAX_FRAME_SLOTS];
+ std::vector perFrameFramebuffers[MAX_FRAME_SLOTS];
+
+ Matrix4 prevMatrix;
+
+ void InitRenderPass();
+ void InitDescriptorSetLayout();
+ void InitPipeline();
+ void InitDescriptorPools();
+
+ VkShaderModule LoadSPIRV(const char* path);
+ VkFramebuffer MakeFramebuffer(VulkanImage* image, int frameSlot);
+ VkDescriptorSet BindTextures(int frameSlot, VkImageView color, VkImageView depth,
+ VkSampler depthSampler);
+
+ void CreatePipeline() override {}
+ void CreateRenderPass() override {}
+
+ public:
+ VulkanCameraBlurFilter(VulkanRenderer& renderer);
+ ~VulkanCameraBlurFilter();
+
+ bool Apply(VkCommandBuffer cmd, VulkanImage* input, VulkanImage* output,
+ float intensity, float radialBlur);
+
+ void Filter(VkCommandBuffer cmd, VulkanImage* input, VulkanImage* output) override {
+ Apply(cmd, input, output, 0.2f, 0.0f);
+ }
+ };
+
+ } // namespace draw
+} // namespace spades
diff --git a/Sources/Draw/Vulkan/VulkanCavityOutlineFilter.cpp b/Sources/Draw/Vulkan/VulkanCavityOutlineFilter.cpp
index 035a2f19b..73b621a26 100644
--- a/Sources/Draw/Vulkan/VulkanCavityOutlineFilter.cpp
+++ b/Sources/Draw/Vulkan/VulkanCavityOutlineFilter.cpp
@@ -28,9 +28,14 @@
#include
#include
#include
+#include
#include
+#include
#include
+DEFINE_SPADES_SETTING(r_outlinesStrength, "1");
+DEFINE_SPADES_SETTING(r_outlinesDepthThreshold, "0.05");
+
namespace spades {
namespace draw {
@@ -348,14 +353,16 @@ namespace spades {
pc.zNearFarFogStrength[1] = def.zFar;
pc.zNearFarFogStrength[2] = renderer.GetFogDistance();
// Edge strength: 1.0 = fully black at peak edge.
- pc.zNearFarFogStrength[3] = 1.0F;
+ pc.zNearFarFogStrength[3] =
+ std::max(0.0F, std::min(4.0F, (float)r_outlinesStrength));
// Relative depth threshold. The Laplacian magnitude is normalised
// by the centre tap's linearised depth, so this is "fraction of
// view distance". 0.05 means: edges where the depth jump exceeds
// 5% of how far the centre is from the camera. Tuned for voxel
// scale (1 unit = 1 voxel cube) and typical viewing distances.
- pc.thresholds[0] = 0.05F;
+ pc.thresholds[0] =
+ std::max(0.001F, std::min(1.0F, (float)r_outlinesDepthThreshold));
pc.thresholds[1] = 0.0F;
pc.thresholds[2] = 0.0F;
pc.thresholds[3] = 0.0F;
diff --git a/Sources/Draw/Vulkan/VulkanFramebufferManager.cpp b/Sources/Draw/Vulkan/VulkanFramebufferManager.cpp
index 2cbd54462..7f95c4d41 100644
--- a/Sources/Draw/Vulkan/VulkanFramebufferManager.cpp
+++ b/Sources/Draw/Vulkan/VulkanFramebufferManager.cpp
@@ -67,12 +67,28 @@ namespace spades {
} else if (useHdr) {
SPLog("Using HDR color format");
fbColorFormat = VK_FORMAT_R16G16B16A16_SFLOAT;
- } else if (useHighPrec) {
- SPLog("Using high precision color format");
- fbColorFormat = VK_FORMAT_A2B10G10R10_UNORM_PACK32;
} else {
- SPLog("Using standard RGBA8 color format");
- fbColorFormat = VK_FORMAT_R8G8B8A8_UNORM;
+ // The lit shaders output linear color with no terminal gamma
+ // encode; an 8-bit UNORM scene FB would band badly and (until
+ // the final sRGB blit) sample ~2x too bright in filters that
+ // assume linear precision. Prefer 10-bit linear whenever the
+ // hardware allows, regardless of r_highPrec.
+ VkFormatProperties colorProps;
+ vkGetPhysicalDeviceFormatProperties(device->GetPhysicalDevice(),
+ VK_FORMAT_A2B10G10R10_UNORM_PACK32,
+ &colorProps);
+ const VkFormatFeatureFlags needed =
+ VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT |
+ VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
+ if ((colorProps.optimalTilingFeatures & needed) == needed) {
+ SPLog(useHighPrec ? "Using high precision color format"
+ : "Using high precision color format "
+ "(auto: linear scene FB requires >8bpc)");
+ fbColorFormat = VK_FORMAT_A2B10G10R10_UNORM_PACK32;
+ } else {
+ SPLog("Using standard RGBA8 color format");
+ fbColorFormat = VK_FORMAT_R8G8B8A8_UNORM;
+ }
}
// Prefer D24_UNORM_S8_UINT, fall back to depth-only D32_SFLOAT
@@ -123,12 +139,20 @@ namespace spades {
}
if (!useMSAA) {
+ // Store depth as R32_SFLOAT color image, NOT as D32_SFLOAT depth.
+ // MoltenVK translates sampler2D to Metal's texture2d, but
+ // D32 depth textures require depth2d — reading a D32 image
+ // through sampler2D silently returns 0 on Metal. Using R32F as the
+ // sample target (with a cross-aspect vkCmdCopyImage) lets all
+ // post-processing shaders (Fog2, LensFlare scanner, etc.) read
+ // depth as a plain float texture. The MSAA path already does this
+ // via VulkanDepthResolveFilter → R32F color target.
sceneDepthSampleImage = Handle::New(
- device, renderWidth, renderHeight, fbDepthFormat,
+ device, renderWidth, renderHeight, VK_FORMAT_R32_SFLOAT,
VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
- sceneDepthSampleImage->CreateImageView(VK_IMAGE_ASPECT_DEPTH_BIT);
+ sceneDepthSampleImage->CreateImageView(VK_IMAGE_ASPECT_COLOR_BIT);
sceneDepthSampleImage->CreateSampler(VK_FILTER_NEAREST, VK_FILTER_NEAREST,
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, false);
}
@@ -1035,7 +1059,8 @@ namespace spades {
pre[1].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
pre[1].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
pre[1].image = sceneDepthSampleImage->GetImage();
- pre[1].subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
+ // R32F color target (not D32 depth) — see creation comment above.
+ pre[1].subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
pre[1].srcAccessMask = 0;
pre[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
@@ -1046,7 +1071,9 @@ namespace spades {
VkImageCopy depthCopy{};
depthCopy.srcSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1};
- depthCopy.dstSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1};
+ // Cross-aspect copy: D32_SFLOAT(DEPTH) → R32_SFLOAT(COLOR).
+ // Same 32-bit float bit pattern; valid since Vulkan 1.1 (maintenance1).
+ depthCopy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
depthCopy.extent = {(uint32_t)renderWidth, (uint32_t)renderHeight, 1};
vkCmdCopyImage(commandBuffer,
renderDepthImage->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
diff --git a/Sources/Draw/Vulkan/VulkanLensFlareFilter.cpp b/Sources/Draw/Vulkan/VulkanLensFlareFilter.cpp
index fdee7c7c1..79c8fc3af 100644
--- a/Sources/Draw/Vulkan/VulkanLensFlareFilter.cpp
+++ b/Sources/Draw/Vulkan/VulkanLensFlareFilter.cpp
@@ -32,16 +32,20 @@
#include "VulkanTemporaryImagePool.h"
#include
#include
+#include
#include
#include
#include
#include
+SPADES_SETTING(r_lensFlareDynamic);
+
namespace spades {
namespace draw {
namespace {
constexpr uint32_t kVisibilityDim = 64;
+ constexpr size_t kMaxDynamicFlares = 8; // descriptor pool budget
// Push-constant blocks (must match the GLSL layout).
@@ -73,7 +77,6 @@ namespace spades {
: VulkanPostProcessFilter(r),
colorFormat(VK_FORMAT_UNDEFINED),
linearSampler(VK_NULL_HANDLE),
- depthShadowSampler(VK_NULL_HANDLE),
scannerRenderPass(VK_NULL_HANDLE),
blurRenderPass(VK_NULL_HANDLE),
finalRenderPass(VK_NULL_HANDLE),
@@ -133,7 +136,6 @@ namespace spades {
if (blurRenderPass != VK_NULL_HANDLE) vkDestroyRenderPass(dev, blurRenderPass, nullptr);
if (scannerRenderPass != VK_NULL_HANDLE) vkDestroyRenderPass(dev, scannerRenderPass, nullptr);
- if (depthShadowSampler != VK_NULL_HANDLE) vkDestroySampler(dev, depthShadowSampler, nullptr);
if (linearSampler != VK_NULL_HANDLE) vkDestroySampler(dev, linearSampler, nullptr);
}
@@ -161,15 +163,13 @@ namespace spades {
if (vkCreateSampler(dev, &si, nullptr, &linearSampler) != VK_SUCCESS)
SPRaise("Failed to create lens flare linear sampler");
- // Shadow-compare sampler for sampler2DShadow on the depth texture.
- // Bilinear filtering of the comparison result gives a soft visibility
- // disc, matching the GL `sampler2DShadow` + `CompareRefToTexture` path.
- VkSamplerCreateInfo ss = si;
- ss.compareEnable = VK_TRUE;
- ss.compareOp = VK_COMPARE_OP_LESS;
-
- if (vkCreateSampler(dev, &ss, nullptr, &depthShadowSampler) != VK_SUCCESS)
- SPRaise("Failed to create lens flare depth-shadow sampler");
+ // Depth is sampled as a plain texture (compare happens in the
+ // scanner shader) with the depth image's own nearest sampler.
+ // A sampler2DShadow hardware-compare sampler was used previously,
+ // but hardware depth-compare is invalid on the R32F color image
+ // that CopySceneDepthForSampling / the MSAA depth-resolve pass
+ // produce (see VulkanFramebufferManager), so the compare is done
+ // in-shader instead (LensFlareScanner.vk.fs).
}
void VulkanLensFlareFilter::InitRenderPasses() {
@@ -414,15 +414,14 @@ namespace spades {
void VulkanLensFlareFilter::InitDescriptorPools() {
VkDevice dev = device->GetDevice();
- // Per Filter() call:
- // 1 (scanner) + 6 (blurs) + 1 (passthrough) + up to 15 (flare draws)
- // ≈ 23 sets × ~2.5 image-info → round up to 32 sets / 64 samplers.
- VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 96};
+ // ~23 sets per flare (scanner + 6 blurs + ≤15 quads); sun +
+ // up to kMaxDynamicFlares lights + passthrough.
+ VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 768};
VkDescriptorPoolCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
info.poolSizeCount = 1;
info.pPoolSizes = &size;
- info.maxSets = 32;
+ info.maxSets = 256;
for (int i = 0; i < MAX_FRAME_SLOTS; ++i) {
if (vkCreateDescriptorPool(dev, &info, nullptr, &perFrameDescPool[i]) != VK_SUCCESS)
@@ -465,7 +464,8 @@ namespace spades {
return fb;
}
- VkDescriptorSet VulkanLensFlareFilter::BindShadowDepth(int frameSlot, VkImageView depthView) {
+ VkDescriptorSet VulkanLensFlareFilter::BindShadowDepth(int frameSlot, VkImageView depthView,
+ VkSampler depthSampler) {
VkDescriptorSetAllocateInfo ai{};
ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
ai.descriptorPool = perFrameDescPool[frameSlot];
@@ -475,7 +475,7 @@ namespace spades {
if (vkAllocateDescriptorSets(device->GetDevice(), &ai, &set) != VK_SUCCESS)
SPRaise("Failed to allocate lens flare shadow descriptor set");
- VkDescriptorImageInfo img{depthShadowSampler, depthView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
+ VkDescriptorImageInfo img{depthSampler, depthView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
VkWriteDescriptorSet w{};
w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w.dstSet = set;
@@ -584,7 +584,6 @@ namespace spades {
int frameSlot = static_cast(renderer.GetCurrentFrameIndex());
- // Reclaim per-frame resources from the previous use of this slot.
{
VkDevice dev = device->GetDevice();
for (VkFramebuffer fb : perFrameFramebuffers[frameSlot])
@@ -596,105 +595,104 @@ namespace spades {
const client::SceneDefinition& def = renderer.GetSceneDef();
const float outW = static_cast(output->GetWidth());
const float outH = static_cast(output->GetHeight());
-
- // ── 1. Sun NDC position (GL convention, +Y up) ────────────
- // Mirrors GLLensFlareFilter::Draw()'s hard-coded sun call.
- const Vector3 sunDir = renderer.GetSunDirection();
- const Vector3 sunCol = MakeVector3(1.0F, 0.9F, 0.8F);
- constexpr bool infinityDistance = true;
- constexpr bool renderReflections = true;
-
- Vector3 sunView = {
- Vector3::Dot(sunDir, def.viewAxis[0]),
- Vector3::Dot(sunDir, def.viewAxis[1]),
- Vector3::Dot(sunDir, def.viewAxis[2]),
+ const float fovTanX = std::tan(def.fovX * 0.5F);
+ const float fovTanY = std::tan(def.fovY * 0.5F);
+
+ // One entry per flare: sun first, then per-light flares
+ // (mirrors GLLensFlareFilter::Draw being called once per source).
+ struct FlareRequest {
+ float glX, glY; // GL NDC (+Y up)
+ float texPosX, texPosY;
+ float texSizeX, texSizeY;
+ float scanZ;
+ Vector3 color;
+ bool reflections;
+ Handle visibility;
};
+ std::vector requests;
+
+ auto AddRequest = [&](const Vector3& dir, const Vector3& col,
+ bool infinityDistance, bool reflections) {
+ Vector3 view = {
+ Vector3::Dot(dir, def.viewAxis[0]),
+ Vector3::Dot(dir, def.viewAxis[1]),
+ Vector3::Dot(dir, def.viewAxis[2]),
+ };
+ if (view.z <= 0.0F)
+ return;
- const bool sunVisible = sunView.z > 0.0F;
-
- float sunScreenGLx = 0.0F;
- float sunScreenGLy = 0.0F;
- float sunTexPosX = 0.5F;
- float sunTexPosY = 0.5F;
- float sunTexSizeX = 0.0F;
- float sunTexSizeY = 0.0F;
- float scanZ = 0.9999999F;
-
- if (sunVisible) {
- const float fovTanX = std::tan(def.fovX * 0.5F);
- const float fovTanY = std::tan(def.fovY * 0.5F);
- sunScreenGLx = sunView.x / (sunView.z * fovTanX);
- sunScreenGLy = sunView.y / (sunView.z * fovTanY);
+ FlareRequest rq{};
+ rq.glX = view.x / (view.z * fovTanX);
+ rq.glY = view.y / (view.z * fovTanY);
const float sunRadiusTan = std::tan(0.53F * 0.5F * M_PI_F / 180.0F);
- const float sunSizeXNDC = sunRadiusTan / fovTanX;
- const float sunSizeYNDC = sunRadiusTan / fovTanY;
-
- // Convert sunScreen to the offscreen-texture UV space.
- // Vulkan stores row 0 at the top of the displayed scene
- // (negative-height main viewport), so the +Y-up GL NDC
- // needs a vertical flip: vk_y = -gl_y, then *0.5+0.5.
- sunTexPosX = sunScreenGLx * 0.5F + 0.5F;
- sunTexPosY = -sunScreenGLy * 0.5F + 0.5F;
- sunTexSizeX = sunSizeXNDC * 0.5F;
- sunTexSizeY = sunSizeYNDC * 0.5F;
+ // vk texcoords: row 0 on top, so flip the +Y-up GL NDC
+ rq.texPosX = rq.glX * 0.5F + 0.5F;
+ rq.texPosY = -rq.glY * 0.5F + 0.5F;
+ rq.texSizeX = (sunRadiusTan / fovTanX) * 0.5F;
+ rq.texSizeY = (sunRadiusTan / fovTanY) * 0.5F;
+ rq.scanZ = 0.9999999F;
if (!infinityDistance) {
const float fnear = def.zNear;
const float ffar = def.zFar;
- const float depth = sunView.z;
- scanZ = ffar * (fnear - depth) / (depth * (fnear - ffar));
+ const float depth = view.z;
+ rq.scanZ = ffar * (fnear - depth) / (depth * (fnear - ffar));
+ }
+
+ rq.color = col;
+ rq.reflections = reflections;
+ requests.push_back(std::move(rq));
+ };
+
+ // sun
+ AddRequest(renderer.GetSunDirection(), MakeVector3(1.0F, 0.9F, 0.8F),
+ true, true);
+
+ // dynamic lights (r_lensFlareDynamic); culls copied from
+ // GLRenderer's dynamic-flare loop
+ if ((int)r_lensFlareDynamic) {
+ for (const auto& param : renderer.GetDynamicLights()) {
+ if (requests.size() > kMaxDynamicFlares)
+ break;
+ if (!param.useLensFlare)
+ continue;
+
+ Vector3 color = param.color * 0.6F;
+ {
+ float rad = (param.origin - def.viewOrigin).GetSquaredLength();
+ rad /= param.radius * param.radius * 18.0F;
+ if (rad > 1.0F)
+ continue;
+ color *= 1.0F - rad;
+ }
+
+ if (param.type == client::DynamicLightTypeSpotlight) {
+ Vector3 diff = (def.viewOrigin - param.origin).Normalize();
+ Vector3 lightDir = param.spotAxis[2];
+ lightDir = lightDir.Normalize();
+ float cosVal = Vector3::Dot(diff, lightDir);
+ float minCosVal = cosf(param.spotAngle * 0.5F);
+ if (cosVal < minCosVal)
+ continue;
+ color *= (cosVal - minCosVal) / (1.0F - minCosVal);
+ }
+
+ if (Vector3::Dot(def.viewAxis[2], param.origin - def.viewOrigin) < 0.0F)
+ continue;
+
+ AddRequest(param.origin - def.viewOrigin, color, false, true);
}
}
VulkanTemporaryImagePool* pool = renderer.GetTemporaryImagePool();
- // ── 2. Scanner pass (skip if sun not visible) ─────────────
- Handle visibilityImg;
+ // ── Scanner + blur per flare ──────────────────────────────
std::vector> blurTemps;
- if (sunVisible && pool) {
- visibilityImg = pool->Acquire(kVisibilityDim, kVisibilityDim, colorFormat);
- VkFramebuffer fb = MakeFramebuffer(scannerRenderPass,
- visibilityImg.GetPointerOrNull(), frameSlot);
-
- Handle depthImg = renderer.GetFramebufferManager()->GetResolvedDepthImage();
- VkDescriptorSet depthDS = BindShadowDepth(frameSlot, depthImg->GetImageView());
-
- VkClearValue cv{};
- cv.color = {{0.0f, 0.0f, 0.0f, 1.0f}};
-
- VkRenderPassBeginInfo rpBegin{};
- rpBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
- rpBegin.renderPass = scannerRenderPass;
- rpBegin.framebuffer = fb;
- rpBegin.renderArea.extent = {kVisibilityDim, kVisibilityDim};
- rpBegin.clearValueCount = 1;
- rpBegin.pClearValues = &cv;
-
- vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE);
-
- VkViewport viewport{0.0f, 0.0f, (float)kVisibilityDim, (float)kVisibilityDim, 0.0f, 1.0f};
- VkRect2D scissor{{0, 0}, {kVisibilityDim, kVisibilityDim}};
- vkCmdSetViewport(cmd, 0, 1, &viewport);
- vkCmdSetScissor(cmd, 0, 1, &scissor);
-
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, scannerPipeline);
- vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
- scannerLayout, 0, 1, &depthDS, 0, nullptr);
-
- ScannerPC pc{};
- pc.scanRange[0] = sunTexPosX - sunTexSizeX;
- pc.scanRange[1] = sunTexPosY - sunTexSizeY;
- pc.scanRange[2] = sunTexPosX + sunTexSizeX;
- pc.scanRange[3] = sunTexPosY + sunTexSizeY;
- pc.scanZ = scanZ;
- vkCmdPushConstants(cmd, scannerLayout, VK_SHADER_STAGE_VERTEX_BIT,
- 0, sizeof(pc), &pc);
-
- vkCmdDraw(cmd, 6, 1, 0, 0);
- vkCmdEndRenderPass(cmd);
+ if (pool) {
+ Handle depthImg =
+ renderer.GetFramebufferManager()->GetResolvedDepthImage();
- // ── 3. Three 1D-Gauss blurs (spread 1, 2, 4), x then y ─
auto BlurPass = [&](VulkanImage* src, VulkanImage* dst, float shiftX, float shiftY) {
VkFramebuffer fb2 = MakeFramebuffer(blurRenderPass, dst, frameSlot);
VkDescriptorSet srcDS = BindSingleTexture(frameSlot, src->GetImageView());
@@ -719,31 +717,74 @@ namespace spades {
vkCmdPushConstants(cmd, blurLayout, VK_SHADER_STAGE_FRAGMENT_BIT,
0, sizeof(bpc), &bpc);
- vkCmdDraw(cmd, 6, 1, 0, 0);
+ // passVS is a 3-vertex fullscreen triangle
+ vkCmdDraw(cmd, 3, 1, 0, 0);
vkCmdEndRenderPass(cmd);
};
- const float invDim = 1.0F / (float)kVisibilityDim;
- const float spreads[3] = {1.0F, 2.0F, 4.0F};
- VulkanImage* prev = visibilityImg.GetPointerOrNull();
+ for (FlareRequest& rq : requests) {
+ rq.visibility = pool->Acquire(kVisibilityDim, kVisibilityDim, colorFormat);
+ VkFramebuffer fb = MakeFramebuffer(scannerRenderPass,
+ rq.visibility.GetPointerOrNull(), frameSlot);
+
+ VkDescriptorSet depthDS = BindShadowDepth(frameSlot, depthImg->GetImageView(),
+ depthImg->GetSampler());
+
+ VkClearValue cv{};
+ cv.color = {{0.0f, 0.0f, 0.0f, 1.0f}};
+
+ VkRenderPassBeginInfo rpBegin{};
+ rpBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
+ rpBegin.renderPass = scannerRenderPass;
+ rpBegin.framebuffer = fb;
+ rpBegin.renderArea.extent = {kVisibilityDim, kVisibilityDim};
+ rpBegin.clearValueCount = 1;
+ rpBegin.pClearValues = &cv;
+
+ vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE);
+
+ VkViewport viewport{0.0f, 0.0f, (float)kVisibilityDim, (float)kVisibilityDim, 0.0f, 1.0f};
+ VkRect2D scissor{{0, 0}, {kVisibilityDim, kVisibilityDim}};
+ vkCmdSetViewport(cmd, 0, 1, &viewport);
+ vkCmdSetScissor(cmd, 0, 1, &scissor);
- for (int i = 0; i < 3; ++i) {
- Handle tmpX = pool->Acquire(kVisibilityDim, kVisibilityDim, colorFormat);
- BlurPass(prev, tmpX.GetPointerOrNull(), spreads[i] * invDim, 0.0F);
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, scannerPipeline);
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
+ scannerLayout, 0, 1, &depthDS, 0, nullptr);
+
+ ScannerPC pc{};
+ pc.scanRange[0] = rq.texPosX - rq.texSizeX;
+ pc.scanRange[1] = rq.texPosY - rq.texSizeY;
+ pc.scanRange[2] = rq.texPosX + rq.texSizeX;
+ pc.scanRange[3] = rq.texPosY + rq.texSizeY;
+ pc.scanZ = rq.scanZ;
+ vkCmdPushConstants(cmd, scannerLayout, VK_SHADER_STAGE_VERTEX_BIT,
+ 0, sizeof(pc), &pc);
+
+ vkCmdDraw(cmd, 6, 1, 0, 0);
+ vkCmdEndRenderPass(cmd);
+
+ // three 1D-Gauss blurs (spread 1, 2, 4), x then y
+ const float invDim = 1.0F / (float)kVisibilityDim;
+ const float spreads[3] = {1.0F, 2.0F, 4.0F};
- Handle tmpY = pool->Acquire(kVisibilityDim, kVisibilityDim, colorFormat);
- BlurPass(tmpX.GetPointerOrNull(), tmpY.GetPointerOrNull(), 0.0F, spreads[i] * invDim);
+ for (int i = 0; i < 3; ++i) {
+ Handle tmpX = pool->Acquire(kVisibilityDim, kVisibilityDim, colorFormat);
+ BlurPass(rq.visibility.GetPointerOrNull(), tmpX.GetPointerOrNull(),
+ spreads[i] * invDim, 0.0F);
- blurTemps.push_back(std::move(tmpX));
- if (i + 1 < 3) {
- blurTemps.push_back(std::move(visibilityImg));
+ Handle tmpY = pool->Acquire(kVisibilityDim, kVisibilityDim, colorFormat);
+ BlurPass(tmpX.GetPointerOrNull(), tmpY.GetPointerOrNull(),
+ 0.0F, spreads[i] * invDim);
+
+ blurTemps.push_back(std::move(tmpX));
+ blurTemps.push_back(std::move(rq.visibility));
+ rq.visibility = std::move(tmpY);
}
- visibilityImg = std::move(tmpY);
- prev = visibilityImg.GetPointerOrNull();
}
}
- // ── 4. Final pass: passthrough(input → output) + additive flares
+ // ── Final pass: passthrough(input → output) + additive flares
{
VkFramebuffer fb = MakeFramebuffer(finalRenderPass, output, frameSlot);
@@ -759,7 +800,7 @@ namespace spades {
vkCmdSetViewport(cmd, 0, 1, &vp2);
vkCmdSetScissor(cmd, 0, 1, &sc2);
- // 4a. Passthrough scene → output (no blend)
+ // passthrough scene → output (no blend)
{
VkDescriptorSet inputDS = BindSingleTexture(frameSlot, input->GetImageView());
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, passthroughPipeline);
@@ -768,24 +809,28 @@ namespace spades {
vkCmdDraw(cmd, 3, 1, 0, 0);
}
- // 4b. Additive flare quads — only if the sun is in front of us
- if (sunVisible && visibilityImg) {
+ if (!requests.empty())
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, flareDrawPipeline);
- VkImageView visView = visibilityImg->GetImageView();
-
- // Flip the sun's NDC y so quads land at the right pixel row
- // (the Y-down Vulkan framebuffer vs. the +Y-up GL math).
- const float sx = sunScreenGLx;
- const float sy = -sunScreenGLy;
-
- // Pixel-aspect-corrected sprite size. Matches GL:
- // sunSize = (0.01, 0.01); sunSize.x *= ScreenH / ScreenW.
- float szx = 0.01F;
- float szy = 0.01F;
- const float scrW = renderer.ScreenWidth();
- const float scrH = renderer.ScreenHeight();
- if (scrW > 0.0F)
- szx *= scrH / scrW;
+
+ // pixel-aspect-corrected sprite size, like GL:
+ // sunSize = (0.01, 0.01); sunSize.x *= ScreenH / ScreenW
+ float baseSzx = 0.01F;
+ const float baseSzy = 0.01F;
+ const float scrW = renderer.ScreenWidth();
+ const float scrH = renderer.ScreenHeight();
+ if (scrW > 0.0F)
+ baseSzx *= scrH / scrW;
+
+ for (const FlareRequest& rq : requests) {
+ if (!rq.visibility)
+ continue;
+
+ VkImageView visView = rq.visibility->GetImageView();
+ const float sx = rq.glX;
+ const float sy = -rq.glY; // vk Y-down framebuffer
+ const float szx = baseSzx;
+ const float szy = baseSzy;
+ const Vector3 sunCol = rq.color;
const float sqLen = sx * sx + sy * sy;
const float aroundness = sqLen * 0.6F;
@@ -806,14 +851,14 @@ namespace spades {
float dr[4];
float color[3];
- // — Sun glow ring (flare4 + white)
+ // sun glow ring (flare4 + white)
CenteredRange(sx, sy, szx * 256.0F, szy * 256.0F, dr);
color[0] = sunCol.x * 0.04F;
color[1] = sunCol.y * 0.03F;
color[2] = sunCol.z * 0.04F;
DrawFlareQuad(cmd, frameSlot, visView, white, flare4, dr, color);
- // — Concentric white sun glows
+ // concentric white glows
CenteredRange(sx, sy, szx * 64.0F, szy * 64.0F, dr);
color[0] = sunCol.x * 0.3F; color[1] = sunCol.y * 0.3F; color[2] = sunCol.z * 0.3F;
DrawFlareQuad(cmd, frameSlot, visView, white, white, dr, color);
@@ -830,22 +875,21 @@ namespace spades {
color[0] = sunCol.x * 1.0F; color[1] = sunCol.y * 1.0F; color[2] = sunCol.z * 1.0F;
DrawFlareQuad(cmd, frameSlot, visView, white, white, dr, color);
- // — Horizontal sun stripe (256×8 in NDC)
+ // horizontal stripe (256×8 in NDC)
CenteredRange(sx, sy, szx * 256.0F, szy * 8.0F, dr);
color[0] = sunCol.x * 0.1F;
color[1] = sunCol.y * 0.05F;
color[2] = sunCol.z * 0.1F;
DrawFlareQuad(cmd, frameSlot, visView, white, white, dr, color);
- // — Dust ring (mask3 + white), modulated by `aroundness`
+ // dust ring (mask3 + white), modulated by aroundness
CenteredRange(sx, sy, szx * 188.0F, szy * 188.0F, dr);
color[0] = sunCol.x * 0.4F * aroundness;
color[1] = sunCol.y * 0.4F * aroundness;
color[2] = sunCol.z * 0.4F * aroundness;
DrawFlareQuad(cmd, frameSlot, visView, mask3, white, dr, color);
- if (renderReflections) {
- // Reflection 1 (white + flare2, mirrored through origin × 0.4)
+ if (rq.reflections) {
MakeRange(-(sx - szx * 18.0F) * 0.4F,
-(sy - szy * 18.0F) * 0.4F,
-(sx + szx * 18.0F) * 0.4F,
@@ -874,7 +918,6 @@ namespace spades {
color[0] = sunCol.x * 0.3F; color[1] = sunCol.y * 0.3F; color[2] = sunCol.z * 0.3F;
DrawFlareQuad(cmd, frameSlot, visView, white, flare2, dr, color);
- // — mask2 + flare1 mirrors
MakeRange((sx - szx * 96.0F) * 2.3F,
(sy - szy * 96.0F) * 2.3F,
(sx + szx * 96.0F) * 2.3F,
@@ -893,7 +936,6 @@ namespace spades {
color[2] = sunCol.z * 0.1F;
DrawFlareQuad(cmd, frameSlot, visView, mask2, flare1, dr, color);
- // — mask2 + flare3 (close mirror)
MakeRange((sx - szx * 18.0F) * 0.5F,
(sy - szy * 18.0F) * 0.5F,
(sx + szx * 18.0F) * 0.5F,
@@ -901,7 +943,6 @@ namespace spades {
color[0] = sunCol.x * 0.3F; color[1] = sunCol.y * 0.3F; color[2] = sunCol.z * 0.3F;
DrawFlareQuad(cmd, frameSlot, visView, mask2, flare3, dr, color);
- // — mask1 + flare3 large aroundness ring
const float reflSize = 50.0F + aroundness2 * 60.0F;
MakeRange((sx - szx * reflSize) * -2.0F,
(sy - szy * reflSize) * -2.0F,
@@ -917,13 +958,11 @@ namespace spades {
vkCmdEndRenderPass(cmd);
}
- // Return temp images only after the command buffer has finished
- // referencing them — defer Return() to end of Filter() (the pool
- // is reused next frame, but this call is the last command-buffer
- // recording in the chain for these images).
+ // return temps only after all recording that references them
if (pool) {
- if (visibilityImg)
- pool->Return(visibilityImg.GetPointerOrNull());
+ for (FlareRequest& rq : requests)
+ if (rq.visibility)
+ pool->Return(rq.visibility.GetPointerOrNull());
for (auto& tmp : blurTemps)
pool->Return(tmp.GetPointerOrNull());
}
diff --git a/Sources/Draw/Vulkan/VulkanLensFlareFilter.h b/Sources/Draw/Vulkan/VulkanLensFlareFilter.h
index ad0964732..b3046eaf6 100644
--- a/Sources/Draw/Vulkan/VulkanLensFlareFilter.h
+++ b/Sources/Draw/Vulkan/VulkanLensFlareFilter.h
@@ -48,7 +48,6 @@ namespace spades {
VkFormat colorFormat;
VkSampler linearSampler;
- VkSampler depthShadowSampler;
VkRenderPass scannerRenderPass;
VkRenderPass blurRenderPass;
@@ -88,7 +87,8 @@ namespace spades {
VkFramebuffer MakeFramebuffer(VkRenderPass rp, VulkanImage* image, int frameSlot);
- VkDescriptorSet BindShadowDepth(int frameSlot, VkImageView depthView);
+ VkDescriptorSet BindShadowDepth(int frameSlot, VkImageView depthView,
+ VkSampler depthSampler);
VkDescriptorSet BindSingleTexture(int frameSlot, VkImageView view);
VkDescriptorSet BindFlareTextures(int frameSlot,
VkImageView visibility,
diff --git a/Sources/Draw/Vulkan/VulkanMapRenderer.cpp b/Sources/Draw/Vulkan/VulkanMapRenderer.cpp
index fa4308c88..cb9d3884d 100644
--- a/Sources/Draw/Vulkan/VulkanMapRenderer.cpp
+++ b/Sources/Draw/Vulkan/VulkanMapRenderer.cpp
@@ -543,7 +543,7 @@ namespace spades {
bindingDescription.stride = 20;
bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
- std::array attributeDescriptions{};
+ std::array attributeDescriptions{};
// Position (location 0) - x, y, z are uint8_t at offset 0
attributeDescriptions[0].binding = 0;
@@ -569,6 +569,14 @@ namespace spades {
attributeDescriptions[3].format = VK_FORMAT_R8G8B8_SINT;
attributeDescriptions[3].offset = 12;
+ // Fixed position (location 4) - sx, sy, sz are int8_t at offset 16.
+ // Face-center * 2 (chunk-local); used for map-shadow/AO/radiosity
+ // sampling to avoid voxel-boundary bleed (lit sliver on face edges).
+ attributeDescriptions[4].binding = 0;
+ attributeDescriptions[4].location = 4;
+ attributeDescriptions[4].format = VK_FORMAT_R8G8B8_SINT;
+ attributeDescriptions[4].offset = 16;
+
VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 1;
diff --git a/Sources/Draw/Vulkan/VulkanMapShadowRenderer.cpp b/Sources/Draw/Vulkan/VulkanMapShadowRenderer.cpp
index 8e157db2f..39b742bbf 100644
--- a/Sources/Draw/Vulkan/VulkanMapShadowRenderer.cpp
+++ b/Sources/Draw/Vulkan/VulkanMapShadowRenderer.cpp
@@ -25,6 +25,7 @@
#include
#include
#include
+#include
namespace spades {
namespace draw {
@@ -240,7 +241,12 @@ namespace spades {
void VulkanMapShadowRenderer::Update(VkCommandBuffer commandBuffer) {
SPADES_MARK_FUNCTION();
- bool anyChanges = false;
+ // Dirty 32-texel spans, coalesced per row; adjacent dirty words in
+ // the same row merge into one copy region.
+ struct Span {
+ int x, y, width;
+ };
+ std::vector spans;
for (size_t i = 0; i < updateBitmap.size(); i++) {
if (updateBitmap[i] == 0)
@@ -250,28 +256,72 @@ namespace spades {
int x = static_cast((i - y * updateBitmapPitch) * 32);
size_t bitmapPixelPosBase = i * 32;
+ bool wordChanged = false;
for (int j = 0; j < 32; j++) {
uint32_t pixel = GeneratePixel(x + j, y);
if (bitmap[bitmapPixelPosBase + j] != pixel) {
bitmap[bitmapPixelPosBase + j] = pixel;
- anyChanges = true;
+ wordChanged = true;
}
}
updateBitmap[i] = 0;
+
+ if (wordChanged) {
+ if (!spans.empty() && spans.back().y == y &&
+ spans.back().x + spans.back().width == x) {
+ spans.back().width += 32;
+ } else {
+ spans.push_back({x, y, 32});
+ }
+ }
}
- if (!anyChanges)
+ if (spans.empty())
return;
- // Upload entire bitmap to staging buffer and copy to image
- stagingBuffer->UpdateData(bitmap.data(), w * h * 4);
+ size_t dirtyTexels = 0;
+ for (const Span& s : spans)
+ dirtyTexels += (size_t)s.width;
shadowImage->TransitionLayout(commandBuffer, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
- shadowImage->CopyFromBuffer(commandBuffer, stagingBuffer->GetBuffer());
+ // Heavy edits: one full upload beats hundreds of tiny copies.
+ if (dirtyTexels * 4 >= (size_t)w * h || spans.size() > 256) {
+ stagingBuffer->UpdateData(bitmap.data(), w * h * 4);
+ shadowImage->CopyFromBuffer(commandBuffer, stagingBuffer->GetBuffer());
+ } else {
+ // Pack dirty spans contiguously into staging, one copy region each.
+ std::vector regions;
+ regions.reserve(spans.size());
+ VkDeviceSize offset = 0;
+ char* mapped = static_cast(stagingBuffer->Map());
+ for (const Span& s : spans) {
+ std::memcpy(mapped + offset, bitmap.data() + s.y * w + s.x,
+ (size_t)s.width * 4);
+
+ VkBufferImageCopy region{};
+ region.bufferOffset = offset;
+ region.bufferRowLength = 0;
+ region.bufferImageHeight = 0;
+ region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+ region.imageSubresource.mipLevel = 0;
+ region.imageSubresource.baseArrayLayer = 0;
+ region.imageSubresource.layerCount = 1;
+ region.imageOffset = {s.x, s.y, 0};
+ region.imageExtent = {(uint32_t)s.width, 1, 1};
+ regions.push_back(region);
+
+ offset += (VkDeviceSize)s.width * 4;
+ }
+ stagingBuffer->Unmap();
+ vkCmdCopyBufferToImage(commandBuffer, stagingBuffer->GetBuffer(),
+ shadowImage->GetImage(),
+ VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+ (uint32_t)regions.size(), regions.data());
+ }
shadowImage->TransitionLayout(commandBuffer, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
diff --git a/Sources/Draw/Vulkan/VulkanOptimizedVoxelModel.cpp b/Sources/Draw/Vulkan/VulkanOptimizedVoxelModel.cpp
index 2a1f1feb3..8a60badd8 100644
--- a/Sources/Draw/Vulkan/VulkanOptimizedVoxelModel.cpp
+++ b/Sources/Draw/Vulkan/VulkanOptimizedVoxelModel.cpp
@@ -22,6 +22,7 @@
#include "VulkanSpirvCache.h"
#include "VulkanRenderer.h"
#include "VulkanMapRenderer.h"
+#include "VulkanShadowMapRenderer.h"
#include "VulkanBuffer.h"
#include "VulkanImage.h"
#include "VulkanImageWrapper.h"
@@ -471,6 +472,15 @@ namespace spades {
&shadowDs, 0, nullptr);
}
}
+ {
+ VulkanShadowMapRenderer* smr = renderer.GetShadowMapRenderer();
+ if (smr && smr->GetSamplingDescriptorSet() != VK_NULL_HANDLE) {
+ VkDescriptorSet samplingSet = smr->GetSamplingDescriptorSet();
+ vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
+ sharedPipeline.pipelineLayout, 1, 1,
+ &samplingSet, 0, nullptr);
+ }
+ }
// Bind vertex buffer
VkBuffer vb = vertexBuffer->GetBuffer();
@@ -814,6 +824,15 @@ namespace spades {
&shadowDs, 0, nullptr);
}
}
+ {
+ VulkanShadowMapRenderer* smr = renderer.GetShadowMapRenderer();
+ if (smr && smr->GetSamplingDescriptorSet() != VK_NULL_HANDLE) {
+ VkDescriptorSet samplingSet = smr->GetSamplingDescriptorSet();
+ vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
+ sharedPipeline.pipelineLayout, 1, 1,
+ &samplingSet, 0, nullptr);
+ }
+ }
// Bind vertex buffer
VkBuffer vb = vertexBuffer->GetBuffer();
@@ -1312,10 +1331,20 @@ namespace spades {
pushConstantRange.size = offsetof(ModelSolidPushConstants, physicalTail);
}
+ // Set 1 = model-shadow cascade sampling (owned by the shadow map
+ // renderer, same set the map lit pipeline binds). The Phys/ghost
+ // fragment shaders that don't declare it are still compatible with
+ // the wider layout.
+ VulkanShadowMapRenderer* smrLayout = renderer.GetShadowMapRenderer();
+ VkDescriptorSetLayout modelSetLayouts[2] = {
+ sharedPipeline.descriptorSetLayout,
+ smrLayout ? smrLayout->GetSamplingSetLayout() : VK_NULL_HANDLE};
+
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
- pipelineLayoutInfo.setLayoutCount = 1;
- pipelineLayoutInfo.pSetLayouts = &sharedPipeline.descriptorSetLayout;
+ pipelineLayoutInfo.setLayoutCount =
+ (modelSetLayouts[1] != VK_NULL_HANDLE) ? 2 : 1;
+ pipelineLayoutInfo.pSetLayouts = modelSetLayouts;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
diff --git a/Sources/Draw/Vulkan/VulkanRenderer.cpp b/Sources/Draw/Vulkan/VulkanRenderer.cpp
index 7b3a5600e..c08f1b2a1 100644
--- a/Sources/Draw/Vulkan/VulkanRenderer.cpp
+++ b/Sources/Draw/Vulkan/VulkanRenderer.cpp
@@ -18,6 +18,7 @@
*/
+#include
#include "VulkanRenderer.h"
#include "VulkanMapRenderer.h"
#include "VulkanModelRenderer.h"
@@ -43,6 +44,8 @@
#include "VulkanFogFilter.h"
#include "VulkanDepthOfFieldFilter.h"
#include "VulkanFXAAFilter.h"
+#include "VulkanCameraBlurFilter.h"
+#include "VulkanResampleBicubicFilter.h"
#include "VulkanCavityOutlineFilter.h"
#include "VulkanDepthResolveFilter.h"
#include "VulkanColorCorrectionFilter.h"
@@ -67,6 +70,8 @@ SPADES_SETTING(r_modelShadows);
SPADES_SETTING(r_radiosity);
SPADES_SETTING(r_depthOfField);
SPADES_SETTING(r_fxaa);
+SPADES_SETTING(r_cameraBlur);
+SPADES_SETTING(r_scaleFilter);
SPADES_SETTING(r_water);
SPADES_SETTING(r_softParticles);
SPADES_SETTING(r_outlines);
@@ -189,6 +194,8 @@ namespace spades {
fogFilter = stmp::make_unique(*this);
depthOfFieldFilter = stmp::make_unique(*this);
fxaaFilter = stmp::make_unique(*this);
+ cameraBlurFilter = stmp::make_unique(*this);
+ resampleBicubicFilter = stmp::make_unique(*this);
cavityOutlineFilter = stmp::make_unique(*this);
colorCorrectionFilter = stmp::make_unique(*this);
lensFlareFilter = stmp::make_unique(*this);
@@ -238,6 +245,8 @@ namespace spades {
colorCorrectionFilter.reset();
cavityOutlineFilter.reset();
depthResolveFilter.reset();
+ resampleBicubicFilter.reset();
+ cameraBlurFilter.reset();
fxaaFilter.reset();
depthOfFieldFilter.reset();
fogFilter.reset();
@@ -2043,11 +2052,12 @@ namespace spades {
debugLines.clear();
}
- // Clear for next frame
+ // Clear for next frame. lights survives until after the
+ // post-process chain: the lens flare filter reads it for
+ // r_lensFlareDynamic.
if (modelRenderer) {
modelRenderer->Clear();
}
- lights.clear();
// End offscreen render pass (scene without water is now complete)
vkCmdEndRenderPass(commandBuffer);
@@ -2414,6 +2424,20 @@ namespace spades {
}
}
+ // Camera motion blur (+ radial blur). GL order: DoF -> CameraBlur
+ // -> Bloom. Apply() returns false when skipped (no motion), in
+ // which case currentInput passes through unchanged.
+ {
+ const client::SceneDefinition& def = GetSceneDef();
+ if ((float)r_cameraBlur > 0.0f && !def.denyCameraBlur && cameraBlurFilter &&
+ currentInput && currentOutput) {
+ float intensity = std::min((float)r_cameraBlur * 0.2f, 1.0f);
+ if (cameraBlurFilter->Apply(commandBuffer, currentInput, currentOutput,
+ intensity, def.radialBlur))
+ std::swap(currentInput, currentOutput);
+ }
+ }
+
// Bloom
if ((int)r_bloom && bloomFilter && currentInput && currentOutput) {
bloomFilter->Filter(commandBuffer, currentInput, currentOutput);
@@ -2462,15 +2486,45 @@ namespace spades {
std::swap(currentInput, currentOutput);
}
- // --- Blit final post-process result to swapchain ---
- // Transition final image (currentInput) from SHADER_READ_ONLY to TRANSFER_SRC
+ lights.clear();
+
+ // --- Resample + blit final post-process result to swapchain ---
+ // r_scaleFilter: 0 = nearest, 1 = bilinear (blit filter),
+ // 2 = bicubic via VulkanResampleBicubicFilter to a swapchain-sized
+ // temp, then a 1:1 blit. Mirrors GLRenderer.cpp:1073.
+ // currentInput stays the render-res image for the screenshot
+ // mirror below; blitSrc is what actually reaches the swapchain.
+ VulkanImage* blitSrc = currentInput;
+ Handle resampledImage;
+ VkFilter blitFilter = VK_FILTER_LINEAR;
+ {
+ VkExtent2D scExtent = device->GetSwapchainExtent();
+ int sf = (int)r_scaleFilter;
+ if (sf == 0) {
+ blitFilter = VK_FILTER_NEAREST;
+ } else if (sf == 2 && resampleBicubicFilter && temporaryImagePool &&
+ currentInput &&
+ ((uint32_t)renderWidth != scExtent.width ||
+ (uint32_t)renderHeight != scExtent.height)) {
+ resampledImage = temporaryImagePool->Acquire(
+ scExtent.width, scExtent.height,
+ framebufferManager->GetMainColorFormat());
+ if (resampledImage) {
+ resampleBicubicFilter->Filter(commandBuffer, currentInput,
+ resampledImage.GetPointerOrNull());
+ blitSrc = resampledImage.GetPointerOrNull();
+ }
+ }
+ }
+
+ // Transition blit source from SHADER_READ_ONLY to TRANSFER_SRC
VkImageMemoryBarrier barrier1{};
barrier1.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier1.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier1.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier1.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier1.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
- barrier1.image = currentInput->GetImage();
+ barrier1.image = blitSrc->GetImage();
barrier1.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier1.subresourceRange.baseMipLevel = 0;
barrier1.subresourceRange.levelCount = 1;
@@ -2510,7 +2564,8 @@ namespace spades {
blitRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blitRegion.srcSubresource.layerCount = 1;
blitRegion.srcOffsets[0] = {0, 0, 0};
- blitRegion.srcOffsets[1] = {renderWidth, renderHeight, 1};
+ blitRegion.srcOffsets[1] = {(int32_t)blitSrc->GetWidth(),
+ (int32_t)blitSrc->GetHeight(), 1};
blitRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blitRegion.dstSubresource.layerCount = 1;
blitRegion.dstOffsets[0] = {0, 0, 0};
@@ -2518,9 +2573,20 @@ namespace spades {
static_cast(device->GetSwapchainExtent().height), 1};
vkCmdBlitImage(commandBuffer,
- currentInput->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+ blitSrc->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
device->GetSwapchainImage(imageIndex), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
- 1, &blitRegion, VK_FILTER_LINEAR);
+ 1, &blitRegion, blitFilter);
+
+ // If the bicubic pass ran, currentInput was never transitioned to
+ // TRANSFER_SRC; do it now for the screenshot mirror copy below.
+ if (blitSrc != currentInput) {
+ VkImageMemoryBarrier miBarrier = barrier1;
+ miBarrier.image = currentInput->GetImage();
+ vkCmdPipelineBarrier(commandBuffer,
+ VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
+ VK_PIPELINE_STAGE_TRANSFER_BIT,
+ 0, 0, nullptr, 0, nullptr, 1, &miBarrier);
+ }
// Mirror the final post-process result into renderColorImage so
// ReadBitmap (screenshot capture) sees what the player sees. The
diff --git a/Sources/Draw/Vulkan/VulkanRenderer.h b/Sources/Draw/Vulkan/VulkanRenderer.h
index edbf1666a..2d50acbbf 100644
--- a/Sources/Draw/Vulkan/VulkanRenderer.h
+++ b/Sources/Draw/Vulkan/VulkanRenderer.h
@@ -61,6 +61,8 @@ namespace spades {
class VulkanFogFilter;
class VulkanDepthOfFieldFilter;
class VulkanFXAAFilter;
+ class VulkanCameraBlurFilter;
+ class VulkanResampleBicubicFilter;
class VulkanCavityOutlineFilter;
class VulkanDepthResolveFilter;
class VulkanColorCorrectionFilter;
@@ -172,6 +174,8 @@ namespace spades {
std::unique_ptr fogFilter;
std::unique_ptr depthOfFieldFilter;
std::unique_ptr fxaaFilter;
+ std::unique_ptr cameraBlurFilter;
+ std::unique_ptr resampleBicubicFilter;
std::unique_ptr cavityOutlineFilter;
std::unique_ptr colorCorrectionFilter;
std::unique_ptr lensFlareFilter;
@@ -292,6 +296,7 @@ namespace spades {
float GetFogDistance() { return fogDistance; }
const client::SceneDefinition& GetSceneDef() const { return sceneDef; }
+ const std::vector& GetDynamicLights() const { return lights; }
// Canonical sun direction (points TOWARD the sun), matching the lens
// flare, water and lit shaders. Single source of truth so the shadow
diff --git a/Sources/Draw/Vulkan/VulkanResampleBicubicFilter.cpp b/Sources/Draw/Vulkan/VulkanResampleBicubicFilter.cpp
new file mode 100644
index 000000000..e10d93393
--- /dev/null
+++ b/Sources/Draw/Vulkan/VulkanResampleBicubicFilter.cpp
@@ -0,0 +1,334 @@
+/*
+ Copyright (c) 2013 Fran6nd
+
+ This file is part of ZeroSpades, a fork of OpenSpades.
+
+ OpenSpades is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OpenSpades is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OpenSpades. If not, see .
+
+ */
+
+#include "VulkanResampleBicubicFilter.h"
+#include "VulkanFramebufferManager.h"
+#include "VulkanImage.h"
+#include "VulkanRenderer.h"
+#include "VulkanRenderPassUtils.h"
+#include
+#include
+#include
+#include
+#include
+
+namespace spades {
+ namespace draw {
+
+ VulkanResampleBicubicFilter::VulkanResampleBicubicFilter(VulkanRenderer& r)
+ : VulkanPostProcessFilter(r),
+ colorFormat(VK_FORMAT_UNDEFINED),
+ linearSampler(VK_NULL_HANDLE),
+ ppRenderPass(VK_NULL_HANDLE),
+ singleSamplerDSL(VK_NULL_HANDLE),
+ resampleLayout(VK_NULL_HANDLE),
+ resamplePipeline(VK_NULL_HANDLE) {
+ SPADES_MARK_FUNCTION();
+
+ for (int i = 0; i < MAX_FRAME_SLOTS; ++i)
+ perFrameDescPool[i] = VK_NULL_HANDLE;
+
+ colorFormat = r.GetFramebufferManager()->GetMainColorFormat();
+
+ InitRenderPass();
+ InitDescriptorSetLayout();
+ InitPipeline();
+ InitDescriptorPools();
+ }
+
+ VulkanResampleBicubicFilter::~VulkanResampleBicubicFilter() {
+ SPADES_MARK_FUNCTION();
+
+ VkDevice dev = device->GetDevice();
+
+ for (int i = 0; i < MAX_FRAME_SLOTS; ++i) {
+ for (VkFramebuffer fb : perFrameFramebuffers[i])
+ vkDestroyFramebuffer(dev, fb, nullptr);
+ if (perFrameDescPool[i] != VK_NULL_HANDLE)
+ vkDestroyDescriptorPool(dev, perFrameDescPool[i], nullptr);
+ }
+
+ if (resamplePipeline != VK_NULL_HANDLE) vkDestroyPipeline(dev, resamplePipeline, nullptr);
+ if (resampleLayout != VK_NULL_HANDLE) vkDestroyPipelineLayout(dev, resampleLayout, nullptr);
+ if (singleSamplerDSL != VK_NULL_HANDLE) vkDestroyDescriptorSetLayout(dev, singleSamplerDSL, nullptr);
+ if (linearSampler != VK_NULL_HANDLE) vkDestroySampler(dev, linearSampler, nullptr);
+ if (ppRenderPass != VK_NULL_HANDLE) vkDestroyRenderPass(dev, ppRenderPass, nullptr);
+ }
+
+ void VulkanResampleBicubicFilter::InitRenderPass() {
+ VkDevice dev = device->GetDevice();
+
+ VkSubpassDependency dep{};
+ dep.srcSubpass = VK_SUBPASS_EXTERNAL;
+ dep.dstSubpass = 0;
+ dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+ dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
+ dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+ dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
+
+ ppRenderPass = CreateSimpleColorRenderPass(
+ dev, colorFormat,
+ VK_ATTACHMENT_LOAD_OP_DONT_CARE,
+ VK_IMAGE_LAYOUT_UNDEFINED,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+ &dep);
+
+ VkSamplerCreateInfo si{};
+ si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
+ si.magFilter = VK_FILTER_LINEAR;
+ si.minFilter = VK_FILTER_LINEAR;
+ si.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ si.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ si.anisotropyEnable = VK_FALSE;
+ si.maxAnisotropy = 1.0f;
+ si.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
+ si.unnormalizedCoordinates = VK_FALSE;
+ si.compareEnable = VK_FALSE;
+ si.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
+
+ if (vkCreateSampler(dev, &si, nullptr, &linearSampler) != VK_SUCCESS)
+ SPRaise("Failed to create resample sampler");
+ }
+
+ void VulkanResampleBicubicFilter::InitDescriptorSetLayout() {
+ VkDescriptorSetLayoutBinding b{};
+ b.binding = 0;
+ b.descriptorCount = 1;
+ b.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+ b.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
+
+ VkDescriptorSetLayoutCreateInfo info{};
+ info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
+ info.bindingCount = 1;
+ info.pBindings = &b;
+
+ if (vkCreateDescriptorSetLayout(device->GetDevice(), &info, nullptr, &singleSamplerDSL) != VK_SUCCESS)
+ SPRaise("Failed to create resample descriptor set layout");
+ }
+
+ VkShaderModule VulkanResampleBicubicFilter::LoadSPIRV(const char* path) {
+ std::string data = FileManager::ReadAllBytes(path);
+ std::vector code(data.size() / sizeof(uint32_t));
+ std::memcpy(code.data(), data.data(), data.size());
+
+ VkShaderModuleCreateInfo info{};
+ info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
+ info.codeSize = data.size();
+ info.pCode = code.data();
+
+ VkShaderModule mod;
+ if (vkCreateShaderModule(device->GetDevice(), &info, nullptr, &mod) != VK_SUCCESS)
+ SPRaise("Failed to create shader module: %s", path);
+ return mod;
+ }
+
+ void VulkanResampleBicubicFilter::InitPipeline() {
+ VkDevice dev = device->GetDevice();
+ VkPipelineCache cache = renderer.GetPipelineCache();
+
+ VkShaderModule vs = LoadSPIRV("Shaders/Vulkan/PostFilters/PassThrough.vk.vs.spv");
+ VkShaderModule fs = LoadSPIRV("Shaders/Vulkan/PostFilters/ResampleBicubic.vk.fs.spv");
+
+ VkPipelineVertexInputStateCreateInfo vertexInput{};
+ vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
+
+ VkPipelineInputAssemblyStateCreateInfo ia{};
+ ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
+ ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
+
+ VkPipelineViewportStateCreateInfo vp{};
+ vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
+ vp.viewportCount = 1;
+ vp.scissorCount = 1;
+
+ VkPipelineRasterizationStateCreateInfo rs{};
+ rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
+ rs.polygonMode = VK_POLYGON_MODE_FILL;
+ rs.cullMode = VK_CULL_MODE_NONE;
+ rs.lineWidth = 1.0f;
+
+ VkPipelineMultisampleStateCreateInfo ms{};
+ ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
+ ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
+
+ VkPipelineDepthStencilStateCreateInfo ds{};
+ ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
+
+ VkDynamicState dynArr[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
+ VkPipelineDynamicStateCreateInfo dyn{};
+ dyn.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
+ dyn.dynamicStateCount = 2;
+ dyn.pDynamicStates = dynArr;
+
+ VkPipelineColorBlendAttachmentState noBlend{};
+ noBlend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
+ VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
+
+ VkPipelineColorBlendStateCreateInfo blend{};
+ blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
+ blend.attachmentCount = 1;
+ blend.pAttachments = &noBlend;
+
+ VkPushConstantRange pcr{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(float) * 2};
+ VkPipelineLayoutCreateInfo li{};
+ li.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
+ li.setLayoutCount = 1;
+ li.pSetLayouts = &singleSamplerDSL;
+ li.pushConstantRangeCount = 1;
+ li.pPushConstantRanges = &pcr;
+ if (vkCreatePipelineLayout(dev, &li, nullptr, &resampleLayout) != VK_SUCCESS)
+ SPRaise("Failed to create resample pipeline layout");
+
+ VkPipelineShaderStageCreateInfo stages[2]{};
+ stages[0] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0,
+ VK_SHADER_STAGE_VERTEX_BIT, vs, "main", nullptr};
+ stages[1] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0,
+ VK_SHADER_STAGE_FRAGMENT_BIT, fs, "main", nullptr};
+
+ VkGraphicsPipelineCreateInfo pi{};
+ pi.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
+ pi.stageCount = 2;
+ pi.pStages = stages;
+ pi.pVertexInputState = &vertexInput;
+ pi.pInputAssemblyState = &ia;
+ pi.pViewportState = &vp;
+ pi.pRasterizationState = &rs;
+ pi.pMultisampleState = &ms;
+ pi.pDepthStencilState = &ds;
+ pi.pColorBlendState = &blend;
+ pi.pDynamicState = &dyn;
+ pi.layout = resampleLayout;
+ pi.renderPass = ppRenderPass;
+ pi.subpass = 0;
+
+ if (vkCreateGraphicsPipelines(dev, cache, 1, &pi, nullptr, &resamplePipeline) != VK_SUCCESS)
+ SPRaise("Failed to create resample pipeline");
+
+ vkDestroyShaderModule(dev, vs, nullptr);
+ vkDestroyShaderModule(dev, fs, nullptr);
+ }
+
+ void VulkanResampleBicubicFilter::InitDescriptorPools() {
+ VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 4};
+ VkDescriptorPoolCreateInfo info{};
+ info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
+ info.poolSizeCount = 1;
+ info.pPoolSizes = &size;
+ info.maxSets = 4;
+
+ for (int i = 0; i < MAX_FRAME_SLOTS; ++i) {
+ if (vkCreateDescriptorPool(device->GetDevice(), &info, nullptr, &perFrameDescPool[i]) != VK_SUCCESS)
+ SPRaise("Failed to create resample descriptor pool");
+ }
+ }
+
+ VkFramebuffer VulkanResampleBicubicFilter::MakeFramebuffer(VulkanImage* image, int frameSlot) {
+ VkImageView view = image->GetImageView();
+ VkFramebufferCreateInfo fbInfo{};
+ fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
+ fbInfo.renderPass = ppRenderPass;
+ fbInfo.attachmentCount = 1;
+ fbInfo.pAttachments = &view;
+ fbInfo.width = image->GetWidth();
+ fbInfo.height = image->GetHeight();
+ fbInfo.layers = 1;
+
+ VkFramebuffer fb;
+ if (vkCreateFramebuffer(device->GetDevice(), &fbInfo, nullptr, &fb) != VK_SUCCESS)
+ SPRaise("Failed to create resample framebuffer");
+ perFrameFramebuffers[frameSlot].push_back(fb);
+ return fb;
+ }
+
+ VkDescriptorSet VulkanResampleBicubicFilter::BindTexture(int frameSlot, VkImageView view) {
+ VkDescriptorSetAllocateInfo ai{};
+ ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
+ ai.descriptorPool = perFrameDescPool[frameSlot];
+ ai.descriptorSetCount = 1;
+ ai.pSetLayouts = &singleSamplerDSL;
+ VkDescriptorSet set;
+ if (vkAllocateDescriptorSets(device->GetDevice(), &ai, &set) != VK_SUCCESS)
+ SPRaise("Failed to allocate resample descriptor set");
+
+ VkDescriptorImageInfo img{linearSampler, view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
+ VkWriteDescriptorSet w{};
+ w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+ w.dstSet = set;
+ w.dstBinding = 0;
+ w.descriptorCount = 1;
+ w.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+ w.pImageInfo = &img;
+ vkUpdateDescriptorSets(device->GetDevice(), 1, &w, 0, nullptr);
+ return set;
+ }
+
+ void VulkanResampleBicubicFilter::Filter(VkCommandBuffer cmd,
+ VulkanImage* input,
+ VulkanImage* output) {
+ SPADES_MARK_FUNCTION();
+
+ int frameSlot = static_cast(renderer.GetCurrentFrameIndex());
+
+ {
+ VkDevice dev = device->GetDevice();
+ for (VkFramebuffer fb : perFrameFramebuffers[frameSlot])
+ vkDestroyFramebuffer(dev, fb, nullptr);
+ perFrameFramebuffers[frameSlot].clear();
+ vkResetDescriptorPool(dev, perFrameDescPool[frameSlot], 0);
+ }
+
+ uint32_t w = static_cast(output->GetWidth());
+ uint32_t h = static_cast(output->GetHeight());
+
+ VkFramebuffer fb = MakeFramebuffer(output, frameSlot);
+ VkDescriptorSet ds = BindTexture(frameSlot, input->GetImageView());
+
+ VkRenderPassBeginInfo rpBegin{};
+ rpBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
+ rpBegin.renderPass = ppRenderPass;
+ rpBegin.framebuffer = fb;
+ rpBegin.renderArea.extent = {w, h};
+
+ vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE);
+
+ VkViewport viewport{0.0f, 0.0f, (float)w, (float)h, 0.0f, 1.0f};
+ VkRect2D scissor{{0, 0}, {w, h}};
+ vkCmdSetViewport(cmd, 0, 1, &viewport);
+ vkCmdSetScissor(cmd, 0, 1, &scissor);
+
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, resamplePipeline);
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
+ resampleLayout, 0, 1, &ds, 0, nullptr);
+
+ // inverseVP = 1 / SOURCE size (shader converts output UV to
+ // source pixel coords with it), matching GLResampleBicubicFilter.
+ float invVP[2] = {1.0f / (float)input->GetWidth(),
+ 1.0f / (float)input->GetHeight()};
+ vkCmdPushConstants(cmd, resampleLayout, VK_SHADER_STAGE_FRAGMENT_BIT,
+ 0, sizeof(invVP), invVP);
+
+ vkCmdDraw(cmd, 3, 1, 0, 0);
+
+ vkCmdEndRenderPass(cmd);
+ }
+
+ } // namespace draw
+} // namespace spades
diff --git a/Sources/Draw/Vulkan/VulkanResampleBicubicFilter.h b/Sources/Draw/Vulkan/VulkanResampleBicubicFilter.h
new file mode 100644
index 000000000..7b4213155
--- /dev/null
+++ b/Sources/Draw/Vulkan/VulkanResampleBicubicFilter.h
@@ -0,0 +1,68 @@
+/*
+ Copyright (c) 2013 Fran6nd
+
+ This file is part of ZeroSpades, a fork of OpenSpades.
+
+ OpenSpades is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OpenSpades is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OpenSpades. If not, see .
+
+ */
+
+#pragma once
+
+#include
+#include
+#include "VulkanPostProcessFilter.h"
+
+namespace spades {
+ namespace draw {
+
+ // Bicubic upscale (Vulkan port of GLResampleBicubicFilter,
+ // r_scaleFilter == 2). Single pass; output size may differ from
+ // input size — the framebuffer/viewport follow `output`.
+
+ class VulkanResampleBicubicFilter : public VulkanPostProcessFilter {
+
+ VkFormat colorFormat;
+ VkSampler linearSampler;
+
+ VkRenderPass ppRenderPass;
+ VkDescriptorSetLayout singleSamplerDSL;
+ VkPipelineLayout resampleLayout;
+ VkPipeline resamplePipeline;
+
+ static constexpr int MAX_FRAME_SLOTS = 2;
+ VkDescriptorPool perFrameDescPool[MAX_FRAME_SLOTS];
+ std::vector perFrameFramebuffers[MAX_FRAME_SLOTS];
+
+ void InitRenderPass();
+ void InitDescriptorSetLayout();
+ void InitPipeline();
+ void InitDescriptorPools();
+
+ VkShaderModule LoadSPIRV(const char* path);
+ VkFramebuffer MakeFramebuffer(VulkanImage* image, int frameSlot);
+ VkDescriptorSet BindTexture(int frameSlot, VkImageView view);
+
+ void CreatePipeline() override {}
+ void CreateRenderPass() override {}
+
+ public:
+ VulkanResampleBicubicFilter(VulkanRenderer& renderer);
+ ~VulkanResampleBicubicFilter();
+
+ void Filter(VkCommandBuffer cmd, VulkanImage* input, VulkanImage* output) override;
+ };
+
+ } // namespace draw
+} // namespace spades