Vulkan renderer: close out a bunch of GL parity gaps from TODO.md#3
Merged
Conversation
Map shadow/AO/radiosity coords were sampled at the raw vertex position. Vertices sit exactly on voxel boundaries, so the map-shadow lookup could hit the neighboring column and leak a lit sliver along face edges — visible as a thin band of light at the top of north faces when viewed from the south. The vertex already carries a "fixed position" (sx/sy/sz = face-center * 2, chunk-local), which was computed to avoid this exact glitch but never passed to the shaders. Add it as vertex attribute location 4 and use it for shadowCoord, aoCoord, and radiosityTextureCoord, matching the GL renderer's fixedPositionAttribute. - BasicMap.vert, BasicMapPhys.vert: new fixedPositionAttribute input; sample shadow/AO/radiosity at face center - VulkanMapRenderer.cpp: add attribute 4 (R8G8B8_SINT, offset 16)
Same voxel-boundary shadow bleed as the map geometry fix, but for models: the ghost cursor cube's faces lie exactly on voxel boundaries, so the map-shadow lookup could hit the neighboring column and show a lit sliver along face edges. Models have arbitrary geometry, so the map renderer's face-center sampling doesn't apply. Instead, offset the shadow sample point slightly inward along the surface normal (0.05 units) — negligible for regular models, and keeps boundary-aligned faces sampling their own column. - BasicModelVertexColor.vert, BasicModelVertexColorPhys.vert: sample map shadow at worldPos - normal * 0.05
shadow bitmap on any block change. Dirty 32-texel words are coalesced into per-row spans, packed contiguously into the existing staging buffer (single map/unmap), and copied with one multi-region vkCmdCopyBufferToImage. Falls back to a full upload when >25% of the image is dirty or spans exceed 256 (heavy edits, map load). Matches GLMapShadowRenderer's glTexSubImage2D behavior; removes the perf cliff in build-heavy games. Coarse min/max map path unchanged (already cell-granular). TODO.md corrections (three entries did not match reality): - 'Water.frag / Fog2 hardcode the sun' -- false. Water*.vk.fs and Fog2.vk.fs already read GetSunDirection() via push constants. The hardcoded const lives in Water.frag, a dead unreferenced file; added a TODO to delete it. - 'PreloadShaders is empty' (map + voxel model) -- false, both preload SPIR-V. Real remaining gap noted: pipelines still build lazily. - Marked the shadow-upload item done.
- CPU side replicates GL exactly: rotation-only view diff vs previous frame, ReverseMatrix, camera-cut guard (diag < 0.3 skips unless radial blur active), movePixels estimate, log5 iteration count, shutter scale /5 per pass, radialBlur^2 scaling of the reverse matrix. - Shaders CameraBlur.vk.vs/.fs: fullscreen triangle reprojected via reverseMatrix push constant; 5-tap depth^2-weighted smear so the view weapon (depth ~[0,0.1]) stays sharp. Y flipped in/out of the GL bottom-left math. Linear framebuffer, so GL's linearize/sqrt round trip is dropped. - Multi-pass ping-pong through VulkanTemporaryImagePool; final pass writes the chain's currentOutput. Apply() returns false on skip (no motion / cut without radial blur) so the chain doesn't swap -- mirrors GL returning the input handle untouched. - Reads resolved depth (GetResolvedDepthImage), works with and without MSAA. Chain insertion matches GL order: DoF -> CameraBlur -> Bloom (GLRenderer.cpp:963). Gated on r_cameraBlur > 0 and !sceneDef.denyCameraBlur; intensity = min(r_cameraBlur * 0.2, 1). Sources and shaders are picked up by existing CMake globs; no build system changes. TODO table updated.
nearest New VulkanResampleBicubicFilter (r_scaleFilter == 2): single fullscreen pass running Danny Ruijters' 4-fetch bicubic B-spline upscale (ResampleBicubic.vk.fs, straight port of the GL shader incl. its license notice), rendering the render-res image into a swapchain-sized temporary from VulkanTemporaryImagePool. The final swapchain blit then runs 1:1. Skipped when render size already equals swapchain size, matching GL which only resamples when scaling. The swapchain blit also honors r_scaleFilter == 0 now (VK_FILTER_NEAREST instead of always LINEAR). blitSrc/blitFilter are resolved before the transfer barriers; currentInput is kept as the render-res image for the screenshot mirror copy and gets its own TRANSFER_SRC barrier when the bicubic path replaced it as blit source. TODO.md: marked the resample row wired; corrected the water dynamic-light stub entry -- it is already an intentional no-op and GL water has no dynamic light pass either (stale TODO, fourth one so far).
grain) VulkanBloomFilter's final composite previously did a plain scene*0.8 + bloom*0.2 mix; GL's actual r_bloom (GLLensDustFilter) composites through a lens-dust overlay and adds per-frame film grain. Ported: - BloomComposite.vk.fs now implements LensDust.fs on a linear framebuffer: final = scene*0.95 + dust^2 * bloom * 2 + grain*0.003, with grain = fract(noise(uv1) + noise(uv2)) - 0.5 using GL's noiseTexCoordFactor (pushed as a vec4). - Textures/LensDustTexture.jpg registered through the renderer and bound as the dust input (falls back to the bloom texture if the wrapper cast fails, degrading to roughly the old look). - New 128x128 RGBA8 noise texture, refreshed every frame from SampleRandom() via a persistent host-coherent staging buffer (UpdateNoise mirrors GL's UpdateNoise; upload happens before the bloom passes, outside any render pass). - Descriptor plumbing: 4-binding set layout for the composite, composite pipeline layout gains a vec4 push-constant range. Remaining (noted in TODO): GL additionally runs Gauss1D H+V on the first downsample level before building the pyramid; the Vulkan pyramid skips that pre-blur, so the bloom is slightly sharper than GL. Also recorded why SSAO stays unported for now: it needs a full depth prepass, a mid-frame render-pass split for depth resolve, and a new sampler binding in every lit map/model pipeline -- too invasive to land untested in one blind commit.
VulkanLensFlareFilter::Filter no longer hard-codes the sun as the only flare source. The sun/light projection, scanner push constants and depth-based scanZ now live in a FlareRequest built per source; the sun is simply the first request (infinity distance, warm tint), and under r_lensFlareDynamic each dynamic light with useLensFlare adds another. Per-light culls are copied verbatim from GLRenderer's dynamic-flare loop: distance attenuation against radius^2 * 18, spotlight cone falloff, and behind-camera rejection. Each request gets its own 64x64 visibility scan + 3x (H+V) Gauss blur chain; all quads then draw additively inside the single final render pass after the scene passthrough, so the pp chain still sees one input -> output hop. Supporting changes: - VulkanRenderer::GetDynamicLights() getter; the per-frame lights vector now survives until after the post-process chain (it was cleared right after the opaque/dlight passes, before the flare filter could read it) and is cleared just before the swapchain blit. The EndScene dropped-frame path still clears it immediately. - Dynamic flares capped at 8 per frame and the filter's descriptor pools grown to 256 sets / 768 samplers per frame slot (each flare costs ~23 sets: scanner + 6 blurs + up to 15 quads). GL has no cap, but GL also does not have to pre-size descriptor pools; 8 visible muzzle-flash flares in one frame is already generous. No new shaders: scanner, blur and flare-draw pipelines are reused per request. TODO table row updated
…urns 0) The lens flare visibility scanner always sampled 0 from the depth texture, so the sun/light occlusion test failed everywhere and no flare quads ever passed the visibility check. Root cause: VulkanFramebufferManager::sceneDepthSampleImage was a D32_SFLOAT image with a VK_IMAGE_ASPECT_DEPTH_BIT view, sampled in shaders as plain `sampler2D`. MoltenVK maps GLSL sampler2D to Metal's texture2d<float>, which cannot read a depth-format image (that needs depth2d<float>) — the read silently returns 0 instead of failing loudly. Every post-process pass sampling scene depth was affected; this file only fixes the lens flare path, but Fog2's independently tracked "dimmer than GL" issue is a likely second symptom of the same bug (noted in TODO.md for follow-up). Fix: store sceneDepthSampleImage as R32_SFLOAT with a COLOR_BIT view instead, matching the MSAA depth-resolve path, which already worked around this by resolving depth into an R32F color target. The depth-to-depth vkCmdCopyImage becomes a cross-aspect depth-to-color copy (valid since Vulkan 1.1 / VK_KHR_maintenance1); same 32-bit float bit pattern, no format conversion needed. Since depth is now a color image, hardware depth-compare (sampler2DShadow + VK_COMPARE_OP_LESS) is also invalid on it. Replaced with an equivalent in-shader `step(scanPos.z, depth)` compare in LensFlareScanner.vk.fs, and removed the now-unused depthShadowSampler. Softness still comes from the existing 3x Gauss blur, so visual output is unchanged. Also fixed while in this code path: the visibility blur pass called vkCmdDraw(6, ...) against PassThrough.vk.vs, a 3-vertex fullscreen triangle — rasterizing a spurious degenerate second triangle. Harmless under the blur pass's non-blended output, but wrong; corrected to vkCmdDraw(3, ...).
- Fog.vk.fs: early-out on near-vertical view rays instead of snapping dir.xy, which caused fragment-level flicker when looking straight down - FramebufferManager: prefer A2B10G10R10_UNORM for the scene FB even when r_highPrec=0 (hardware permitting), so linear shader output never lands in an 8-bit target without gamma encoding - Remove dead Water.frag/Water.vert (unreferenced, stale hardcoded sun) - Remove committed .spv blobs and gitignore them; CMake regenerates all SPIR-V and glslangValidator is already a hard build requirement - TODO.md: check off the four resolved items
- Add Gauss1DRGBA.vk.fs (RGBA port of the GL Gauss1D kernel; the existing Gauss1D.vk.fs is single-channel and reserved for the DoF CoC path) - VulkanBloomFilter: run the separable H+V gaussian on every downsample level, matching GLLensDustFilter; adds a dedicated gauss pipeline/layout and defers temp-image returns until recording completes - VulkanCavityOutlineFilter: promote the hardcoded edge strength and depth threshold to r_outlinesStrength (0-4, default 1) and r_outlinesDepthThreshold (0.001-1, default 0.05), read per frame - TODO.md: mark the bloom delta and outline-tuning items resolved
- BasicModelVertexColor.vert/frag: declare the set-1 shadow-sampling layout (cascadeMatrix UBO + three cascade depth maps, matching BasicMap), compute per-cascade light-clip coordinates from the world position, and multiply the model-on-model shadow term into the existing map-shadow factor - VulkanOptimizedVoxelModel: extend the shared lit pipeline layout to two descriptor sets (set 1 = the shadow map renderer's sampling layout) and bind the sampling set in the prerender and sunlight passes; the ghost pipelines draw from the same passes and their fragment shaders remain compatible with the wider layout - TODO.md: mark the model self-shadowing follow-up as done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR works through most of the outstanding Vulkan-vs-GL parity items — model self-shadowing, bloom/lens-flare/outline fixes, a MoltenVK depth-sampling bug, shadow map upload perf, and a couple of shading bleed fixes on map/model geometry. Also fixes several stale entries in TODO.md that no longer matched the code.
Rendering features
Model self-shadowing via shadow map cascades
BasicModelVertexColor.vert/fragnow sample the set-1 cascade shadow maps (same layout asBasicMap), computing per-cascade light-clip coords from world position and multiplying the result into the existing map-shadow term.VulkanOptimizedVoxelModelextends its shared lit pipeline layout to two descriptor sets and binds the shadow-sampling set in both the prerender and sunlight passes. Ghost pipelines share the same passes/layout, so no extra work needed there.Dynamic per-light lens flares (
r_lensFlareDynamic)VulkanLensFlareFilterno longer hardcodes the sun as the only flare source — sun + each dynamic light withuseLensFlarenow gets its ownFlareRequest(projection, push constants, scanZ), with per-light culling logic ported directly fromGLRenderer's dynamic-flare loop (distance attenuation, spotlight falloff, behind-camera rejection).VulkanRenderer::GetDynamicLights()added; the per-frame lights vector now survives until after the post-process chain instead of being cleared before the flare filter can read it (still cleared on dropped frames).Bloom parity with
GLLensDustFilterGauss1DRGBA.vk.fs(RGBA gaussian kernel; the existingGauss1D.vk.fsstays single-channel for DoF CoC).VulkanBloomFilternow runs separable H+V gaussian blur at every downsample level, matching GL.BloomComposite.vk.fsportsLensDust.fs:scene*0.95 + dust² * bloom*2 + grain*0.003, with per-frame film grain driven by a 128×128 RGBA8 noise texture refreshed viaSampleRandom().Camera motion blur (
VulkanCameraBlurFilter)GLCameraBlurFilter: rotation-only view diff vs. previous frame, camera-cut guard, log5 iteration count, radial-blur scaling — all replicated on the CPU side.r_cameraBlur > 0and!sceneDef.denyCameraBlur.Bicubic resample filter (
r_scaleFilter == 2)VulkanResampleBicubicFilter, a port of Danny Ruijters' 4-fetch bicubic B-spline upscale from the GL shader (license notice carried over). Skipped when render size already matches swapchain size, same as GL.r_scaleFilter == 0(nearest) instead of always linear-filtering.Outline tuning cvars
VulkanCavityOutlineFilter: hardcoded edge strength and depth threshold promoted tor_outlinesStrength(0–4, default 1) andr_outlinesDepthThreshold(0.001–1, default 0.05), read per frame.Bug fixes
Lens flares not rendering at all (MoltenVK)
sceneDepthSampleImagewasD32_SFLOATwith a depth-aspect view, sampled as plainsampler2D. MoltenVK maps that to Metal'stexture2d<float>, which silently returns 0 for depth-format images instead of erroring — so every visibility check failed.R32_SFLOATwith a color-aspect view (same trick already used on the MSAA depth-resolve path), and replace the now-invalid hardware depth-compare with an in-shaderstep()inLensFlareScanner.vk.fs. Visual output unchanged.vkCmdDraw(6, ...)against a 3-vertex fullscreen-triangle shader, rasterizing a spurious second triangle. Harmless but wrong; now draws 3 vertices.Fog nadir flicker + 8-bit framebuffer banding
Fog.vk.fs: early-out on near-vertical view rays instead of snappingdir.xy, fixing flicker when looking straight down.FramebufferManager: preferA2B10G10R10_UNORMfor the scene FB even withr_highPrec=0(hardware permitting), so linear output doesn't land in an 8-bit target without gamma encoding.Light bleed on map and model geometry
fixedPositionAttribute(face-center, already computed but never wired to the shaders) as vertex attribute 4 inBasicMap.vert/BasicMapPhys.vert, matching GL'sfixedPositionAttribute.BasicModelVertexColor.vert/BasicModelVertexColorPhys.vert.Shadow map upload performance
VulkanMapShadowRenderer::Updateno longer re-uploads the full 512×512 shadow bitmap on every block change. Dirty 32-texel words are coalesced into per-row spans and uploaded in one staging-buffer map/unmap + one multi-regionvkCmdCopyBufferToImage. Falls back to a full upload past a dirty/span threshold. Removes the perf cliff during heavy building.Housekeeping
Water.frag/Water.vert(unreferenced, stale hardcoded sun direction)..spvblobs and gitignored them going forward — CMake regenerates SPIR-V andglslangValidatoris already a hard build requirement.