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