aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src
diff options
context:
space:
mode:
Diffstat (limited to 'adapters/fparkan-render-vulkan/src')
-rw-r--r--adapters/fparkan-render-vulkan/src/asset_mesh.rs12
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs154
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs72
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs93
4 files changed, 297 insertions, 34 deletions
diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs
index 3249012..f691b34 100644
--- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs
+++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs
@@ -1379,8 +1379,10 @@ mod tests {
normals: None,
uv0: None,
uv1: None,
+ type16_present: false,
indices,
batches,
+ triangle_descriptors: None,
node_names: None,
animation: None,
}
@@ -1438,6 +1440,7 @@ mod tests {
normals: None,
uv0: None,
uv1: None,
+ type16_present: false,
indices: Vec::new(),
batches: vec![
batch(0, 0, 0),
@@ -1446,6 +1449,7 @@ mod tests {
..batch(0, 0, 0)
},
],
+ triangle_descriptors: None,
node_names: None,
animation: None,
};
@@ -1684,8 +1688,10 @@ mod tests {
normals: None,
uv0: None,
uv1: None,
+ type16_present: false,
indices: vec![0, 1, 2, 0, 1, 2],
batches: vec![batch(0, 3, 0), batch(3, 3, 0)],
+ triangle_descriptors: None,
node_names: None,
animation: Some(ModelAnimation {
keys: vec![
@@ -1743,8 +1749,10 @@ mod tests {
normals: None,
uv0: None,
uv1: None,
+ type16_present: false,
indices: Vec::new(),
batches: Vec::new(),
+ triangle_descriptors: None,
node_names: None,
animation: Some(ModelAnimation {
keys: vec![
@@ -1849,8 +1857,10 @@ mod tests {
normals: None,
uv0: None,
uv1: None,
+ type16_present: false,
indices: vec![0, 1, 2, 0, 1, 2],
batches: vec![batch(0, 3, 0), batch(3, 3, 0)],
+ triangle_descriptors: None,
node_names: None,
animation: Some(ModelAnimation {
keys: vec![AnimKey24 {
@@ -1940,8 +1950,10 @@ mod tests {
normals: None,
uv0: None,
uv1: None,
+ type16_present: false,
indices: vec![0, 1, 2],
batches: vec![batch(0, 3, 0)],
+ triangle_descriptors: None,
node_names: None,
animation: Some(ModelAnimation {
keys: vec![AnimKey24 {
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
index a2481f7..0e1cebb 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
@@ -423,6 +423,7 @@ impl VulkanSmokeRenderer {
draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()],
draw_order_dirty: false,
draw_range_world_queue: vec![None; create_info.mesh.draw_ranges.len()],
+ draw_range_diffuse_alpha_overrides: vec![None; create_info.mesh.draw_ranges.len()],
world_transparent_background_ranges: Vec::new(),
draw_range_base_pipeline_states: create_info
.mesh
@@ -901,6 +902,27 @@ impl VulkanSmokeRenderer {
Ok(())
}
+ /// Overrides one draw's diffuse alpha without changing its shared material.
+ pub fn set_draw_range_diffuse_alpha_override(
+ &mut self,
+ range_index: usize,
+ diffuse_alpha: Option<f32>,
+ ) -> Result<(), VulkanSmokeRendererError> {
+ if diffuse_alpha.is_some_and(|alpha| !alpha.is_finite() || !(0.0..=1.0).contains(&alpha)) {
+ return Err(VulkanSmokeRendererError::InvalidStaticMesh {
+ context: "dynamic draw-range diffuse alpha must be finite and 0..1",
+ });
+ }
+ let alpha_override = self
+ .draw_range_diffuse_alpha_overrides
+ .get_mut(range_index)
+ .ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
+ context: "dynamic draw-range alpha index is out of bounds",
+ })?;
+ *alpha_override = diffuse_alpha;
+ Ok(())
+ }
+
/// Sets the native per-range alpha reference in legacy 0..=255 units.
///
/// Zero remains an explicit value; use the native-default constant when
@@ -1808,6 +1830,7 @@ impl VulkanSmokeRenderer {
0,
vk::IndexType::UINT32,
);
+ let mut viewport_depth = None;
for &range_index in &self.draw_order {
let range = self.draw_ranges.get(range_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
@@ -1873,11 +1896,16 @@ impl VulkanSmokeRenderer {
context: "static material alpha",
},
)?;
- let material_alphas = [
- material_alpha[0].to_ne_bytes(),
- material_alpha[1].to_ne_bytes(),
- ]
- .concat();
+ let diffuse_alpha_override = self
+ .draw_range_diffuse_alpha_overrides
+ .get(range_index)
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "draw-range diffuse-alpha override",
+ })?;
+ let material_alphas =
+ draw_material_alphas(*material_alpha, *diffuse_alpha_override)
+ .map(f32::to_ne_bytes)
+ .concat();
device.device().cmd_push_constants(
command_buffer,
resources.pipeline_layout,
@@ -1929,6 +1957,17 @@ impl VulkanSmokeRenderer {
} else {
*material_mode
};
+ let viewport = super::swapchain_resources::draw_viewport(
+ swapchain.report.plan.extent,
+ material_mode,
+ );
+ let next_viewport_depth = [viewport.min_depth, viewport.max_depth];
+ if viewport_depth != Some(next_viewport_depth) {
+ device
+ .device()
+ .cmd_set_viewport(command_buffer, 0, &[viewport]);
+ viewport_depth = Some(next_viewport_depth);
+ }
let material_mode = [material_mode.to_ne_bytes()];
device.device().cmd_push_constants(
command_buffer,
@@ -2356,6 +2395,16 @@ fn apply_world_transparency(
}
}
+fn draw_material_alphas(
+ material_alphas: [f32; 2],
+ diffuse_alpha_override: Option<f32>,
+) -> [f32; 2] {
+ [
+ diffuse_alpha_override.unwrap_or(material_alphas[0]),
+ material_alphas[1],
+ ]
+}
+
fn world_material_pipeline_states(
original: LegacyPipelineState,
category: u8,
@@ -2523,12 +2572,14 @@ mod tests {
use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh};
use super::{
apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index,
- mark_draw_order_dirty_on_activity_change, set_and_mark_dirty_if_changed,
- sorted_world_draw_order, take_runtime_children_with_validation_snapshot,
- take_runtime_owners_in_dependency_order, validate_geometry_upload,
- world_material_pipeline_states, RollbackOnDrop,
+ draw_material_alphas, mark_draw_order_dirty_on_activity_change,
+ set_and_mark_dirty_if_changed, sorted_world_draw_order,
+ take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order,
+ validate_geometry_upload, world_material_pipeline_states, RollbackOnDrop,
+ };
+ use fparkan_render::{
+ LegacyBlendMode, LegacyCullMode, LegacyDepthBias, LegacyDepthMode, LegacyPipelineState,
};
- use fparkan_render::{LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState};
use std::cell::RefCell;
use std::rc::Rc;
@@ -3096,12 +3147,74 @@ mod tests {
}
#[test]
+ fn control_fx_queue_follows_opaque_and_sky_without_reactivating_empty_ranges() {
+ const SCREEN_GRADIENT: usize = 0;
+ const CONTROL_FX: usize = 1;
+ const TERRAIN: usize = 2;
+ const BUILDING: usize = 3;
+ const SHADOW: usize = 4;
+ const NEBULA: usize = 5;
+ const DOME: usize = 6;
+ const CLOUDS: usize = 7;
+ const EMPTY_CONTROL_FX: usize = 8;
+ const SUN: usize = 9;
+ const WEATHER: usize = 10;
+ let mut ranges = (0..11)
+ .map(|index| VulkanStaticDrawRange {
+ first_index: index * 3,
+ index_count: 3,
+ material_index: 0,
+ lightmap_index: u8::MAX,
+ batch_flags: 0,
+ pipeline_state: LegacyPipelineState::default(),
+ terrain_base_only: false,
+ alpha_test_reference: 0,
+ })
+ .collect::<Vec<_>>();
+ ranges[EMPTY_CONTROL_FX].index_count = 0;
+ let mut world_queue = vec![None; ranges.len()];
+ world_queue[TERRAIN] = Some(false);
+ world_queue[BUILDING] = Some(false);
+ world_queue[CONTROL_FX] = Some(true);
+ world_queue[EMPTY_CONTROL_FX] = Some(true);
+ let mut sort_keys = vec![None; ranges.len()];
+ sort_keys[CONTROL_FX] = Some(20.0);
+ sort_keys[EMPTY_CONTROL_FX] = Some(30.0);
+
+ let order = sorted_world_draw_order(
+ ranges.len(),
+ &ranges,
+ &world_queue,
+ &sort_keys,
+ &[NEBULA, DOME, CLOUDS],
+ );
+ assert_eq!(
+ order,
+ vec![
+ SCREEN_GRADIENT,
+ TERRAIN,
+ BUILDING,
+ SHADOW,
+ NEBULA,
+ DOME,
+ CLOUDS,
+ CONTROL_FX,
+ EMPTY_CONTROL_FX,
+ SUN,
+ WEATHER,
+ ]
+ );
+ assert_eq!(ranges[EMPTY_CONTROL_FX].index_count, 0);
+ }
+
+ #[test]
fn material_category_and_transparency_preserve_world_state_in_either_order() {
let original = LegacyPipelineState {
blend: LegacyBlendMode::Opaque,
depth: LegacyDepthMode::TestWrite,
cull: LegacyCullMode::FrontFace,
alpha_test: false,
+ depth_bias: LegacyDepthBias::ProjectedDecal,
};
let (base, category_first) =
world_material_pipeline_states(original, 1, Some(true)).expect("category 1");
@@ -3113,14 +3226,17 @@ mod tests {
assert!(base.alpha_test);
assert_eq!(base.depth, LegacyDepthMode::TestWrite);
assert_eq!(base.cull, LegacyCullMode::FrontFace);
+ assert_eq!(base.depth_bias, LegacyDepthBias::ProjectedDecal);
assert_eq!(category_first.depth, LegacyDepthMode::TestReadOnly);
assert_eq!(category_first.cull, LegacyCullMode::FrontFace);
+ assert_eq!(category_first.depth_bias, LegacyDepthBias::ProjectedDecal);
let fixed_state = LegacyPipelineState {
blend: LegacyBlendMode::Additive,
depth: LegacyDepthMode::Disabled,
cull: LegacyCullMode::Disabled,
alpha_test: false,
+ depth_bias: LegacyDepthBias::Disabled,
};
assert_eq!(
world_material_pipeline_states(fixed_state, 1, None),
@@ -3129,4 +3245,22 @@ mod tests {
assert!(world_material_pipeline_states(original, 5, Some(false)).is_none());
assert!(world_material_pipeline_states(fixed_state, 5, None).is_none());
}
+
+ #[test]
+ fn draw_alpha_override_is_per_range_and_none_restores_material_alpha() {
+ let shared_material = [0.25, 0.5];
+
+ assert_eq!(draw_material_alphas(shared_material, Some(0.0)), [0.0, 0.5]);
+ assert_eq!(draw_material_alphas(shared_material, None), shared_material);
+
+ let updated_material = [0.75, 0.125];
+ assert_eq!(
+ draw_material_alphas(updated_material, None),
+ updated_material
+ );
+ assert_eq!(
+ draw_material_alphas(updated_material, Some(0.0)),
+ [0.0, 0.125]
+ );
+ }
}
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
index 51f8384..0996fa1 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
@@ -284,6 +284,9 @@ impl VulkanFrameUniforms {
.chain([self.fog_start, self.fog_end])
.chain(self.camera_position)
.all(f32::is_finite)
+ && (self.fog_end <= self.fog_start
+ || ((self.fog_start * self.fog_start).is_finite()
+ && (self.fog_end * self.fog_end).is_finite()))
}
/// Serializes one frame slot in the exact `std140` member order.
@@ -344,7 +347,17 @@ impl VulkanFrameUniforms {
self.fog_color[2],
0.0,
]);
- values[84..88].copy_from_slice(&[self.fog_start, self.fog_end, 0.0, 0.0]);
+ let squared_fog_range = if self.fog_end > self.fog_start {
+ [self.fog_start * self.fog_start, self.fog_end * self.fog_end]
+ } else {
+ [0.0; 2]
+ };
+ values[84..88].copy_from_slice(&[
+ self.fog_start,
+ self.fog_end,
+ squared_fog_range[0],
+ squared_fog_range[1],
+ ]);
values[88..92].copy_from_slice(&[
self.camera_position[0],
self.camera_position[1],
@@ -756,8 +769,8 @@ impl VulkanStaticMesh {
material_index: 0,
lightmap_index: u8::MAX,
batch_flags: 0,
- terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(),
+ terrain_base_only: false,
alpha_test_reference: 0,
}],
}
@@ -875,6 +888,31 @@ mod static_mesh_tests {
}
#[test]
+ fn fog_uniforms_match_live_native_squared_distance_range() {
+ // Terrain ShadeIndexedStrided: far=0x442389e6, far²=0x48d0f1ca;
+ // distance²=0x47707b04 gives visibility=0x3f5b2b94.
+ let frame = VulkanFrameUniforms {
+ fog_start: 0.0,
+ fog_end: f32::from_bits(0x4423_89e6),
+ ..Default::default()
+ };
+ assert!(frame.is_finite());
+ let bytes = frame.to_ne_bytes();
+ let read = |offset| f32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
+ assert_eq!(read(344), 0.0);
+ assert_eq!(read(348).to_bits(), 0x48d0_f1ca);
+ let visibility = 1.0 - (f32::from_bits(0x4770_7b04) - read(344)) / (read(348) - read(344));
+ assert!((visibility - f32::from_bits(0x3f5b_2b94)).abs() < 1.0e-6);
+ assert!(VulkanFrameUniforms::default().is_finite());
+ assert!(!VulkanFrameUniforms {
+ fog_start: 0.0,
+ fog_end: f32::MAX,
+ ..frame
+ }
+ .is_finite());
+ }
+
+ #[test]
fn static_camera_composes_recovered_d3d7_view_before_projection() {
let transform = RawCameraTransform {
words: [
@@ -992,8 +1030,8 @@ mod static_mesh_tests {
material_index: 0,
lightmap_index: u8::MAX,
batch_flags: 0,
- terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(),
+ terrain_base_only: false,
alpha_test_reference: 0,
}],
};
@@ -1023,8 +1061,8 @@ mod static_mesh_tests {
material_index: 7,
lightmap_index: u8::MAX,
batch_flags: 0,
- terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(),
+ terrain_base_only: false,
alpha_test_reference: 0,
},
VulkanStaticDrawRange {
@@ -1033,8 +1071,8 @@ mod static_mesh_tests {
material_index: 2,
lightmap_index: u8::MAX,
batch_flags: 0,
- terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(),
+ terrain_base_only: false,
alpha_test_reference: 0,
},
];
@@ -1101,6 +1139,11 @@ mod static_mesh_tests {
},
];
+ assert_eq!(
+ resolve_draw_texture_indices(&ranges, &materials),
+ Ok(vec![1, 0])
+ );
+
let mut screen_tl = materials[0].clone();
screen_tl.screen_tl = true;
screen_tl.sky_far_depth = true;
@@ -1109,11 +1152,9 @@ mod static_mesh_tests {
far_sky.unlit = true;
far_sky.sky_far_depth = true;
assert_eq!(far_sky.encoded_material_mode(), 5.0);
-
- assert_eq!(
- resolve_draw_texture_indices(&ranges, &materials),
- Ok(vec![1, 0])
- );
+ let mut nebula_with_stars = far_sky.clone();
+ nebula_with_stars.sky_nebula_stars = true;
+ assert_eq!(nebula_with_stars.encoded_material_mode(), 6.0);
let mut non_finite_base = materials.clone();
non_finite_base[0].specular_rgb[0] = f32::NAN;
@@ -1149,6 +1190,7 @@ mod static_mesh_tests {
fn alpha_cutoff_is_dynamic_and_disabled_outside_alpha_test_pipeline() {
let base = VulkanStaticMesh::smoke_triangle().draw_ranges[0];
let disabled = VulkanStaticDrawRange {
+ terrain_base_only: false,
alpha_test_reference: 200,
..base
};
@@ -1157,6 +1199,7 @@ mod static_mesh_tests {
alpha_test: true,
..LegacyPipelineState::default()
},
+ terrain_base_only: false,
alpha_test_reference: 128,
..base
};
@@ -1167,6 +1210,7 @@ mod static_mesh_tests {
alpha_test: true,
..LegacyPipelineState::default()
},
+ terrain_base_only: false,
alpha_test_reference: 0,
..base
};
@@ -1176,6 +1220,7 @@ mod static_mesh_tests {
alpha_test: true,
..LegacyPipelineState::default()
},
+ terrain_base_only: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
..base
};
@@ -1456,6 +1501,9 @@ pub struct VulkanSmokeRenderer {
/// a fixed screen/sky/weather pass; `Some(false)` is world opaque and
/// `Some(true)` is world transparent.
pub(super) draw_range_world_queue: Vec<Option<bool>>,
+ /// Optional diffuse-alpha override for each draw range, independent of
+ /// the shared material record.
+ pub(super) draw_range_diffuse_alpha_overrides: Vec<Option<f32>>,
/// Fixed sky background ranges placed between world opaque and transparent.
pub(super) world_transparent_background_ranges: Vec<usize>,
/// Pipeline state before transparent queue depth policy is applied.
@@ -1469,8 +1517,8 @@ pub struct VulkanSmokeRenderer {
pub(super) material_selectors: Vec<u16>,
/// Whether the host-side specular records need a fence-safe GPU upload.
pub(super) material_specular_dirty: bool,
- /// Encoded material mode pushed at offset 124: 0 lit, 1 unlit, 2 native
- /// sky nebula/stars texture composite, 3 native lightmap composite.
+ /// Encoded material mode pushed at offset 124: low bits select the
+ /// combiner, bit 4 selects sky far-depth, and bit 8 selects screen/TL.
pub(super) material_unlit: Vec<f32>,
pub(super) readback_enabled: bool,
pub(super) camera: VulkanStaticCamera,
diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
index 89ca43a..973e8b1 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
@@ -3,7 +3,8 @@
use ash::vk;
use fparkan_platform::DepthStencilSupport;
use fparkan_render::{
- LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState, PipelineKey,
+ LegacyBlendMode, LegacyCullMode, LegacyDepthBias, LegacyDepthMode, LegacyPipelineState,
+ PipelineKey,
};
use std::collections::BTreeMap;
@@ -922,8 +923,25 @@ fn create_texture_descriptor_bundle(
Ok((layout, pool, descriptor_sets, sampler))
}
-fn extent_component_to_f32(value: u32) -> f32 {
- value as f32
+pub(super) fn draw_viewport(extent: (u32, u32), material_mode: f32) -> vk::Viewport {
+ // Ngi32 RVA 0x7170 maps world depth to 0.1..0.99; TL and sky depth bypass it.
+ let explicit_depth = material_mode.round() as u32 & 12 != 0;
+ vk::Viewport {
+ x: 0.0,
+ y: 0.0,
+ width: extent.0 as f32,
+ height: extent.1 as f32,
+ min_depth: if explicit_depth { 0.0 } else { 0.1 },
+ max_depth: if explicit_depth { 1.0 } else { 0.99 },
+ }
+}
+
+fn depth_bias_parameters(depth_bias: LegacyDepthBias) -> (bool, f32, f32) {
+ match depth_bias {
+ LegacyDepthBias::Disabled => (false, 0.0, 0.0),
+ // shortcut: Vulkan constantFactor=-1.0/slopeFactor=0.0 is empirical; verify native raster state before claiming parity.
+ LegacyDepthBias::ProjectedDecal => (true, -1.0, 0.0),
+ }
}
fn create_graphics_pipeline_cache(
@@ -1033,14 +1051,7 @@ fn create_graphics_pipeline(
let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo::default()
.topology(vk::PrimitiveTopology::TRIANGLE_LIST)
.primitive_restart_enable(false);
- let viewports = [vk::Viewport {
- x: 0.0,
- y: 0.0,
- width: extent_component_to_f32(extent.0),
- height: extent_component_to_f32(extent.1),
- min_depth: 0.0,
- max_depth: 1.0,
- }];
+ let viewports = [draw_viewport(extent, 0.0)];
let scissors = [vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: vk::Extent2D {
@@ -1051,6 +1062,8 @@ fn create_graphics_pipeline(
let viewport_state = vk::PipelineViewportStateCreateInfo::default()
.viewports(&viewports)
.scissors(&scissors);
+ let (depth_bias_enable, depth_bias_constant_factor, depth_bias_slope_factor) =
+ depth_bias_parameters(state.depth_bias);
let rasterization_state = vk::PipelineRasterizationStateCreateInfo::default()
.depth_clamp_enable(false)
.rasterizer_discard_enable(false)
@@ -1062,7 +1075,10 @@ fn create_graphics_pipeline(
LegacyCullMode::FrontFace => vk::CullModeFlags::FRONT,
})
.front_face(vk::FrontFace::CLOCKWISE)
- .depth_bias_enable(false);
+ .depth_bias_enable(depth_bias_enable)
+ .depth_bias_constant_factor(depth_bias_constant_factor)
+ .depth_bias_slope_factor(depth_bias_slope_factor)
+ .depth_bias_clamp(0.0);
let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()
.sample_shading_enable(false)
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
@@ -1105,6 +1121,9 @@ fn create_graphics_pipeline(
let color_blend_state = vk::PipelineColorBlendStateCreateInfo::default()
.logic_op_enable(false)
.attachments(&color_blend_attachments);
+ let dynamic_states = [vk::DynamicState::VIEWPORT];
+ let dynamic_state =
+ vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
let create_info = vk::GraphicsPipelineCreateInfo::default()
.stages(&shader_stages)
.vertex_input_state(&vertex_input_state)
@@ -1114,6 +1133,7 @@ fn create_graphics_pipeline(
.multisample_state(&multisample_state)
.depth_stencil_state(&depth_stencil_state)
.color_blend_state(&color_blend_state)
+ .dynamic_state(&dynamic_state)
.layout(pipeline_layout)
.render_pass(render_pass)
.subpass(0);
@@ -1328,3 +1348,52 @@ fn destroy_partial_swapchain_resources(
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::{depth_bias_parameters, draw_viewport};
+ use fparkan_render::LegacyDepthBias;
+
+ #[test]
+ fn projected_decal_bias_parameters_use_empirical_vulkan_factors() {
+ assert_eq!(
+ depth_bias_parameters(LegacyDepthBias::Disabled),
+ (false, 0.0, 0.0)
+ );
+ assert_eq!(
+ depth_bias_parameters(LegacyDepthBias::ProjectedDecal),
+ (true, -1.0, 0.0)
+ );
+ }
+
+ #[test]
+ fn world_viewport_depth_keeps_distant_geometry_in_front_of_screen_sun() {
+ let projection = fparkan_render::LegacyD3d7Projection {
+ viewport: [0, 0, 1280, 1024],
+ near_plane: 0.5,
+ far_plane: 700.0,
+ field_of_view_radians: 1.04,
+ }
+ .try_direct3d7_projection_row_major()
+ .expect("native projection");
+ for mode in [0.0, 1.0, 2.0, 3.0, 16.0, 17.0] {
+ let viewport = draw_viewport((1280, 1024), mode);
+ assert_eq!([viewport.width, viewport.height], [1280.0, 1024.0]);
+ assert_eq!([viewport.min_depth, viewport.max_depth], [0.1, 0.99]);
+ for distance in [0.5, 100.0, 350.0, 600.0, 700.0] {
+ let clip_z = distance * projection[10] + projection[14];
+ let clip_w = distance * projection[11];
+ let depth = viewport.min_depth
+ + (viewport.max_depth - viewport.min_depth) * (clip_z / clip_w);
+ assert!(
+ depth < 0.999,
+ "mode {mode}, distance {distance}, depth {depth}"
+ );
+ }
+ }
+ for mode in [4.0, 5.0, 6.0, 7.0, 9.0] {
+ let viewport = draw_viewport((640, 480), mode);
+ assert_eq!([viewport.min_depth, viewport.max_depth], [0.0, 1.0]);
+ }
+ }
+}