From 1249e59f6faecccd79231d5cdc4d4f131661fb92 Mon Sep 17 00:00:00 2001 From: the-e Date: Sat, 15 Aug 2026 12:57:21 +0200 Subject: [PATCH 1/2] Fix Vulkan RT shadows on player cockpits and expand TLAS coverage The raytraced shadow TLAS included only ships, asteroids, and debris. It now also includes the player's cockpit model and OBJ_RAW_POF / OBJ_PROP objects, matching the object set the rasterized shadow path already covers. The viewer's own hull was self-shadowing the cockpit as solid green. Each TLAS instance now carries a ray-cull mask, and the viewer hull gets a dedicated mask bit so shadow rays can exclude it outside the cockpit's own shading pass. The cockpit render pass uses a camera-relative view frame, not true world space, so a ray reconstructed from it needs a world-space correction before tracing against the TLAS. shadow_cascade_params_bind() now takes this offset (and a self-shadow flag) per bind, so the hull and cockpit draws inside ship_render_player_ship() can each supply their own. Also: - Extract ship_player_cockpit_model_would_render() and ship_cockpit_render_offset() so the render, shadow, and TLAS paths share one definition of "is the cockpit rendering" and "where is it". - Add shadow_cascade_params_bind_deferred() to remove the duplicated offset/count selection between the GL and Vulkan deferred-lighting backends. - Bind the shadow-cascade uniform buffer with a full replace instead of an offset-append: this buffer is now bound several times per frame with different content, and an offset-append only bump-allocates on its first call per frame on Vulkan's streaming buffer. Co-Authored-By: Claude Sonnet 5 --- code/def_files/data/effects/deferred-f.sdr | 11 ++- code/def_files/data/effects/main-f.sdr | 12 ++- code/def_files/data/effects/main-v.sdr | 6 ++ code/def_files/data/effects/shadow_map-g.sdr | 6 ++ code/def_files/data/effects/shadow_map-v.sdr | 6 ++ code/def_files/data/effects/shadows.sdr | 10 +- code/graphics/opengl/gropengldeferred.cpp | 4 +- code/graphics/shadows.cpp | 43 ++++++--- code/graphics/shadows.h | 27 +++++- code/graphics/util/uniform_structs.h | 26 ++++++ .../vulkan/VulkanPostProcessingLighting.cpp | 5 +- code/graphics/vulkan/VulkanRaytracing.h | 24 ++++- code/graphics/vulkan/VulkanRaytracingTlas.cpp | 93 ++++++++++++++++--- code/object/objectsort.cpp | 2 +- code/ship/ship.cpp | 66 +++++++++---- code/ship/ship.h | 13 +++ 16 files changed, 296 insertions(+), 58 deletions(-) diff --git a/code/def_files/data/effects/deferred-f.sdr b/code/def_files/data/effects/deferred-f.sdr index fb5d9021ebd..b8702e9a434 100644 --- a/code/def_files/data/effects/deferred-f.sdr +++ b/code/def_files/data/effects/deferred-f.sdr @@ -65,6 +65,12 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // shadow_ray_cull_mask/shadow_ray_world_offset: see shadow_cascade_static_data + // (uniform_structs.h) for what these mean and shadow_cascade_params_bind() + // (shadows.cpp) for how they're set. + vec3 shadow_ray_world_offset; + int shadow_ray_cull_mask; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; @@ -310,12 +316,13 @@ void main() if (enable_shadows != 0) { #ifdef RT_SHADOWS - vec3 worldPos = (inv_view_matrix * vec4(position, 1.0)).xyz; + // Zero outside the cockpit pass; see shadow_cascade_static_data (uniform_structs.h). + vec3 worldPos = (inv_view_matrix * vec4(position, 1.0)).xyz + shadow_ray_world_offset; vec3 worldNormal = normalize((inv_view_matrix * vec4(normal, 0.0)).xyz); vec3 worldLightDir = normalize((inv_view_matrix * vec4(lightDir, 0.0)).xyz); float rtShadowBias = computeRtShadowBias(length(position), rtShadowBiasMin, rtShadowBiasMax); - attenuation *= traceShadowRay(shadow_tlas, worldPos, worldNormal, worldLightDir, lightDist, rtShadowBias); + attenuation *= traceShadowRay(shadow_tlas, worldPos, worldNormal, worldLightDir, lightDist, rtShadowBias, uint(shadow_ray_cull_mask)); #else vec4 fragShadowPos = shadow_mv_matrix * inv_view_matrix * vec4(position, 1.0); vec4 fragShadowUV[NUM_SHADOW_CASCADES]; diff --git a/code/def_files/data/effects/main-f.sdr b/code/def_files/data/effects/main-f.sdr index f8390b496fc..2829e2be796 100644 --- a/code/def_files/data/effects/main-f.sdr +++ b/code/def_files/data/effects/main-f.sdr @@ -103,6 +103,12 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // shadow_ray_cull_mask/shadow_ray_world_offset: see shadow_cascade_static_data + // (uniform_structs.h) for what these mean and shadow_cascade_params_bind() + // (shadows.cpp) for how they're set. + vec3 shadow_ray_world_offset; + int shadow_ray_cull_mask; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; @@ -237,7 +243,8 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, rtShadowsActive = true; #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_SHADOWS mat4 invView = inverse(viewMatrix); - vec3 worldPos = (invView * vertIn.position).xyz; + // Zero outside the cockpit pass; see shadow_cascade_static_data (uniform_structs.h). + vec3 worldPos = (invView * vertIn.position).xyz + shadow_ray_world_offset; vec3 worldNormal = normalize((invView * vec4(normal, 0.0)).xyz); int shadowedDirectionalCount = 0; #endif @@ -248,7 +255,7 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, if (rtShadowsActive && lights[i].light_type == LT_DIRECTIONAL && shadowedDirectionalCount < MAX_RT_SHADOW_LIGHTS) { vec3 worldSunDir = normalize((invView * vec4(lights[i].position.xyz, 0.0)).xyz); float rtShadowBias = computeRtShadowBias(length(vertIn.position.xyz), rtShadowBiasMin, rtShadowBiasMax); - shadow = traceShadowRay(shadow_tlas, worldPos, worldNormal, worldSunDir, RT_SHADOW_MAX_DISTANCE, rtShadowBias); + shadow = traceShadowRay(shadow_tlas, worldPos, worldNormal, worldSunDir, RT_SHADOW_MAX_DISTANCE, rtShadowBias, uint(shadow_ray_cull_mask)); ++shadowedDirectionalCount; } else { shadow = 1.0; @@ -270,6 +277,7 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, lightDiffuse += (lights[i].diffuse_color.rgb * diffuseFactor * NdotL * attenuation) * shadow; lightSpecular += lights[i].diffuse_color.rgb * computeLighting(specularMaterial, diffuseMaterial, lightDir, normal, halfVec, eyeDir, roughness, fresnel, NdotL) * attenuation * shadow; } + return diffuseMaterial * lightAmbient + lightSpecular; } diff --git a/code/def_files/data/effects/main-v.sdr b/code/def_files/data/effects/main-v.sdr index 7933ecf3578..d7639d84a55 100644 --- a/code/def_files/data/effects/main-v.sdr +++ b/code/def_files/data/effects/main-v.sdr @@ -117,6 +117,12 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // shadow_ray_cull_mask/shadow_ray_world_offset: see shadow_cascade_static_data + // (uniform_structs.h) for what these mean and shadow_cascade_params_bind() + // (shadows.cpp) for how they're set. + vec3 shadow_ray_world_offset; + int shadow_ray_cull_mask; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; diff --git a/code/def_files/data/effects/shadow_map-g.sdr b/code/def_files/data/effects/shadow_map-g.sdr index 585276dca69..75e370faeda 100644 --- a/code/def_files/data/effects/shadow_map-g.sdr +++ b/code/def_files/data/effects/shadow_map-g.sdr @@ -14,6 +14,12 @@ layout (std140) uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // shadow_ray_cull_mask/shadow_ray_world_offset: see shadow_cascade_static_data + // (uniform_structs.h) for what these mean and shadow_cascade_params_bind() + // (shadows.cpp) for how they're set. + vec3 shadow_ray_world_offset; + int shadow_ray_cull_mask; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; diff --git a/code/def_files/data/effects/shadow_map-v.sdr b/code/def_files/data/effects/shadow_map-v.sdr index f3e92cd21d1..ed416a4bb18 100644 --- a/code/def_files/data/effects/shadow_map-v.sdr +++ b/code/def_files/data/effects/shadow_map-v.sdr @@ -44,6 +44,12 @@ uniform shadowCascadeParams { float rtShadowBiasMax; mat4 shadow_mv_matrix; + + // shadow_ray_cull_mask/shadow_ray_world_offset: see shadow_cascade_static_data + // (uniform_structs.h) for what these mean and shadow_cascade_params_bind() + // (shadows.cpp) for how they're set. + vec3 shadow_ray_world_offset; + int shadow_ray_cull_mask; mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES]; vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4]; diff --git a/code/def_files/data/effects/shadows.sdr b/code/def_files/data/effects/shadows.sdr index b13e4cbf41c..b82937e139c 100644 --- a/code/def_files/data/effects/shadows.sdr +++ b/code/def_files/data/effects/shadows.sdr @@ -114,14 +114,20 @@ float computeRtShadowBias(float cameraDist, float biasMin, float biasMax) // // bias is a world-unit offset applied along worldNormal before tracing, to clear // the source triangle -- see computeRtShadowBias() above for how callers derive it. -float traceShadowRay(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNormal, vec3 worldLightDir, float tMax, float bias) +// +// cullMask selects which TLAS instances this ray can hit (Vulkan spec: an instance +// is a candidate iff (cullMask & instance.mask) != 0). Callers should pass +// shadow_ray_cull_mask (shadowCascadeParams uniform block) rather than a literal -- +// see SHADOW_RAY_CULL_MASK_EXCLUDE_VIEWER_HULL/TLAS_MASK_VIEWER_HULL (shadows.h) +// for how it excludes the viewer ship's own hull outside the cockpit pass. +float traceShadowRay(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNormal, vec3 worldLightDir, float tMax, float bias, uint cullMask) { vec3 origin = worldPos + worldNormal * bias; vec3 direction = normalize(worldLightDir); rayQueryEXT rq; rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT | gl_RayFlagsTerminateOnFirstHitEXT, - 0xFF, origin, 0.001, direction, tMax); + cullMask, origin, 0.001, direction, tMax); while (rayQueryProceedEXT(rq)) {} return (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) ? 1.0 : 0.0; diff --git a/code/graphics/opengl/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp index 35d67419977..a198c7274e6 100644 --- a/code/graphics/opengl/gropengldeferred.cpp +++ b/code/graphics/opengl/gropengldeferred.cpp @@ -316,9 +316,7 @@ void gr_opengl_deferred_lighting_finish() auto header = light_uniform_aligner.getHeader(); if (Shadow_quality != ShadowQuality::Disabled) { vm_inverse_matrix4(&header->inv_view_matrix, &Shadow_view_matrix_render); - int offset = (Lighting_mode == lighting_mode::COCKPIT) ? 0 : Num_cockpit_shadow_cascades; - int count = (Lighting_mode == lighting_mode::COCKPIT) ? Num_cockpit_shadow_cascades : Num_shadow_cascades; - shadow_cascade_params_bind(offset, count); + shadow_cascade_params_bind_deferred(); } header->invScreenWidth = 1.0f / gr_screen.max_w; diff --git a/code/graphics/shadows.cpp b/code/graphics/shadows.cpp index 82713c6c5db..6dfa331b448 100644 --- a/code/graphics/shadows.cpp +++ b/code/graphics/shadows.cpp @@ -665,11 +665,11 @@ matrix shadows_start_render(matrix *eye_orient, vec3d *eye_pos, fov_t fov, fov_t gr_shadow_map_start(&Shadow_view_matrix_light, &light_matrix, eye_pos, true); if (cascade_distances_override) - shadow_cascade_params_bind(max_skip_override, num_cascades - max_skip_override); + shadow_cascade_params_bind(max_skip_override, num_cascades - max_skip_override, vmd_zero_vector); else if (render_cockpit_cascades) - shadow_cascade_params_bind(0, num_cascades); + shadow_cascade_params_bind(0, num_cascades, vmd_zero_vector); else - shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades); + shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades, vmd_zero_vector); return light_matrix; } @@ -764,9 +764,9 @@ static void render_viewer_shadow(object* objp, const matrix* light_matrix, gr_shadow_map_start(&dummy_view, light_matrix, &vmd_zero_vector, false); if (casts_shadow_on_cockpit) - shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades + Num_shadow_cascades); + shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades + Num_shadow_cascades, vmd_zero_vector); else - shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades); + shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades, vmd_zero_vector); model_clear_instance(sip->model_num); polymodel_instance* pmi = nullptr; @@ -782,17 +782,14 @@ static void render_viewer_shadow(object* objp, const matrix* light_matrix, viewer_list.render_all(); } - const bool renderCockpitModel = (Viewer_mode != VM_TOPDOWN) && sip->cockpit_model_num >= 0 && !Disable_cockpits; + const bool renderCockpitModel = ship_player_cockpit_model_would_render(sip); if (renderCockpitModel && !Shadow_disable_overrides.disable_cockpit) { matrix4 dummy_view; gr_shadow_map_start(&dummy_view, light_matrix, &vmd_zero_vector, false); - shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades); + shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades, vmd_zero_vector); - vec3d cockpit_offset = sip->cockpit_offset; - vm_vec_unrotate(&cockpit_offset, &cockpit_offset, &objp->orient); - if (!Disable_cockpit_sway) - cockpit_offset += sip->cockpit_sway_val * objp->phys_info.acceleration; + vec3d cockpit_offset = ship_cockpit_render_offset(sip, objp); model_clear_instance(sip->cockpit_model_num); polymodel_instance* cockpit_pmi = nullptr; @@ -1035,7 +1032,8 @@ void shadow_cascade_params_shutdown() { } int Shadow_cascade_count = 0; -void shadow_cascade_params_bind(int cascade_offset, int cascade_count) { +void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const vec3d& world_offset, + bool allow_viewer_self_shadow) { if (!Shadow_cascade_params_buffer.isValid()) { return; } @@ -1058,6 +1056,10 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count) { static_data.rtShadowBiasMax = Rt_shadow_bias_max; static_data.shadow_mv_matrix = Shadow_view_matrix_light; + // See shadow_cascade_params_bind()'s declaration (shadows.h) for what these mean. + static_data.shadow_ray_cull_mask = allow_viewer_self_shadow ? 0xFF : SHADOW_RAY_CULL_MASK_EXCLUDE_VIEWER_HULL; + static_data.shadow_ray_world_offset = world_offset; + Shadow_cascade_count = cascade_count; offset += sizeof(graphics::shadow_cascade_static_data); @@ -1082,10 +1084,25 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count) { } offset += sizeof(float) * padding; - gr_update_buffer_data_offset(Shadow_cascade_params_buffer, 0, required_size, buffer.data()); + // Must be the full-replacement update, not _offset: this is called several times per + // frame with different content, and on Vulkan's streaming buffer _offset only bump- + // allocates fresh GPU memory on the first call, silently overwriting the same region + // on later calls -- so every draw this frame (command buffers are pre-recorded) would + // read whichever bind ran last, not the one current when it was recorded. This always + // bump-allocates fresh on Vulkan (matching deferred_global_data/deferred_light_data) + // and is a plain buffer-orphaning glBufferData() on OpenGL. + gr_update_buffer_data(Shadow_cascade_params_buffer, required_size, buffer.data()); gr_bind_uniform_buffer(uniform_block_type::ShadowCascadeParams, 0, required_size, Shadow_cascade_params_buffer); } +void shadow_cascade_params_bind_deferred() { + int offset = (Lighting_mode == lighting_mode::COCKPIT) ? 0 : Num_cockpit_shadow_cascades; + int count = (Lighting_mode == lighting_mode::COCKPIT) ? Num_cockpit_shadow_cascades : Num_shadow_cascades; + vec3d world_offset = (Lighting_mode == lighting_mode::COCKPIT && Viewer_obj != nullptr) ? Viewer_obj->pos : vmd_zero_vector; + bool allow_viewer_self_shadow = Lighting_mode == lighting_mode::COCKPIT && ship_render_player_ship_casts_shadow_on_cockpit(); + shadow_cascade_params_bind(offset, count, world_offset, allow_viewer_self_shadow); +} + shadow_render_list::shadow_render_list() { reset(); } diff --git a/code/graphics/shadows.h b/code/graphics/shadows.h index 63a3abd3258..317730f3d6e 100644 --- a/code/graphics/shadows.h +++ b/code/graphics/shadows.h @@ -95,6 +95,15 @@ extern SCP_vector Shadow_proj_matrix; extern SCP_vector Shadow_cascade_distances; extern int Shadow_cascade_count; +// TLAS ray-cull mask bit reserved for the viewer ship's own hull instance +// (VulkanRaytracingManager::gatherShadowCasterInstances()'s OBJ_SHIP case), so shadow +// rays can selectively exclude it -- see shadow_cascade_params_bind()'s +// shadow_ray_cull_mask and traceShadowRay() (shadows.sdr). +constexpr uint8_t TLAS_MASK_VIEWER_HULL = 0x80; +// Default TLAS instance mask (visible to every ray) minus TLAS_MASK_VIEWER_HULL -- +// the shadow_ray_cull_mask used everywhere the viewer's own hull must not self-shadow. +constexpr uint8_t SHADOW_RAY_CULL_MASK_EXCLUDE_VIEWER_HULL = static_cast(~TLAS_MASK_VIEWER_HULL); + void shadows_construct_light_frustum(vec3d *min_out, vec3d *max_out, vec3d light_vec, matrix *orient, vec3d *pos, fov_t fov, float aspect, float z_near, float z_far); bool shadows_obj_in_frustum(object *objp, vec3d *min, vec3d *max, matrix *light_orient); void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos, @@ -102,7 +111,23 @@ void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos, void shadow_cascade_params_init(); void shadow_cascade_params_shutdown(); -void shadow_cascade_params_bind(int cascade_offset, int cascade_count); + +// world_offset: added to traceShadowRay()'s reconstructed world position before tracing. +// Zero for every pass rendering in true world space; nonzero only for the cockpit pass +// (see shadow_cascade_static_data::shadow_ray_world_offset, uniform_structs.h, for the +// derivation). A pass with draws in more than one internal frame -- e.g. +// ship_render_player_ship()'s hull vs. cockpit draws -- must rebind before each one. +// allow_viewer_self_shadow: lets the viewer ship's own hull (TLAS_MASK_VIEWER_HULL, +// shadows.h) cast shadows in this pass. True only for the cockpit's own shading. +void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const vec3d& world_offset, + bool allow_viewer_self_shadow = false); + +// Binds the cascade range/world_offset/self-shadow-mask appropriate for the current +// Lighting_mode, for callers that (unlike ship_render_player_ship()) only ever shade +// one frame per pass -- currently the deferred-lighting full-screen passes +// (gropengldeferred.cpp, VulkanPostProcessingLighting.cpp). See shadow_cascade_params_bind() +// above for what world_offset/allow_viewer_self_shadow mean and their known limitation here. +void shadow_cascade_params_bind_deferred(); matrix shadows_start_render(matrix *eye_orient, vec3d *eye_pos, fov_t fov, fov_t cockpit_fov, float aspect, const std::optional>& cascade_distances_override = std::nullopt); void shadows_end_render(); diff --git a/code/graphics/util/uniform_structs.h b/code/graphics/util/uniform_structs.h index 974ad275732..638b3d1a6a5 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -147,7 +147,33 @@ struct shadow_cascade_static_data { float rtShadowBiasMin; float rtShadowBiasMax; matrix4 shadow_mv_matrix; + + // World-space correction added to the RT shadow ray's reconstructed position before + // tracing. Zero everywhere except the cockpit pass: ship_render_player_ship() renders + // the cockpit using a view matrix anchored at `leaning_position` and model positions + // offset in that same un-translated frame (never combined with objp->pos), so the + // reconstructed position isn't true world space like the shadow TLAS is. Adding the + // viewer ship's world position back here closes that gap -- see + // shadow_cascade_params_bind() (shadows.cpp) for where this is set. + // + // Declared before shadow_ray_cull_mask so the trailing int packs into this vec3's + // std140 alignment padding instead of needing a manual pad field (std140 gives a vec3 + // 16-byte alignment but only 12-byte size, so a following scalar fills the gap for free). + vec3d shadow_ray_world_offset; + + // Ray cull mask for raytraced shadow queries (traceShadowRay()/shadows.sdr). Excludes + // the viewer ship's own hull TLAS instance (TLAS_MASK_VIEWER_HULL, shadows.h) outside + // the cockpit pass, matching the rasterized path's exclusion of Viewer_obj from the + // main shadow cascades. Set in shadow_cascade_params_bind() (shadows.cpp). + int shadow_ray_cull_mask; }; +// Must match the GLSL shadowCascadeParams block's implicit std140 padding +// exactly (16 [4 leading scalars] + 64 [matrix4] + 12 [shadow_ray_world_offset] +// + 4 [shadow_ray_cull_mask, packed into the vec3's alignment padding] = 96) -- +// shadow_cascade_params_bind() packs shadow_proj_matrix[] immediately after +// this struct via sizeof(), so a mismatch here silently shifts every cascade +// matrix in the buffer, in both backends. +static_assert(sizeof(shadow_cascade_static_data) == 96, "shadow_cascade_static_data must match the GLSL shadowCascadeParams layout (see comment above)"); enum class NanoVGShaderType: int32_t { FillGradient = 0, FillImage = 1, Simple = 2, Image = 3 diff --git a/code/graphics/vulkan/VulkanPostProcessingLighting.cpp b/code/graphics/vulkan/VulkanPostProcessingLighting.cpp index 08dda92663c..e9ce9dde7f9 100644 --- a/code/graphics/vulkan/VulkanPostProcessingLighting.cpp +++ b/code/graphics/vulkan/VulkanPostProcessingLighting.cpp @@ -507,10 +507,7 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd) if (m_shadow->isInitialized() && Shadow_quality != ShadowQuality::Disabled) { vm_inverse_matrix4(&header->inv_view_matrix, &Shadow_view_matrix_render); - - int offset = (Lighting_mode == lighting_mode::COCKPIT) ? 0 : Num_cockpit_shadow_cascades; - int count = (Lighting_mode == lighting_mode::COCKPIT) ? Num_cockpit_shadow_cascades : Num_shadow_cascades; - shadow_cascade_params_bind(offset, count); + shadow_cascade_params_bind_deferred(); } } diff --git a/code/graphics/vulkan/VulkanRaytracing.h b/code/graphics/vulkan/VulkanRaytracing.h index 45fe2b22e1c..7023552c81d 100644 --- a/code/graphics/vulkan/VulkanRaytracing.h +++ b/code/graphics/vulkan/VulkanRaytracing.h @@ -192,23 +192,41 @@ class VulkanRaytracingManager { // Shared by walkSubmodelTree/addSingleSubmodelInstance: appends one TLAS // instance referencing blasAddress, placed at the given world orient/pos. + // `mask` is the instance's ray-cull mask (vk::AccelerationStructureInstanceKHR::mask, + // see TLAS_MASK_VIEWER_HULL in shadows.h); defaults to 0xFF (visible to every ray). static void pushInstance(SCP_vector& instances, vk::DeviceAddress blasAddress, const matrix& orient, - const vec3d& pos); + const vec3d& pos, + uint8_t mask = 0xFF); void gatherShadowCasterInstances(SCP_vector& instances); + // Adds one instance walk for Viewer_obj's cockpit polymodel (sip->cockpit_model_num), + // which -- unlike ships/asteroids/debris -- has no backing `object` for + // gatherShadowCasterInstances() to discover it through. Mirrors the gating + // render_viewer_shadow()'s cockpit block uses (shadows.cpp) so the cockpit only + // gets a TLAS instance when it would also get a rasterized shadow-map pass. + void gatherCockpitShadowCasterInstance(SCP_vector& instances); + // `skipDetailBoxCheck`: the detail-box gate (submodelPassesDetailBox()) compares + // against the global `Eye_position`, which isn't correct for the cockpit model (its + // rasterized detail-box checks use a cockpit-relative eye position instead -- see + // shadows.cpp). gatherCockpitShadowCasterInstance() passes true to skip the check + // rather than derive that; cockpit models are small and sit right against the camera, + // so detail-box culling is unlikely to matter there. Every other caller defaults false. void walkSubmodelTree(SCP_vector& instances, transform_stack& stack, const polymodel* pm, const polymodel_instance* pmi, - int submodel_num); + int submodel_num, + uint8_t mask = 0xFF, + bool skipDetailBoxCheck = false); void addSingleSubmodelInstance(SCP_vector& instances, const polymodel* pm, const polymodel_instance* pmi, int submodel_num, const matrix& orient, - const vec3d& pos); + const vec3d& pos, + uint8_t mask = 0xFF); // One full set of grow-only TLAS resources per frame-in-flight slot, indexed // by currentFrameIndex() -- NOT a single shared instance. buildTlas() diff --git a/code/graphics/vulkan/VulkanRaytracingTlas.cpp b/code/graphics/vulkan/VulkanRaytracingTlas.cpp index d1ba33a9878..29278110868 100644 --- a/code/graphics/vulkan/VulkanRaytracingTlas.cpp +++ b/code/graphics/vulkan/VulkanRaytracingTlas.cpp @@ -1,5 +1,6 @@ // Per-frame TLAS: gathers the current shadow-casting object set (ships, -// asteroids, debris) into one top-level acceleration structure each frame. +// asteroids, debris, raw POFs/props) into one top-level acceleration +// structure each frame. // Split out from VulkanRaytracing.cpp since this is the only part of the // raytracing manager that reaches into Ships/Asteroids/Debris/Objects, a // distinctly different dependency set from the BLAS cache in @@ -14,6 +15,9 @@ #include "asteroid/asteroid.h" #include "debris/debris.h" +#include "globalincs/systemvars.h" +#include "graphics/shadows.h" +#include "mod_table/mod_table.h" #include "model/model.h" #include "model/modelrender.h" #include "object/object.h" @@ -40,12 +44,13 @@ static bool submodelPassesDetailBox(const polymodel* pm, int submodel_num, const void VulkanRaytracingManager::pushInstance(SCP_vector& instances, vk::DeviceAddress blasAddress, const matrix& orient, - const vec3d& pos) + const vec3d& pos, + uint8_t mask) { vk::AccelerationStructureInstanceKHR instance; instance.transform = toVkTransform(orient, pos); instance.instanceCustomIndex = 0; - instance.mask = 0xFF; + instance.mask = mask; instance.instanceShaderBindingTableRecordOffset = 0; instance.flags = static_cast(vk::GeometryInstanceFlagBitsKHR::eTriangleFacingCullDisable); instance.accelerationStructureReference = blasAddress; @@ -57,7 +62,8 @@ void VulkanRaytracingManager::addSingleSubmodelInstance(SCP_vector= pm->n_models) { return; @@ -80,14 +86,16 @@ void VulkanRaytracingManager::addSingleSubmodelInstance(SCP_vectoraddress, orient, pos); + pushInstance(instances, entry->address, orient, pos, mask); } void VulkanRaytracingManager::walkSubmodelTree(SCP_vector& instances, transform_stack& stack, const polymodel* pm, const polymodel_instance* pmi, - int submodel_num) + int submodel_num, + uint8_t mask, + bool skipDetailBoxCheck) { if (submodel_num < 0 || submodel_num >= pm->n_models) { return; @@ -135,19 +143,19 @@ void VulkanRaytracingManager::walkSubmodelTree(SCP_vectorid, submodel_num); if (entry != nullptr) { - pushInstance(instances, entry->address, world_orient, world_pos); + pushInstance(instances, entry->address, world_orient, world_pos, mask); } for (int child = sm.first_child; child >= 0; child = pm->submodel[child].next_sibling) { if (!pm->submodel[child].flags[Model::Submodel_flags::Is_thruster]) { - walkSubmodelTree(instances, stack, pm, pmi, child); + walkSubmodelTree(instances, stack, pm, pmi, child, mask, skipDetailBoxCheck); } } @@ -157,7 +165,7 @@ void VulkanRaytracingManager::walkSubmodelTree(SCP_vector& instances) { // Mirrors the object selection in shadows_render_all() (shadows.cpp) -- - // ships/asteroids/debris -- but without its per-cascade frustum + // ships/asteroids/debris/raw POFs/props -- but without its per-cascade frustum // pre-filter, which lives in shadows.cpp's private state (Shadow_frustums) // and is specific to the rasterized cascade layout. Starting unfiltered is // simpler and safe (never wrongly excludes a caster); spatial culling of @@ -187,9 +195,15 @@ void VulkanRaytracingManager::gatherShadowCasterInstances(SCP_vectorpos, &objp->orient); - walkSubmodelTree(instances, stack, pm, pmi, pm->detail[0]); + walkSubmodelTree(instances, stack, pm, pmi, pm->detail[0], instanceMask); break; } case OBJ_ASTEROID: { @@ -224,12 +238,68 @@ void VulkanRaytracingManager::gatherShadowCasterInstances(SCP_vectororient, objp->pos); break; } + case OBJ_RAW_POF: + case OBJ_PROP: { + // Mirrors shadows.cpp's OBJ_RAW_POF/OBJ_PROP case: same shape as + // OBJ_SHIP (a polymodel + optional polymodel_instance), just + // resolved generically instead of via ship_info/Ships[]. + int model_num = object_get_model_num(objp); + polymodel* pm = model_get(model_num); + if (pm == nullptr || pm->detail[0] < 0) { + continue; + } + + int instance_num = object_get_model_instance_num(objp); + polymodel_instance* pmi = instance_num < 0 ? nullptr : model_get_instance(instance_num); + + transform_stack stack; + stack.push(&objp->pos, &objp->orient); + walkSubmodelTree(instances, stack, pm, pmi, pm->detail[0]); + break; + } default: break; } } } +void VulkanRaytracingManager::gatherCockpitShadowCasterInstance(SCP_vector& instances) +{ + object* objp = Viewer_obj; + if (objp == nullptr || objp->type != OBJ_SHIP || objp->instance < 0) { + return; + } + + ship* shipp = &Ships[objp->instance]; + ship_info* sip = &Ship_info[shipp->ship_info_index]; + + if (!ship_player_cockpit_model_would_render(sip) || Shadow_disable_overrides.disable_cockpit) { + return; // matches render_viewer_shadow()'s gate on the rasterized cockpit shadow pass (shadows.cpp) + } + + polymodel* cockpit_pm = model_get(sip->cockpit_model_num); + if (cockpit_pm == nullptr || cockpit_pm->detail[0] < 0) { + return; + } + polymodel_instance* cockpit_pmi = + shipp->cockpit_model_instance < 0 ? nullptr : model_get_instance(shipp->cockpit_model_instance); + + // World-space anchor for the cockpit model. ship_cockpit_render_offset() (ship.h) + // gives the same rotated+swayed offset ship_render_player_ship() draws the cockpit + // with, but that offset is camera-relative there (never combined with objp->pos) -- + // a rendering convenience specific to the rasterized forward-draw call. The TLAS is + // a persistent world-space structure (gatherShadowCasterInstances' OBJ_SHIP case + // anchors at true objp->pos/objp->orient, same as here), so adding it onto objp->pos + // here is what anchors the cockpit instance the same way. + vec3d cockpit_render_offset = ship_cockpit_render_offset(sip, objp); + vec3d cockpit_world_pos = objp->pos; + vm_vec_add2(&cockpit_world_pos, &cockpit_render_offset); + + transform_stack stack; + stack.push(&cockpit_world_pos, &objp->orient); + walkSubmodelTree(instances, stack, cockpit_pm, cockpit_pmi, cockpit_pm->detail[0], /* mask */ 0xFF, /* skipDetailBoxCheck */ true); +} + bool VulkanRaytracingManager::ensureInstanceCapacity(FrameTlasResources& frame, vk::DeviceSize requiredBytes) { if (requiredBytes <= frame.instanceCapacity) { @@ -364,6 +434,7 @@ void VulkanRaytracingManager::buildTlas() SCP_vector instances; gatherShadowCasterInstances(instances); + gatherCockpitShadowCasterInstance(instances); if (instances.empty()) { return; // keep whatever TLAS (if any) was built last time this slot was used diff --git a/code/object/objectsort.cpp b/code/object/objectsort.cpp index 7629f6fe3ef..40097ba4e6f 100644 --- a/code/object/objectsort.cpp +++ b/code/object/objectsort.cpp @@ -371,7 +371,7 @@ void obj_render_queue_all() scene.init_render(); if (Shadow_quality != ShadowQuality::Disabled) { - shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades); + shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades, vmd_zero_vector); } scene.render_all(ZBUFFER_TYPE_FULL); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 5e20b90d07e..ff826511582 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -8119,6 +8119,19 @@ static bool ship_render_player_renderShipModel(const ship_info* sip) { && (!Viewer_mode || (Viewer_mode & VM_PADLOCK_ANY) || (Viewer_mode & VM_OTHER_SHIP) || (Viewer_mode & VM_TRACK) || !(Viewer_mode & VM_EXTERNAL)); } +bool ship_player_cockpit_model_would_render(const ship_info* sip) { + return (Viewer_mode != VM_TOPDOWN) && sip->cockpit_model_num >= 0 && !Disable_cockpits; +} + +vec3d ship_cockpit_render_offset(const ship_info* sip, const object* objp) { + vec3d offset; + vm_vec_unrotate(&offset, &sip->cockpit_offset, &objp->orient); + if (!Disable_cockpit_sway) { + offset += sip->cockpit_sway_val * objp->phys_info.acceleration; + } + return offset; +} + bool ship_render_player_ship_casts_shadow_on_cockpit() { if (Viewer_obj == nullptr) return false; @@ -8146,9 +8159,7 @@ bool ship_render_player_has_closeup_visuals() { ship* shipp = &Ships[Viewer_obj->instance]; ship_info* sip = &Ship_info[shipp->ship_info_index]; - const bool hasCockpitModel = sip->cockpit_model_num >= 0; - - const bool renderCockpitModel = (Viewer_mode != VM_TOPDOWN) && hasCockpitModel && !Disable_cockpits; + const bool renderCockpitModel = ship_player_cockpit_model_would_render(sip); const bool renderShipModel = ship_render_player_renderShipModel(sip); return renderCockpitModel || renderShipModel; @@ -8161,7 +8172,7 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix const bool hasCockpitModel = sip->cockpit_model_num >= 0; - const bool renderCockpitModel = (Viewer_mode != VM_TOPDOWN) && hasCockpitModel && !Disable_cockpits; + const bool renderCockpitModel = ship_player_cockpit_model_would_render(sip); const bool renderShipModel = ship_render_player_renderShipModel(sip); Cockpit_active = renderCockpitModel; @@ -8252,14 +8263,42 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix Shadow_view_matrix_render = gr_view_matrix; matrix4 shadow_view_light_backup = Shadow_view_matrix_light; + bool cockpit_shadow_rendering_active = false; if (shadow_maybe_start_frame(Shadow_disable_overrides.disable_cockpit)) { Shadow_override = false; Shadow_view_matrix_light.a1d[12] = 0; Shadow_view_matrix_light.a1d[13] = 0; Shadow_view_matrix_light.a1d[14] = 0; - shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades); + cockpit_shadow_rendering_active = true; } + // Hull and cockpit render in different internal frames (see the &eye_offset vs. + // &cockpit_offset draws below), so each needs its own RT shadow-ray world-space + // correction -- see shadow_cascade_static_data::shadow_ray_world_offset + // (uniform_structs.h) for the derivation. + vec3d hull_shadow_ray_world_offset; + vm_vec_sub(&hull_shadow_ray_world_offset, &objp->pos, &eye_offset); + const vec3d& cockpit_shadow_ray_world_offset = objp->pos; + const bool cockpit_shadow_allows_hull_self_shadow = ship_render_player_ship_casts_shadow_on_cockpit(); + + model_render_params ship_render_info; + model_render_params cockpit_render_info; + vec3d cockpit_offset = vmd_zero_vector; + + auto renderHull = [&](int render_pass) { + if (cockpit_shadow_rendering_active) { + shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades, hull_shadow_ray_world_offset); + } + model_render_immediate(&ship_render_info, sip->model_num, shipp->model_instance_num, &objp->orient, &eye_offset, render_pass); + }; + auto renderCockpit = [&](int render_pass) { + if (cockpit_shadow_rendering_active) { + shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades, cockpit_shadow_ray_world_offset, + cockpit_shadow_allows_hull_self_shadow); + } + model_render_immediate(&cockpit_render_info, sip->cockpit_model_num, shipp->cockpit_model_instance, &objp->orient, &cockpit_offset, render_pass); + }; + if (light_deferredcockpit_enabled()) { gr_deferred_lighting_begin(true); @@ -8276,10 +8315,6 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix render_flags |= MR_NO_GLOWMAPS; } - model_render_params ship_render_info; - model_render_params cockpit_render_info; - vec3d cockpit_offset = sip->cockpit_offset; - //Properly render ship and cockpit model if (deferredRenderShipModel) { ship_render_info.set_detail_level_lock(0); @@ -8289,17 +8324,16 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix if (sip->uses_team_colors) ship_render_info.set_team_color(shipp->team_name, shipp->secondary_team_name, 0, 0); - model_render_immediate(&ship_render_info, sip->model_num, shipp->model_instance_num, &objp->orient, &eye_offset, MODEL_RENDER_OPAQUE); + renderHull(MODEL_RENDER_OPAQUE); gr_zbuffer_clear(true); } if (renderCockpitModel) { cockpit_render_info.set_detail_level_lock(0); cockpit_render_info.set_flags(render_flags); cockpit_render_info.set_replacement_textures(Player_cockpit_textures); - vm_vec_unrotate(&cockpit_offset, &cockpit_offset, &objp->orient); - if (!Disable_cockpit_sway) - cockpit_offset += sip->cockpit_sway_val * objp->phys_info.acceleration; - model_render_immediate(&cockpit_render_info, sip->cockpit_model_num, shipp->cockpit_model_instance, &objp->orient, &cockpit_offset, MODEL_RENDER_OPAQUE); + cockpit_offset = ship_cockpit_render_offset(sip, objp); + + renderCockpit(MODEL_RENDER_OPAQUE); } if (light_deferredcockpit_enabled()) { @@ -8327,11 +8361,11 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix gr_zbuffer_set(ZBUFFER_TYPE_READ); if (deferredRenderShipModel) { - model_render_immediate(&ship_render_info, sip->model_num, shipp->model_instance_num, &objp->orient, &eye_offset, MODEL_RENDER_TRANS); + renderHull(MODEL_RENDER_TRANS); } if (renderCockpitModel) { - model_render_immediate(&cockpit_render_info, sip->cockpit_model_num, shipp->cockpit_model_instance, &objp->orient, &cockpit_offset, MODEL_RENDER_TRANS); + renderCockpit(MODEL_RENDER_TRANS); } if (light_deferredcockpit_enabled()) { diff --git a/code/ship/ship.h b/code/ship/ship.h index 1d0c40112e1..7d857931329 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1780,6 +1780,19 @@ extern void ship_process_post( object * objp, float frametime ); extern void ship_render( object * obj, model_draw_list * scene ); extern bool ship_render_player_ship_casts_shadow_on_cockpit(); extern bool ship_render_player_has_closeup_visuals(); +// True if the player ship's cockpit model would be rendered right now (viewer mode, +// cockpit-model presence, and the Disable_cockpits override). Shared by every path that +// needs to know whether a cockpit is currently on-screen: ship_render_player_ship(), +// render_viewer_shadow() (shadows.cpp), and gatherCockpitShadowCasterInstance() +// (VulkanRaytracingTlas.cpp). +extern bool ship_player_cockpit_model_would_render(const ship_info* sip); +// The cockpit model's render offset: sip->cockpit_offset rotated into world orientation, +// plus acceleration-driven sway (unless Disable_cockpit_sway). This is the camera-relative +// offset used to draw the cockpit model, NOT combined with objp->pos -- callers that need +// a world-space position must add objp->pos themselves. Shared by every path that draws or +// otherwise anchors the cockpit model: ship_render_player_ship(), render_viewer_shadow() +// (shadows.cpp), and gatherCockpitShadowCasterInstance() (VulkanRaytracingTlas.cpp). +extern vec3d ship_cockpit_render_offset(const ship_info* sip, const object* objp); extern void ship_render_player_ship( object * objp, const vec3d* offset = nullptr, const matrix* rot_offset = nullptr, const fov_t* fov_override = nullptr); extern void ship_delete( object * objp ); extern int ship_check_collision_fast( object * obj, object * other_obj, vec3d * hitpos ); From 580c0761b349e85f902ac4a11907a2de20e59eb1 Mon Sep 17 00:00:00 2001 From: the-e Date: Sat, 15 Aug 2026 12:57:30 +0200 Subject: [PATCH 2/2] Isolate cockpit depth from scene depth in Vulkan cockpit rendering The cockpit model renders around the camera, inside the ship hull the scene pass already drew. OpenGL isolates the cockpit's depth from the scene's by swapping the depth attachment to a separate texture for the cockpit pass. The Vulkan backend left gr_post_process_save_zbuffer() and gr_post_process_restore_zbuffer() as no-ops, so the cockpit shared the scene's depth buffer and the hull incorrectly occluded it. A Vulkan framebuffer owns its attachments, so it cannot swap them like OpenGL does. Instead, VulkanPostProcessor::saveSceneDepth() copies the scene depth into a new backup image and the live depth buffer is cleared for the cockpit; restoreSceneDepth() copies the backup back so later passes that sample scene depth (for example lightshafts) see the scene, not the cockpit. VulkanRenderer::saveSceneDepth()/ restoreSceneDepth() end and resume the active render pass around each copy. Co-Authored-By: Claude Sonnet 5 --- code/graphics/vulkan/VulkanPostProcessing.cpp | 81 ++++++++++++++++--- code/graphics/vulkan/VulkanPostProcessing.h | 26 ++++++ code/graphics/vulkan/VulkanRenderer.h | 25 ++++++ code/graphics/vulkan/VulkanRendererLoop.cpp | 70 +++++++++++++++- code/graphics/vulkan/VulkanRendererSetup.cpp | 1 + 5 files changed, 191 insertions(+), 12 deletions(-) diff --git a/code/graphics/vulkan/VulkanPostProcessing.cpp b/code/graphics/vulkan/VulkanPostProcessing.cpp index 13d13675437..813636cc707 100644 --- a/code/graphics/vulkan/VulkanPostProcessing.cpp +++ b/code/graphics/vulkan/VulkanPostProcessing.cpp @@ -425,10 +425,12 @@ bool VulkanPostProcessor::createSceneTargets(vk::Extent2D extent) m_sceneColor.height = extent.height; // Scene depth target + // eTransferDst needed for restoreSceneDepth (backup→depth copy after the cockpit render) if (!createImage(extent.width, extent.height, m_ctx.depthFormat, vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled - | vk::ImageUsageFlagBits::eTransferSrc, + | vk::ImageUsageFlagBits::eTransferSrc + | vk::ImageUsageFlagBits::eTransferDst, vk::ImageAspectFlagBits::eDepth, // View uses depth-only aspect m_sceneDepth.image, m_sceneDepth.view, m_sceneDepth.allocation)) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene depth image!\n")); @@ -463,6 +465,22 @@ bool VulkanPostProcessor::createSceneTargets(vk::Extent2D extent) m_sceneDepthCopy.width = extent.width; m_sceneDepthCopy.height = extent.height; + // Scene depth backup (holds the scene's depth while the cockpit renders) + // Copy target on the way out, copy source on the way back in. No shader ever reads it, + // but eSampled stays: createImage() always builds a view, and a view needs at least one + // non-transfer usage bit (VUID-VkImageViewCreateInfo-image-04441). + if (!createImage(extent.width, extent.height, m_ctx.depthFormat, + vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eTransferSrc + | vk::ImageUsageFlagBits::eSampled, + vk::ImageAspectFlagBits::eDepth, + m_sceneDepthBackup.image, m_sceneDepthBackup.view, m_sceneDepthBackup.allocation)) { + nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene depth backup image!\n")); + return false; + } + m_sceneDepthBackup.format = m_ctx.depthFormat; + m_sceneDepthBackup.width = extent.width; + m_sceneDepthBackup.height = extent.height; + return true; } @@ -476,6 +494,7 @@ void VulkanPostProcessor::destroySceneTargets() m_ctx.destroyTarget(m_sceneColor); m_ctx.destroyTarget(m_sceneDepth); m_ctx.destroyTarget(m_sceneDepthCopy); + m_ctx.destroyTarget(m_sceneDepthBackup); } bool VulkanPostProcessor::createSceneFramebuffer() @@ -581,6 +600,29 @@ void VulkanPostProcessor::copySceneDepth(vk::CommandBuffer cmd) const imageAspectFromFormat(m_ctx.depthFormat)); } +void VulkanPostProcessor::saveSceneDepth(vk::CommandBuffer cmd) const +{ + // Called outside a render pass, so scene depth is in eDepthStencilAttachmentOptimal + // (the scene/G-buffer render pass finalLayout). The backup keeps no content between + // frames, hence eUndefined: the copy overwrites every texel. + copyImageToImage(cmd, + m_sceneDepth.image, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal, + m_sceneDepthBackup.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferSrcOptimal, + m_ctx.sceneExtent, + imageAspectFromFormat(m_ctx.depthFormat)); +} + +void VulkanPostProcessor::restoreSceneDepth(vk::CommandBuffer cmd) const +{ + // The backup was left in eTransferSrcOptimal by saveSceneDepth() and stays there -- + // the next save transitions it from eUndefined anyway. + copyImageToImage(cmd, + m_sceneDepthBackup.image, vk::ImageLayout::eTransferSrcOptimal, vk::ImageLayout::eTransferSrcOptimal, + m_sceneDepth.image, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal, + m_ctx.sceneExtent, + imageAspectFromFormat(m_ctx.depthFormat)); +} + void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd) { // C8: unlike PostProcessContext::drawFullscreenTriangle (which begins and ends @@ -822,15 +864,34 @@ void VulkanPostProcessor::encodeOutputSdr(vk::CommandBuffer cmd, vk::RenderPass void vulkan_post_process_begin() {} void vulkan_post_process_end() {} -// No-op: In OpenGL, save/restore swap the depth attachment between -// Scene_depth_texture and Cockpit_depth_texture to isolate cockpit -// depth from the main scene. In Vulkan, the render pass loadOp=eClear -// clears depth at the start of each scene pass, and separate cockpit -// depth isolation is not yet implemented. Called from ship.cpp during -// cockpit rendering but degrades gracefully as a no-op (cockpit just -// shares the scene depth buffer). -void vulkan_post_process_save_zbuffer() {} -void vulkan_post_process_restore_zbuffer() {} +// Isolate the cockpit's depth from the scene's, the way OpenGL does by swapping the +// depth attachment between Scene_depth_texture and Cockpit_depth_texture. A Vulkan +// framebuffer owns its attachments, so the scene depth is parked in a backup image and +// the live depth buffer is cleared for the cockpit; the restore puts the scene back. +// +// Both halves are needed. Without the clear the ship hull the scene already drew wins +// the depth test against the cockpit around the camera. Without the restore the +// post-processing passes that sample scene depth (lightshafts) see cockpit depth. +void vulkan_post_process_save_zbuffer() +{ + auto* renderer = getRendererInstance(); + if (renderer != nullptr) { + renderer->saveSceneDepth(); + } + + // Unconditional, exactly as in OpenGL: when there is nothing to park (post-processing + // off, or no scene render pass), clearing is still what gives the cockpit a depth + // buffer of its own. + gr_zbuffer_clear(TRUE); +} + +void vulkan_post_process_restore_zbuffer() +{ + auto* renderer = getRendererInstance(); + if (renderer != nullptr) { + renderer->restoreSceneDepth(); + } +} void vulkan_post_process_set_effect(const char* name, int value, const vec3d* rgb) { diff --git a/code/graphics/vulkan/VulkanPostProcessing.h b/code/graphics/vulkan/VulkanPostProcessing.h index b66867a0676..20748afa160 100644 --- a/code/graphics/vulkan/VulkanPostProcessing.h +++ b/code/graphics/vulkan/VulkanPostProcessing.h @@ -990,6 +990,31 @@ class VulkanPostProcessor { */ void copySceneDepth(vk::CommandBuffer cmd) const; + /** + * @brief Copy scene depth aside so the cockpit can render on a cleared depth buffer + * + * The cockpit model shares the world with the ship hull around it, so it needs a + * depth buffer of its own -- OpenGL gets one by swapping the depth attachment to + * Cockpit_depth_texture. Here the scene depth stays the attachment and its content + * is parked in a backup image instead, which restoreSceneDepth() puts back. + * + * Must be called outside a render pass. Leaves scene depth in + * eDepthStencilAttachmentOptimal and the backup in eTransferSrcOptimal. + * + * @param cmd Active command buffer (must be outside a render pass) + */ + void saveSceneDepth(vk::CommandBuffer cmd) const; + + /** + * @brief Put the depth that saveSceneDepth() parked back into the scene depth buffer + * + * Discards whatever the cockpit wrote, which is the point: the post-processing + * passes that sample scene depth (lightshafts) must see the scene, not the cockpit. + * + * @param cmd Active command buffer (must be outside a render pass) + */ + void restoreSceneDepth(vk::CommandBuffer cmd) const; + /** * @brief Check if LDR targets are available (tonemapping + FXAA ready) */ @@ -1164,6 +1189,7 @@ class VulkanPostProcessor { RenderTarget m_sceneColor; // RGBA16F HDR scene color RenderTarget m_sceneDepth; // Depth buffer for scene RenderTarget m_sceneDepthCopy; // Samplable copy of scene depth (for soft particles) + RenderTarget m_sceneDepthBackup; // Scene depth parked across the cockpit render (transfer only) RenderTarget m_sceneEffect; // RGBA16F effect/composite (snapshot of scene color) // Scene render pass and framebuffer diff --git a/code/graphics/vulkan/VulkanRenderer.h b/code/graphics/vulkan/VulkanRenderer.h index 3d890b18a84..06a29f21499 100644 --- a/code/graphics/vulkan/VulkanRenderer.h +++ b/code/graphics/vulkan/VulkanRenderer.h @@ -245,6 +245,24 @@ class VulkanRenderer { */ void copySceneDepthForParticles(); + /** + * @brief Park the scene depth so the cockpit can render on a cleared depth buffer + * + * Called by vulkan_post_process_save_zbuffer(). Ends the current scene render + * pass, copies scene depth → backup image, then resumes the pass with + * loadOp=eLoad. The caller clears the depth buffer afterwards. No-op when a + * save is already outstanding, or outside scene rendering. + */ + void saveSceneDepth(); + + /** + * @brief Put the parked scene depth back, discarding what the cockpit wrote + * + * Called by vulkan_post_process_restore_zbuffer(). No-op unless saveSceneDepth() + * parked something. + */ + void restoreSceneDepth(); + /** * @brief Check if scene depth copy is available for sampling this frame */ @@ -336,6 +354,12 @@ class VulkanRenderer { */ void resumeScenePassAfterCopy(); + /** + * @brief Restore the color attachment layouts a depth-only copy left behind, + * then resume the scene (or G-buffer) render pass + */ + void resumeScenePassAfterDepthCopy(); + bool initDisplayDevice() const; bool initializeInstance(); @@ -473,6 +497,7 @@ class VulkanRenderer { std::unique_ptr m_postProcessor; bool m_sceneRendering = false; bool m_sceneDepthCopiedThisFrame = false; + bool m_sceneDepthSaved = false; // True between saveSceneDepth() and restoreSceneDepth() bool m_useGbufRenderPass = false; // True when scene uses G-buffer (deferred lighting) bool m_supportsShaderViewportLayerOutput = false; // VK_EXT_shader_viewport_index_layer diff --git a/code/graphics/vulkan/VulkanRendererLoop.cpp b/code/graphics/vulkan/VulkanRendererLoop.cpp index 601dad04485..c08b0c10d52 100644 --- a/code/graphics/vulkan/VulkanRendererLoop.cpp +++ b/code/graphics/vulkan/VulkanRendererLoop.cpp @@ -139,6 +139,7 @@ void VulkanRenderer::setupFrame() // Reset per-frame flags m_sceneDepthCopiedThisFrame = false; + m_sceneDepthSaved = false; // Reset per-frame draw statistics Assertion(m_drawManager, "Vulkan DrawManager not initialized in setupFrame!"); @@ -431,9 +432,18 @@ void VulkanRenderer::copySceneDepthForParticles() // Copy scene depth → samplable depth copy (handles all depth image transitions) m_postProcessor->copySceneDepth(m_currentCommandBuffer); + resumeScenePassAfterDepthCopy(); + + m_sceneDepthCopiedThisFrame = true; +} + +// Shared tail of every mid-scene copy that touches only depth: put the color +// attachments back the way the resumed loadOp=eLoad pass expects them, then resume. +void VulkanRenderer::resumeScenePassAfterDepthCopy() +{ // Transition scene color: eShaderReadOnlyOptimal → eColorAttachmentOptimal // (needed for the resumed render pass with loadOp=eLoad, which expects - // initialLayout=eColorAttachmentOptimal; copySceneDepth only touches depth) + // initialLayout=eColorAttachmentOptimal; a depth copy only touches depth) { ImageBarrier2 barrier; barrier.image = m_postProcessor->getSceneColorImage(); @@ -460,8 +470,64 @@ void VulkanRenderer::copySceneDepthForParticles() // Resume the scene render pass with loadOp=eLoad resumeScenePassAfterCopy(); +} - m_sceneDepthCopiedThisFrame = true; +// The cockpit model is rendered around the camera, inside the ship hull that the scene +// pass already drew, so it needs a depth buffer that does not know about that hull -- +// otherwise the hull wins the depth test and shows through the cockpit. OpenGL swaps +// the depth attachment to Cockpit_depth_texture for the duration; Vulkan bakes the +// attachment into the framebuffer, so the scene depth is copied aside and cleared +// instead. Only the scene depth image is parked: with MSAA the cockpit renders against +// the multisampled depth, which the G-buffer pass clears on its own, and +// gr_deferred_lighting_msaa() resolves back into the scene depth before restore runs. +void VulkanRenderer::saveSceneDepth() +{ + if (m_sceneDepthSaved || !m_sceneRendering || !m_postProcessor || !m_postProcessor->isInitialized()) { + return; + } + + // resumeScenePassAfterCopy() knows the scene pass and the non-MSAA G-buffer pass only. + // Both callers (ship_render_player_ship) run between one deferred pass and the next, so + // the multisampled G-buffer pass is never the live one -- resuming the wrong pass would + // desync the state tracker and build pipelines against it, so catch a future move here. + Assertion(m_stateTracker->getCurrentSampleCount() == vk::SampleCountFlagBits::e1, + "Tried to park the scene depth while a multisampled render pass was active!"); + + // End the current scene render pass + // This transitions: color → eShaderReadOnlyOptimal, depth → eDepthStencilAttachmentOptimal + // For G-buffer: all 6 color attachments → eShaderReadOnlyOptimal + m_currentCommandBuffer.endRenderPass(); + + m_postProcessor->saveSceneDepth(m_currentCommandBuffer); + + resumeScenePassAfterDepthCopy(); + + m_sceneDepthSaved = true; +} + +void VulkanRenderer::restoreSceneDepth() +{ + if (!m_sceneDepthSaved) { + return; + } + + // Cleared first: a scene that ends between the save and the restore (a failed + // resize dropping the post-processor, say) must not leave the flag set for the + // next frame's restore to act on stale content. + m_sceneDepthSaved = false; + + if (!m_sceneRendering || !m_postProcessor || !m_postProcessor->isInitialized()) { + return; + } + + Assertion(m_stateTracker->getCurrentSampleCount() == vk::SampleCountFlagBits::e1, + "Tried to put the scene depth back while a multisampled render pass was active!"); + + m_currentCommandBuffer.endRenderPass(); + + m_postProcessor->restoreSceneDepth(m_currentCommandBuffer); + + resumeScenePassAfterDepthCopy(); } void VulkanRenderer::beginRenderTarget(tcache_slot_vulkan* ts, int face) diff --git a/code/graphics/vulkan/VulkanRendererSetup.cpp b/code/graphics/vulkan/VulkanRendererSetup.cpp index 90ebc51b567..30fb46c5bd0 100644 --- a/code/graphics/vulkan/VulkanRendererSetup.cpp +++ b/code/graphics/vulkan/VulkanRendererSetup.cpp @@ -1190,6 +1190,7 @@ bool VulkanRenderer::recreateSwapChain() m_drawManager->onResize(); } m_sceneDepthCopiedThisFrame = false; + m_sceneDepthSaved = false; // Update VulkanRenderFrame handles to point to the new swap chain, and // recreate their semaphores: an acquire that succeeded against the old swap