feat: complete map viewer scene and static CTL pose preview
Complete the interactive mission viewer with environment rendering, audio events, dynamic shadows, free-flight camera controls, and per-component CTL pose sampling.
This commit is contained in:
@@ -2,18 +2,132 @@
|
||||
|
||||
layout(location = 0) in vec3 in_color;
|
||||
layout(location = 1) in vec2 in_uv;
|
||||
layout(location = 2) in vec2 in_detail_uv;
|
||||
layout(location = 3) in float in_overlay_alpha;
|
||||
layout(location = 4) in float in_fog_factor;
|
||||
layout(location = 5) in vec3 in_base_diffuse;
|
||||
layout(location = 6) in vec3 in_base_specular;
|
||||
layout(location = 7) in vec3 in_overlay_diffuse;
|
||||
layout(location = 8) in vec3 in_overlay_specular;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
layout(set = 0, binding = 0) uniform sampler2D base_color;
|
||||
layout(set = 0, binding = 1) uniform sampler2D base_detail;
|
||||
layout(set = 0, binding = 2) uniform sampler2D overlay_color;
|
||||
layout(set = 0, binding = 3) uniform sampler2D overlay_detail;
|
||||
|
||||
layout(push_constant) uniform AlphaTestConstants {
|
||||
layout(offset = 64)
|
||||
float alpha_cutoff;
|
||||
} alpha_test;
|
||||
layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
||||
mat4 clip_from_world;
|
||||
vec4 directional_direction[4];
|
||||
vec4 directional_rgb[4];
|
||||
vec4 directional_coefficients[4];
|
||||
vec4 point_position_range;
|
||||
vec4 point_rgb_active;
|
||||
vec4 point_coefficients_active;
|
||||
vec4 lighting_floor;
|
||||
vec4 fog_color;
|
||||
vec4 fog_distances;
|
||||
vec4 camera_position;
|
||||
} frame;
|
||||
|
||||
layout(push_constant) uniform MaterialConstants {
|
||||
layout(offset = 0) float alpha_cutoff;
|
||||
layout(offset = 4) float diffuse_alpha;
|
||||
layout(offset = 8) float overlay_diffuse_alpha;
|
||||
layout(offset = 12) float directional_r;
|
||||
layout(offset = 16) float directional_g;
|
||||
layout(offset = 20) float directional_b;
|
||||
layout(offset = 24) float additive_r;
|
||||
layout(offset = 28) float additive_g;
|
||||
layout(offset = 32) float additive_b;
|
||||
layout(offset = 36) float overlay_directional_r;
|
||||
layout(offset = 40) float overlay_directional_g;
|
||||
layout(offset = 44) float overlay_directional_b;
|
||||
layout(offset = 48) float overlay_additive_r;
|
||||
layout(offset = 52) float overlay_additive_g;
|
||||
layout(offset = 56) float overlay_additive_b;
|
||||
layout(offset = 60) float base_page_x;
|
||||
layout(offset = 64) float base_page_y;
|
||||
layout(offset = 68) float base_page_w;
|
||||
layout(offset = 72) float base_page_h;
|
||||
layout(offset = 76) float detail_page_x;
|
||||
layout(offset = 80) float detail_page_y;
|
||||
layout(offset = 84) float detail_page_w;
|
||||
layout(offset = 88) float detail_page_h;
|
||||
layout(offset = 92) float overlay_page_x;
|
||||
layout(offset = 96) float overlay_page_y;
|
||||
layout(offset = 100) float overlay_page_w;
|
||||
layout(offset = 104) float overlay_page_h;
|
||||
layout(offset = 108) float overlay_detail_page_x;
|
||||
layout(offset = 112) float overlay_detail_page_y;
|
||||
layout(offset = 116) float overlay_detail_page_w;
|
||||
layout(offset = 120) float overlay_detail_page_h;
|
||||
layout(offset = 124) float material_mode;
|
||||
} material;
|
||||
|
||||
void main() {
|
||||
out_color = texture(base_color, in_uv) * vec4(in_color, 1.0);
|
||||
if (out_color.a < alpha_test.alpha_cutoff) {
|
||||
vec2 base_uv = vec2(material.base_page_x, material.base_page_y)
|
||||
+ in_uv * vec2(material.base_page_w, material.base_page_h);
|
||||
vec2 detail_uv = vec2(material.detail_page_x, material.detail_page_y)
|
||||
+ in_detail_uv * vec2(material.detail_page_w, material.detail_page_h);
|
||||
vec2 overlay_uv = vec2(material.overlay_page_x, material.overlay_page_y)
|
||||
+ in_uv * vec2(material.overlay_page_w, material.overlay_page_h);
|
||||
vec2 overlay_detail_uv = vec2(material.overlay_detail_page_x, material.overlay_detail_page_y)
|
||||
+ in_detail_uv * vec2(material.overlay_detail_page_w, material.overlay_detail_page_h);
|
||||
vec4 base = texture(base_color, base_uv);
|
||||
vec4 detail = texture(base_detail, detail_uv);
|
||||
vec4 overlay = texture(overlay_color, overlay_uv);
|
||||
vec4 overlay_detail_sample = texture(overlay_detail, overlay_detail_uv);
|
||||
int mode = int(round(material.material_mode));
|
||||
int combiner_mode = mode & 3;
|
||||
|
||||
// Native sky mode 4 combines nebula and stars as
|
||||
// mix(TEX0.rgb, TEX1.rgb, TEX1.a) * DIFFUSE.rgb and carries DIFFUSE.a
|
||||
// through as the pass alpha.
|
||||
if (combiner_mode == 2) {
|
||||
vec3 sky_rgb = mix(base.rgb, detail.rgb, clamp(detail.a, 0.0, 1.0));
|
||||
out_color = vec4(sky_rgb * in_color, clamp(in_overlay_alpha, 0.0, 1.0));
|
||||
if (out_color.a < material.alpha_cutoff) {
|
||||
discard;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Native terrain mode 3 combines the base texture and the lightmap
|
||||
// without the regular terrain 2x combiner or NdotL calculation.
|
||||
if (combiner_mode == 3) {
|
||||
vec3 lightmap_lighting = max(
|
||||
vec3(material.additive_r, material.additive_g, material.additive_b),
|
||||
frame.lighting_floor.rgb
|
||||
);
|
||||
vec3 vertex_rgb = base.rgb * detail.rgb * lightmap_lighting * in_color;
|
||||
vec3 final_rgb = mix(vertex_rgb, frame.fog_color.rgb, in_fog_factor);
|
||||
out_color = vec4(final_rgb, base.a * material.diffuse_alpha);
|
||||
if (out_color.a < material.alpha_cutoff) {
|
||||
discard;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float overlay_alpha = clamp(
|
||||
in_overlay_alpha * material.overlay_diffuse_alpha,
|
||||
0.0,
|
||||
1.0
|
||||
);
|
||||
vec3 base_rgb = 2.0 * base.rgb * detail.rgb;
|
||||
vec3 overlay_rgb = 2.0 * overlay.rgb * overlay_detail_sample.rgb;
|
||||
vec3 base_lit_rgb = base_rgb * in_base_diffuse + in_base_specular;
|
||||
vec3 overlay_lit_rgb = overlay_rgb * in_overlay_diffuse + in_overlay_specular;
|
||||
vec3 lit_rgb = mix(base_lit_rgb, overlay_lit_rgb, overlay_alpha);
|
||||
vec3 vertex_rgb = lit_rgb * in_color;
|
||||
vec3 final_rgb = combiner_mode == 0
|
||||
? mix(vertex_rgb, frame.fog_color.rgb, in_fog_factor)
|
||||
: vertex_rgb;
|
||||
float final_alpha = combiner_mode == 1
|
||||
? base.a * material.diffuse_alpha * in_overlay_alpha
|
||||
: base.a * material.diffuse_alpha;
|
||||
out_color = vec4(final_rgb, final_alpha);
|
||||
if (out_color.a < material.alpha_cutoff) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -2,17 +2,273 @@
|
||||
|
||||
layout(location = 0) in vec3 in_position;
|
||||
layout(location = 1) in vec3 in_color;
|
||||
layout(location = 2) in vec2 in_uv;
|
||||
layout(location = 2) in vec3 in_normal;
|
||||
layout(location = 3) in vec2 in_uv;
|
||||
layout(location = 4) in vec2 in_detail_uv;
|
||||
layout(location = 5) in float in_overlay_alpha;
|
||||
|
||||
layout(location = 0) out vec3 out_color;
|
||||
layout(location = 1) out vec2 out_uv;
|
||||
layout(location = 2) out vec2 out_detail_uv;
|
||||
layout(location = 3) out float out_overlay_alpha;
|
||||
// Native D3D vertex fog is written to SPECULAR.a and interpolated by the
|
||||
// fixed-function rasterizer. Keep the existing varying slot, but carry only
|
||||
// that scalar instead of recomputing distance in the fragment stage.
|
||||
layout(location = 4) out float out_fog_factor;
|
||||
layout(location = 5) out vec3 out_base_diffuse;
|
||||
layout(location = 6) out vec3 out_base_specular;
|
||||
layout(location = 7) out vec3 out_overlay_diffuse;
|
||||
layout(location = 8) out vec3 out_overlay_specular;
|
||||
|
||||
layout(push_constant) uniform CameraConstants {
|
||||
layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
||||
mat4 clip_from_world;
|
||||
} camera;
|
||||
vec4 directional_direction[4];
|
||||
vec4 directional_rgb[4];
|
||||
vec4 directional_coefficients[4];
|
||||
vec4 point_position_range;
|
||||
vec4 point_rgb_active;
|
||||
vec4 point_coefficients_active;
|
||||
vec4 lighting_floor;
|
||||
vec4 fog_color;
|
||||
vec4 fog_distances;
|
||||
vec4 camera_position;
|
||||
} frame;
|
||||
|
||||
layout(std430, set = 0, binding = 5) readonly buffer MaterialSpecular {
|
||||
vec4 base;
|
||||
vec4 overlay;
|
||||
} material_specular;
|
||||
|
||||
layout(push_constant) uniform MaterialConstants {
|
||||
layout(offset = 0) float alpha_cutoff;
|
||||
layout(offset = 4) float diffuse_alpha;
|
||||
layout(offset = 8) float overlay_diffuse_alpha;
|
||||
layout(offset = 12) float directional_r;
|
||||
layout(offset = 16) float directional_g;
|
||||
layout(offset = 20) float directional_b;
|
||||
layout(offset = 24) float additive_r;
|
||||
layout(offset = 28) float additive_g;
|
||||
layout(offset = 32) float additive_b;
|
||||
layout(offset = 36) float overlay_directional_r;
|
||||
layout(offset = 40) float overlay_directional_g;
|
||||
layout(offset = 44) float overlay_directional_b;
|
||||
layout(offset = 48) float overlay_additive_r;
|
||||
layout(offset = 52) float overlay_additive_g;
|
||||
layout(offset = 56) float overlay_additive_b;
|
||||
layout(offset = 60) float base_page_x;
|
||||
layout(offset = 64) float base_page_y;
|
||||
layout(offset = 68) float base_page_w;
|
||||
layout(offset = 72) float base_page_h;
|
||||
layout(offset = 76) float detail_page_x;
|
||||
layout(offset = 80) float detail_page_y;
|
||||
layout(offset = 84) float detail_page_w;
|
||||
layout(offset = 88) float detail_page_h;
|
||||
layout(offset = 92) float overlay_page_x;
|
||||
layout(offset = 96) float overlay_page_y;
|
||||
layout(offset = 100) float overlay_page_w;
|
||||
layout(offset = 104) float overlay_page_h;
|
||||
layout(offset = 108) float overlay_detail_page_x;
|
||||
layout(offset = 112) float overlay_detail_page_y;
|
||||
layout(offset = 116) float overlay_detail_page_w;
|
||||
layout(offset = 120) float overlay_detail_page_h;
|
||||
layout(offset = 124) float material_mode;
|
||||
} material;
|
||||
|
||||
struct LightingTerms {
|
||||
vec3 diffuse;
|
||||
vec3 specular;
|
||||
};
|
||||
|
||||
float native_range_compress(float value) {
|
||||
if (value <= 1.0) {
|
||||
return value;
|
||||
}
|
||||
if (value <= 7.0) {
|
||||
return value / 6.0 + 5.0 / 6.0;
|
||||
}
|
||||
return 2.0;
|
||||
}
|
||||
|
||||
float native_specular_curve(float value) {
|
||||
if (value <= 1.0) {
|
||||
return 0.8 * value;
|
||||
}
|
||||
if (value <= 3.0) {
|
||||
return 0.1 * value + 0.7;
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// The original callback uses the reflected vector and squares the cosine for
|
||||
// power-1 iterations. A bounded loop keeps the native byte power while
|
||||
// remaining valid for every material record.
|
||||
float native_specular_power(float cosine, float power) {
|
||||
// CShade enables specular only when the material power byte is non-zero.
|
||||
if (power <= 0.0 || cosine <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
float result = cosine;
|
||||
int iterations = clamp(int(power) - 1, 0, 254);
|
||||
for (int index = 0; index < 254; ++index) {
|
||||
if (index >= iterations) {
|
||||
break;
|
||||
}
|
||||
result *= result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
float native_fog_factor(vec3 world_position) {
|
||||
float fog_span = frame.fog_distances.y - frame.fog_distances.x;
|
||||
float distance_to_camera = distance(world_position, frame.camera_position.xyz);
|
||||
if (fog_span <= 0.000001 || distance_to_camera <= frame.fog_distances.x) {
|
||||
return 0.0;
|
||||
}
|
||||
return clamp((distance_to_camera - frame.fog_distances.x) / fog_span, 0.0, 1.0);
|
||||
}
|
||||
|
||||
vec3 normalized_or(vec3 value, vec3 fallback) {
|
||||
float length_squared = dot(value, value);
|
||||
return length_squared > 0.0000001 ? value * inversesqrt(length_squared) : fallback;
|
||||
}
|
||||
|
||||
vec3 specular_from_light(
|
||||
vec3 normal,
|
||||
vec3 view_direction,
|
||||
vec3 light_direction,
|
||||
vec3 light_rgb,
|
||||
vec3 material_rgb,
|
||||
float power,
|
||||
float attenuation
|
||||
) {
|
||||
float ndotl = dot(normal, light_direction);
|
||||
if (ndotl <= 0.0 || attenuation <= 0.0) {
|
||||
return vec3(0.0);
|
||||
}
|
||||
vec3 reflection = reflect(-light_direction, normal);
|
||||
float cosine = max(dot(reflection, view_direction), 0.0);
|
||||
return material_rgb * light_rgb * native_specular_power(cosine, power) * attenuation;
|
||||
}
|
||||
|
||||
LightingTerms native_material_lighting(
|
||||
vec3 world_position,
|
||||
vec3 normal,
|
||||
vec3 directional_rgb,
|
||||
vec3 additive_rgb,
|
||||
vec3 specular_rgb,
|
||||
float specular_power
|
||||
) {
|
||||
vec3 accumulated = additive_rgb;
|
||||
vec3 specular = vec3(0.0);
|
||||
vec3 view_direction = normalized_or(frame.camera_position.xyz - world_position, vec3(0.0, 0.0, 1.0));
|
||||
for (int index = 0; index < 4; ++index) {
|
||||
vec4 direction_record = frame.directional_direction[index];
|
||||
vec4 rgb_record = frame.directional_rgb[index];
|
||||
if (direction_record.w <= 0.5 || rgb_record.w <= 0.5) {
|
||||
continue;
|
||||
}
|
||||
vec3 light_direction = normalized_or(-direction_record.xyz, vec3(0.0, 0.0, 1.0));
|
||||
float ndotl = max(dot(normal, light_direction), 0.0);
|
||||
accumulated += ndotl * directional_rgb * rgb_record.rgb;
|
||||
specular += specular_from_light(
|
||||
normal,
|
||||
view_direction,
|
||||
light_direction,
|
||||
rgb_record.rgb,
|
||||
specular_rgb,
|
||||
specular_power,
|
||||
1.0
|
||||
);
|
||||
}
|
||||
vec4 point_position_range = frame.point_position_range;
|
||||
vec4 point_rgb_active = frame.point_rgb_active;
|
||||
vec4 point_coefficients_active = frame.point_coefficients_active;
|
||||
vec3 to_light = point_position_range.xyz - world_position;
|
||||
float distance_squared = dot(to_light, to_light);
|
||||
float range = point_position_range.w;
|
||||
if (point_rgb_active.w > 0.5 && range > 0.0 && distance_squared <= range * range) {
|
||||
float distance_to_light = sqrt(distance_squared);
|
||||
if (distance_to_light > 0.000001) {
|
||||
vec3 light_direction = to_light / distance_to_light;
|
||||
float ndotl = dot(normal, light_direction);
|
||||
if (ndotl > 0.0) {
|
||||
float t = (range - distance_to_light) / range;
|
||||
float attenuation = point_coefficients_active.x
|
||||
+ point_coefficients_active.y * t
|
||||
+ point_coefficients_active.z * t * t;
|
||||
attenuation = max(attenuation, 0.0);
|
||||
accumulated += ndotl * directional_rgb * point_rgb_active.rgb * attenuation;
|
||||
specular += specular_from_light(
|
||||
normal,
|
||||
view_direction,
|
||||
light_direction,
|
||||
point_rgb_active.rgb,
|
||||
specular_rgb,
|
||||
specular_power,
|
||||
attenuation
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
accumulated = max(accumulated, frame.lighting_floor.rgb);
|
||||
vec3 compressed = vec3(
|
||||
native_range_compress(accumulated.r),
|
||||
native_range_compress(accumulated.g),
|
||||
native_range_compress(accumulated.b)
|
||||
);
|
||||
LightingTerms terms;
|
||||
terms.diffuse = min(compressed, vec3(1.0));
|
||||
vec3 total_specular = specular + max(compressed - vec3(1.0), vec3(0.0));
|
||||
terms.specular = vec3(
|
||||
native_specular_curve(total_specular.r),
|
||||
native_specular_curve(total_specular.g),
|
||||
native_specular_curve(total_specular.b)
|
||||
);
|
||||
return terms;
|
||||
}
|
||||
|
||||
void main() {
|
||||
out_color = in_color;
|
||||
out_uv = in_uv;
|
||||
gl_Position = camera.clip_from_world * vec4(in_position, 1.0);
|
||||
out_detail_uv = in_detail_uv;
|
||||
out_overlay_alpha = in_overlay_alpha;
|
||||
out_fog_factor = native_fog_factor(in_position);
|
||||
int mode = int(round(material.material_mode));
|
||||
int combiner_mode = mode & 3;
|
||||
if (combiner_mode == 0) {
|
||||
LightingTerms base_terms = native_material_lighting(
|
||||
in_position,
|
||||
in_normal,
|
||||
vec3(material.directional_r, material.directional_g, material.directional_b),
|
||||
vec3(material.additive_r, material.additive_g, material.additive_b),
|
||||
material_specular.base.rgb,
|
||||
material_specular.base.w
|
||||
);
|
||||
LightingTerms overlay_terms = native_material_lighting(
|
||||
in_position,
|
||||
in_normal,
|
||||
vec3(material.overlay_directional_r, material.overlay_directional_g, material.overlay_directional_b),
|
||||
vec3(material.overlay_additive_r, material.overlay_additive_g, material.overlay_additive_b),
|
||||
material_specular.overlay.rgb,
|
||||
material_specular.overlay.w
|
||||
);
|
||||
out_base_diffuse = base_terms.diffuse;
|
||||
out_base_specular = base_terms.specular;
|
||||
out_overlay_diffuse = overlay_terms.diffuse;
|
||||
out_overlay_specular = overlay_terms.specular;
|
||||
} else {
|
||||
out_base_diffuse = vec3(1.0);
|
||||
out_base_specular = vec3(0.0);
|
||||
out_overlay_diffuse = vec3(1.0);
|
||||
out_overlay_specular = vec3(0.0);
|
||||
}
|
||||
vec4 clip_position = frame.clip_from_world * vec4(in_position, 1.0);
|
||||
// Sky far-depth is a Vulkan projection choice for layers authored around
|
||||
// the camera. Keep world positions, normals, and UVs intact while placing
|
||||
// the resulting primitive at the far depth; this does not assert native
|
||||
// D3D depth-bit equivalence.
|
||||
if ((mode & 4) != 0) {
|
||||
clip_position.z = clip_position.w;
|
||||
}
|
||||
gl_Position = clip_position;
|
||||
}
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -21,16 +21,22 @@ mod swapchain_resources;
|
||||
mod validation;
|
||||
|
||||
pub use self::asset_mesh::{
|
||||
project_land_msh_to_static_mesh, project_land_msh_to_static_mesh_in_legacy_world_space,
|
||||
node38_pose, node38_pose_from_hierarchy, node38_pose_relative_to_root,
|
||||
node38_pose_relative_to_root_from_hierarchy, project_land_msh_to_static_mesh,
|
||||
project_land_msh_to_static_mesh_in_legacy_world_space,
|
||||
project_land_msh_to_static_mesh_in_world_space, project_land_msh_to_static_mesh_in_xy_frame,
|
||||
project_msh_to_static_mesh, project_msh_to_static_mesh_in_world_space,
|
||||
project_msh_to_static_mesh_in_world_space_with_node_fallback_poses,
|
||||
project_msh_to_static_mesh_in_world_space_with_node_fallback_poses_and_mount,
|
||||
project_msh_to_static_mesh_in_world_space_with_node_pose_buffer,
|
||||
project_msh_to_static_mesh_in_world_space_with_node_pose_buffer_and_mount,
|
||||
project_msh_to_static_mesh_in_world_space_with_node_sampled_poses,
|
||||
project_msh_to_static_mesh_in_world_space_with_node_sampled_poses_and_mount,
|
||||
project_msh_to_static_mesh_in_world_space_with_transform,
|
||||
project_msh_to_static_mesh_in_xy_frame,
|
||||
project_msh_to_static_mesh_in_xy_frame_with_node_fallback_poses,
|
||||
project_msh_to_static_mesh_in_xy_frame_with_node_sampled_poses, VulkanAssetMeshError,
|
||||
VulkanStaticXyFrame,
|
||||
VulkanNodePose, VulkanStaticXyFrame,
|
||||
};
|
||||
pub use self::capabilities::{
|
||||
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
|
||||
@@ -45,21 +51,27 @@ pub use self::instance::{
|
||||
use self::instance::{cstring_vec, ensure_instance_extensions_available};
|
||||
use self::resources::{
|
||||
color_subresource_range, create_command_pool, create_depth_attachment, create_frame_sync,
|
||||
create_frame_uniform_buffer, create_index_buffer, create_material_specular_buffer,
|
||||
create_readback_buffer, create_static_mesh_index_buffer, create_static_mesh_vertex_buffer,
|
||||
create_static_texture_image, destroy_allocated_buffer, destroy_allocated_image,
|
||||
destroy_depth_attachment, readback_buffer_bytes, VulkanAllocatedBuffer, VulkanAllocatedImage,
|
||||
VulkanDepthAttachment, VulkanFrameSync,
|
||||
create_static_texture_image, create_vertex_buffer, destroy_allocated_buffer,
|
||||
destroy_allocated_image, destroy_depth_attachment, readback_buffer_bytes,
|
||||
update_static_texture_image, write_frame_uniform, write_material_specular_buffer,
|
||||
write_static_mesh_index_buffer, write_static_mesh_vertex_buffer, VulkanAllocatedBuffer,
|
||||
VulkanAllocatedImage, VulkanDepthAttachment, VulkanFrameSync,
|
||||
};
|
||||
pub use self::runtime::{
|
||||
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
|
||||
VulkanLogicalDeviceError, VulkanLogicalDeviceProbe, VulkanLogicalDeviceReport,
|
||||
};
|
||||
pub use self::smoke_types::{
|
||||
VulkanDirectionalLight, VulkanDynamicDrawRange, VulkanFrameUniforms, VulkanPointLight,
|
||||
VulkanReadbackArtifact, VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot,
|
||||
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
|
||||
VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
|
||||
VulkanStaticCamera, VulkanStaticDrawRange, VulkanStaticMaterial, VulkanStaticMesh,
|
||||
VulkanStaticTexture, VulkanStaticVertex, VulkanValidationReport,
|
||||
VulkanStaticTexture, VulkanStaticTextureMip, VulkanStaticVertex, VulkanValidationReport,
|
||||
VULKAN_DIRECTIONAL_LIGHT_COUNT, VULKAN_FRAME_UNIFORM_BYTES,
|
||||
VULKAN_MATERIAL_SPECULAR_RECORD_BYTES,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use self::surface::extension_name;
|
||||
@@ -72,7 +84,8 @@ pub use self::swapchain::{
|
||||
VulkanSwapchainProbeError, VulkanSwapchainReport,
|
||||
};
|
||||
use self::swapchain_resources::{
|
||||
create_swapchain_resources, destroy_swapchain_resources, VulkanSwapchainResources,
|
||||
create_swapchain_resources, destroy_swapchain_resources, update_material_texture_descriptor,
|
||||
VulkanSwapchainResources,
|
||||
};
|
||||
use self::validation::{create_validation_messenger, VulkanValidationMessenger};
|
||||
use ash::vk;
|
||||
@@ -469,6 +482,8 @@ const LEGACY_TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[
|
||||
];
|
||||
|
||||
const fn spirv_words<const WORD_COUNT: usize>(bytes: &[u8]) -> [u32; WORD_COUNT] {
|
||||
assert!(bytes.len() % 4 == 0);
|
||||
assert!(bytes.len() == WORD_COUNT * 4);
|
||||
let mut words = [0_u32; WORD_COUNT];
|
||||
let mut index = 0;
|
||||
while index < WORD_COUNT {
|
||||
@@ -484,10 +499,12 @@ const fn spirv_words<const WORD_COUNT: usize>(bytes: &[u8]) -> [u32; WORD_COUNT]
|
||||
words
|
||||
}
|
||||
|
||||
static TRIANGLE_VERTEX_SHADER_DATA: [u32; 358] =
|
||||
spirv_words(include_bytes!("../shaders/triangle.vert.spv"));
|
||||
static TRIANGLE_FRAGMENT_SHADER_DATA: [u32; 296] =
|
||||
spirv_words(include_bytes!("../shaders/triangle.frag.spv"));
|
||||
const TRIANGLE_VERTEX_SHADER_BYTES: &[u8] = include_bytes!("../shaders/triangle.vert.spv");
|
||||
const TRIANGLE_FRAGMENT_SHADER_BYTES: &[u8] = include_bytes!("../shaders/triangle.frag.spv");
|
||||
static TRIANGLE_VERTEX_SHADER_DATA: [u32; TRIANGLE_VERTEX_SHADER_BYTES.len() / 4] =
|
||||
spirv_words(TRIANGLE_VERTEX_SHADER_BYTES);
|
||||
static TRIANGLE_FRAGMENT_SHADER_DATA: [u32; TRIANGLE_FRAGMENT_SHADER_BYTES.len() / 4] =
|
||||
spirv_words(TRIANGLE_FRAGMENT_SHADER_BYTES);
|
||||
pub(crate) const TRIANGLE_VERTEX_SHADER_WORDS: &[u32] = &TRIANGLE_VERTEX_SHADER_DATA;
|
||||
pub(crate) const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &TRIANGLE_FRAGMENT_SHADER_DATA;
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ pub(super) struct VulkanAllocatedImage {
|
||||
pub(super) image: vk::Image,
|
||||
pub(super) memory: vk::DeviceMemory,
|
||||
pub(super) view: vk::ImageView,
|
||||
pub(super) width: u32,
|
||||
pub(super) height: u32,
|
||||
pub(super) mip_levels: u32,
|
||||
}
|
||||
|
||||
/// Depth/stencil image and the exact format selected for its render pass.
|
||||
@@ -134,6 +137,9 @@ pub(super) fn create_depth_attachment(
|
||||
image,
|
||||
memory,
|
||||
view,
|
||||
width: extent.0,
|
||||
height: extent.1,
|
||||
mip_levels: 1,
|
||||
},
|
||||
format,
|
||||
})
|
||||
@@ -186,7 +192,6 @@ fn depth_aspect(format: vk::Format) -> vk::ImageAspectFlags {
|
||||
|
||||
pub(super) struct VulkanFrameSync {
|
||||
pub(super) image_available: vk::Semaphore,
|
||||
pub(super) render_finished: vk::Semaphore,
|
||||
pub(super) fence: vk::Fence,
|
||||
}
|
||||
|
||||
@@ -210,17 +215,7 @@ pub(super) fn create_static_mesh_vertex_buffer(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
mesh: &VulkanStaticMesh,
|
||||
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||
let mut bytes = Vec::with_capacity(mesh.vertices.len() * 8 * std::mem::size_of::<f32>());
|
||||
for vertex in &mesh.vertices {
|
||||
for value in vertex
|
||||
.position
|
||||
.into_iter()
|
||||
.chain(vertex.color)
|
||||
.chain(vertex.uv)
|
||||
{
|
||||
bytes.extend_from_slice(&value.to_ne_bytes());
|
||||
}
|
||||
}
|
||||
let bytes = encode_static_vertices(&mesh.vertices)?;
|
||||
create_host_visible_buffer(
|
||||
instance,
|
||||
device,
|
||||
@@ -230,34 +225,176 @@ pub(super) fn create_static_mesh_vertex_buffer(
|
||||
)
|
||||
}
|
||||
|
||||
/// Allocates a vertex buffer for a frame's retained mesh capacity.
|
||||
pub(super) fn create_vertex_buffer(
|
||||
instance: &VulkanInstanceProbe,
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
vertices: &[super::VulkanStaticVertex],
|
||||
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||
let bytes = encode_static_vertices(vertices)?;
|
||||
create_host_visible_buffer(
|
||||
instance,
|
||||
device,
|
||||
&bytes,
|
||||
vk::BufferUsageFlags::VERTEX_BUFFER,
|
||||
"dynamic mesh vertex buffer",
|
||||
)
|
||||
}
|
||||
|
||||
/// Replaces the bytes of a host-visible static vertex allocation.
|
||||
///
|
||||
/// The live renderer waits for the device before calling this helper, so a
|
||||
/// single shared allocation can safely carry camera-relative sky and weather
|
||||
/// geometry between frames.
|
||||
pub(super) fn write_static_mesh_vertex_buffer(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
buffer: &VulkanAllocatedBuffer,
|
||||
vertices: &[super::VulkanStaticVertex],
|
||||
) -> Result<(), VulkanSmokeRendererError> {
|
||||
let bytes = encode_static_vertices(vertices)?;
|
||||
write_mapped_buffer(
|
||||
device,
|
||||
buffer,
|
||||
&bytes,
|
||||
"vkMapMemory(static mesh vertex update)",
|
||||
)
|
||||
}
|
||||
|
||||
fn encode_static_vertices(
|
||||
vertices: &[super::VulkanStaticVertex],
|
||||
) -> Result<Vec<u8>, VulkanSmokeRendererError> {
|
||||
let byte_len = vertices
|
||||
.len()
|
||||
.checked_mul(14)
|
||||
.and_then(|count| count.checked_mul(std::mem::size_of::<f32>()))
|
||||
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "static mesh vertex data size overflows address space",
|
||||
})?;
|
||||
let mut bytes = Vec::new();
|
||||
bytes
|
||||
.try_reserve_exact(byte_len)
|
||||
.map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "static mesh vertex data exceeds addressable memory",
|
||||
})?;
|
||||
for vertex in vertices {
|
||||
for value in vertex
|
||||
.position
|
||||
.into_iter()
|
||||
.chain(vertex.color)
|
||||
.chain(vertex.normal)
|
||||
.chain(vertex.uv)
|
||||
.chain(vertex.detail_uv)
|
||||
.chain([vertex.overlay_alpha])
|
||||
{
|
||||
bytes.extend_from_slice(&value.to_ne_bytes());
|
||||
}
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn write_mapped_buffer(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
buffer: &VulkanAllocatedBuffer,
|
||||
bytes: &[u8],
|
||||
context: &'static str,
|
||||
) -> Result<(), VulkanSmokeRendererError> {
|
||||
let byte_len =
|
||||
u64::try_from(bytes.len()).map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "static mesh upload exceeds address space",
|
||||
})?;
|
||||
if byte_len == 0 {
|
||||
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "static mesh upload cannot be empty",
|
||||
});
|
||||
}
|
||||
// SAFETY: The caller validates that the allocation is large enough and
|
||||
// waits for all queue work that could read it before this write.
|
||||
let mapped = unsafe {
|
||||
device
|
||||
.device()
|
||||
.map_memory(buffer.memory, 0, byte_len, vk::MemoryMapFlags::empty())
|
||||
}
|
||||
.map_err(|result| VulkanSmokeRendererError::VulkanOperation { context, result })?;
|
||||
// SAFETY: `mapped` covers exactly the byte slice copied below.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>(), bytes.len());
|
||||
device.device().unmap_memory(buffer.memory);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn create_static_mesh_index_buffer(
|
||||
instance: &VulkanInstanceProbe,
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
mesh: &VulkanStaticMesh,
|
||||
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||
let mut bytes = Vec::with_capacity(mesh.indices.len() * std::mem::size_of::<u32>());
|
||||
for &index in &mesh.indices {
|
||||
bytes.extend_from_slice(&index.to_ne_bytes());
|
||||
}
|
||||
create_index_buffer(instance, device, &mesh.indices)
|
||||
}
|
||||
|
||||
/// Allocates an index buffer for a frame's retained mesh capacity.
|
||||
pub(super) fn create_index_buffer(
|
||||
instance: &VulkanInstanceProbe,
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
indices: &[u32],
|
||||
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||
let bytes = encode_static_indices(indices)?;
|
||||
create_host_visible_buffer(
|
||||
instance,
|
||||
device,
|
||||
&bytes,
|
||||
vk::BufferUsageFlags::INDEX_BUFFER,
|
||||
"static mesh index buffer",
|
||||
"dynamic mesh index buffer",
|
||||
)
|
||||
}
|
||||
|
||||
/// Replaces the bytes of a host-visible static index allocation.
|
||||
pub(super) fn write_static_mesh_index_buffer(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
buffer: &VulkanAllocatedBuffer,
|
||||
indices: &[u32],
|
||||
) -> Result<(), VulkanSmokeRendererError> {
|
||||
let bytes = encode_static_indices(indices)?;
|
||||
write_mapped_buffer(
|
||||
device,
|
||||
buffer,
|
||||
&bytes,
|
||||
"vkMapMemory(static mesh index update)",
|
||||
)
|
||||
}
|
||||
|
||||
fn encode_static_indices(indices: &[u32]) -> Result<Vec<u8>, VulkanSmokeRendererError> {
|
||||
let byte_len = indices
|
||||
.len()
|
||||
.checked_mul(std::mem::size_of::<u32>())
|
||||
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "static mesh index data size overflows address space",
|
||||
})?;
|
||||
let mut bytes = Vec::new();
|
||||
bytes
|
||||
.try_reserve_exact(byte_len)
|
||||
.map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "static mesh index data exceeds addressable memory",
|
||||
})?;
|
||||
for &index in indices {
|
||||
bytes.extend_from_slice(&index.to_ne_bytes());
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub(super) fn create_static_texture_image(
|
||||
instance: &VulkanInstanceProbe,
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
command_pool: vk::CommandPool,
|
||||
texture: &VulkanStaticTexture,
|
||||
) -> Result<VulkanAllocatedImage, VulkanSmokeRendererError> {
|
||||
texture
|
||||
.validate()
|
||||
.map_err(|context| VulkanSmokeRendererError::InvalidStaticTexture { context })?;
|
||||
let (mip_bytes, mip_levels) = build_static_texture_mip_chain(texture);
|
||||
let staging = create_host_visible_buffer(
|
||||
instance,
|
||||
device,
|
||||
&texture.rgba8,
|
||||
&mip_bytes,
|
||||
vk::BufferUsageFlags::TRANSFER_SRC,
|
||||
"static texture staging buffer",
|
||||
)?;
|
||||
@@ -269,7 +406,7 @@ pub(super) fn create_static_texture_image(
|
||||
height: texture.height,
|
||||
depth: 1,
|
||||
})
|
||||
.mip_levels(1)
|
||||
.mip_levels(mip_levels)
|
||||
.array_layers(1)
|
||||
.samples(vk::SampleCountFlags::TYPE_1)
|
||||
.tiling(vk::ImageTiling::OPTIMAL)
|
||||
@@ -336,6 +473,7 @@ pub(super) fn create_static_texture_image(
|
||||
image,
|
||||
texture.width,
|
||||
texture.height,
|
||||
mip_levels,
|
||||
) {
|
||||
// SAFETY: Both resources were created on this device and are being rolled back once.
|
||||
unsafe {
|
||||
@@ -350,7 +488,7 @@ pub(super) fn create_static_texture_image(
|
||||
.image(image)
|
||||
.view_type(vk::ImageViewType::TYPE_2D)
|
||||
.format(vk::Format::R8G8B8A8_UNORM)
|
||||
.subresource_range(color_subresource_range());
|
||||
.subresource_range(color_subresource_range(mip_levels));
|
||||
// SAFETY: The image is live, initialized and has the stated color subresource.
|
||||
let view = {
|
||||
// SAFETY: The image is live, initialized and has the stated color subresource.
|
||||
@@ -371,9 +509,211 @@ pub(super) fn create_static_texture_image(
|
||||
image,
|
||||
memory,
|
||||
view,
|
||||
width: texture.width,
|
||||
height: texture.height,
|
||||
mip_levels,
|
||||
})
|
||||
}
|
||||
|
||||
/// Allocates two aligned camera slots used by the dynamic frame UBO.
|
||||
pub(super) fn create_frame_uniform_buffer(
|
||||
instance: &VulkanInstanceProbe,
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
) -> Result<(VulkanAllocatedBuffer, u64), VulkanSmokeRendererError> {
|
||||
const FRAME_SLOTS: u64 = 2;
|
||||
const FRAME_BYTES: u64 = super::VULKAN_FRAME_UNIFORM_BYTES as u64;
|
||||
let limits =
|
||||
// SAFETY: The physical device belongs to this live instance and the query returns a value copy.
|
||||
unsafe { instance.instance.get_physical_device_properties(device.physical_device()) };
|
||||
let alignment = u64::from(limits.limits.min_uniform_buffer_offset_alignment).max(1);
|
||||
let stride = FRAME_BYTES.checked_add(alignment - 1).ok_or(
|
||||
VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "frame uniform stride overflow",
|
||||
},
|
||||
)? / alignment
|
||||
* alignment;
|
||||
let size =
|
||||
stride
|
||||
.checked_mul(FRAME_SLOTS)
|
||||
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "frame uniform buffer size overflow",
|
||||
})?;
|
||||
let bytes = vec![
|
||||
0_u8;
|
||||
usize::try_from(size).map_err(|_| {
|
||||
VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "frame uniform buffer size exceeds address space",
|
||||
}
|
||||
})?
|
||||
];
|
||||
let buffer = create_host_visible_buffer(
|
||||
instance,
|
||||
device,
|
||||
&bytes,
|
||||
vk::BufferUsageFlags::UNIFORM_BUFFER,
|
||||
"frame uniform buffer",
|
||||
)?;
|
||||
Ok((buffer, stride))
|
||||
}
|
||||
|
||||
/// Allocates one aligned storage-buffer record for every material descriptor.
|
||||
///
|
||||
/// The descriptor uses a dynamic storage-buffer offset. Keeping the stride
|
||||
/// aligned to the physical device limit lets the renderer select a material
|
||||
/// record with the same descriptor set for every draw without baking material
|
||||
/// indices into the pipeline layout.
|
||||
pub(super) fn create_material_specular_buffer(
|
||||
instance: &VulkanInstanceProbe,
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
material_count: usize,
|
||||
) -> Result<(VulkanAllocatedBuffer, u64), VulkanSmokeRendererError> {
|
||||
if material_count == 0 {
|
||||
return Err(VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "material specular buffer has no material records",
|
||||
});
|
||||
}
|
||||
let limits =
|
||||
// SAFETY: The physical device belongs to this live instance and the query returns a value copy.
|
||||
unsafe { instance.instance.get_physical_device_properties(device.physical_device()) };
|
||||
let alignment = limits.limits.min_storage_buffer_offset_alignment.max(1);
|
||||
let record_bytes = super::VULKAN_MATERIAL_SPECULAR_RECORD_BYTES as u64;
|
||||
let stride = record_bytes.checked_add(alignment - 1).ok_or(
|
||||
VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "material specular stride overflow",
|
||||
},
|
||||
)? / alignment
|
||||
* alignment;
|
||||
let count =
|
||||
u64::try_from(material_count).map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "material specular count exceeds address space",
|
||||
})?;
|
||||
let size = stride
|
||||
.checked_mul(count)
|
||||
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "material specular buffer size overflow",
|
||||
})?;
|
||||
let bytes =
|
||||
vec![
|
||||
0_u8;
|
||||
usize::try_from(size).map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh {
|
||||
context: "material specular buffer size exceeds address space",
|
||||
})?
|
||||
];
|
||||
let buffer = create_host_visible_buffer(
|
||||
instance,
|
||||
device,
|
||||
&bytes,
|
||||
vk::BufferUsageFlags::STORAGE_BUFFER,
|
||||
"material specular buffer",
|
||||
)?;
|
||||
Ok((buffer, stride))
|
||||
}
|
||||
|
||||
/// Replaces all material specular records in one host-coherent upload.
|
||||
pub(super) fn write_material_specular_buffer(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
buffer: &VulkanAllocatedBuffer,
|
||||
stride: u64,
|
||||
records: &[[[f32; 4]; 2]],
|
||||
) -> Result<(), VulkanSmokeRendererError> {
|
||||
if records.is_empty() {
|
||||
return Err(VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "material specular upload has no records",
|
||||
});
|
||||
}
|
||||
let record_bytes = super::VULKAN_MATERIAL_SPECULAR_RECORD_BYTES;
|
||||
let total_bytes = stride
|
||||
.checked_mul(u64::try_from(records.len()).unwrap_or(u64::MAX))
|
||||
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "material specular upload size",
|
||||
})?;
|
||||
let total_bytes_usize =
|
||||
usize::try_from(total_bytes).map_err(|_| VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "material specular upload size exceeds address space",
|
||||
})?;
|
||||
let mut bytes = vec![0_u8; total_bytes_usize];
|
||||
for (record_index, record) in records.iter().enumerate() {
|
||||
let record_offset = usize::try_from(
|
||||
stride
|
||||
.checked_mul(u64::try_from(record_index).unwrap_or(u64::MAX))
|
||||
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "material specular upload offset",
|
||||
})?,
|
||||
)
|
||||
.map_err(|_| VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "material specular upload offset exceeds address space",
|
||||
})?;
|
||||
let mut offset = record_offset;
|
||||
for vector in record {
|
||||
for value in vector {
|
||||
bytes[offset..offset + std::mem::size_of::<f32>()]
|
||||
.copy_from_slice(&value.to_ne_bytes());
|
||||
offset += std::mem::size_of::<f32>();
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(offset - record_offset, record_bytes);
|
||||
}
|
||||
// SAFETY: The storage buffer is host-visible and the caller has waited for
|
||||
// all prior uses before replacing its records.
|
||||
let mapped = unsafe {
|
||||
device
|
||||
.device()
|
||||
.map_memory(buffer.memory, 0, total_bytes, vk::MemoryMapFlags::empty())
|
||||
}
|
||||
.map_err(|result| VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkMapMemory(material specular buffer)",
|
||||
result,
|
||||
})?;
|
||||
// SAFETY: The mapped range is the complete upload and the destination is
|
||||
// valid for the exact byte count copied below.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>(), bytes.len());
|
||||
device.device().unmap_memory(buffer.memory);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes one complete frame block into the slot selected by the
|
||||
/// fence-safe frame index.
|
||||
pub(super) fn write_frame_uniform(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
buffer: &VulkanAllocatedBuffer,
|
||||
stride: u64,
|
||||
frame_index: usize,
|
||||
bytes: &[u8; super::VULKAN_FRAME_UNIFORM_BYTES],
|
||||
) -> Result<(), VulkanSmokeRendererError> {
|
||||
let slot =
|
||||
u64::try_from(frame_index).map_err(|_| VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "frame uniform slot index",
|
||||
})?;
|
||||
let offset = stride
|
||||
.checked_mul(slot)
|
||||
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "frame uniform slot offset",
|
||||
})?;
|
||||
// SAFETY: The offset is one of the two allocated aligned slots and the
|
||||
// mapped range is exactly one complete frame block.
|
||||
let mapped = unsafe {
|
||||
device.device().map_memory(
|
||||
buffer.memory,
|
||||
offset,
|
||||
super::VULKAN_FRAME_UNIFORM_BYTES as u64,
|
||||
vk::MemoryMapFlags::empty(),
|
||||
)
|
||||
}
|
||||
.map_err(|result| VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkMapMemory(frame uniform buffer)",
|
||||
result,
|
||||
})?;
|
||||
// SAFETY: The destination is the mapped frame slot and the byte slice lives
|
||||
// for this copy.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>(), bytes.len());
|
||||
device.device().unmap_memory(buffer.memory);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload_static_texture(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
command_pool: vk::CommandPool,
|
||||
@@ -381,6 +721,7 @@ fn upload_static_texture(
|
||||
image: vk::Image,
|
||||
width: u32,
|
||||
height: u32,
|
||||
mip_levels: u32,
|
||||
) -> Result<(), VulkanSmokeRendererError> {
|
||||
let allocate_info = vk::CommandBufferAllocateInfo::default()
|
||||
.command_pool(command_pool)
|
||||
@@ -407,7 +748,7 @@ fn upload_static_texture(
|
||||
result,
|
||||
});
|
||||
}
|
||||
let range = color_subresource_range();
|
||||
let range = color_subresource_range(mip_levels);
|
||||
let to_transfer = vk::ImageMemoryBarrier::default()
|
||||
.old_layout(vk::ImageLayout::UNDEFINED)
|
||||
.new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
|
||||
@@ -415,19 +756,34 @@ fn upload_static_texture(
|
||||
.dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
|
||||
.image(image)
|
||||
.subresource_range(range);
|
||||
let region = vk::BufferImageCopy::default()
|
||||
.image_subresource(
|
||||
vk::ImageSubresourceLayers::default()
|
||||
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
||||
.mip_level(0)
|
||||
.base_array_layer(0)
|
||||
.layer_count(1),
|
||||
)
|
||||
.image_extent(vk::Extent3D {
|
||||
width,
|
||||
height,
|
||||
depth: 1,
|
||||
});
|
||||
let mut regions = Vec::with_capacity(usize::try_from(mip_levels).unwrap_or(0));
|
||||
let mut buffer_offset = 0_u64;
|
||||
let mut mip_width = width;
|
||||
let mut mip_height = height;
|
||||
for mip_level in 0..mip_levels {
|
||||
let mip_bytes = u64::from(mip_width)
|
||||
.saturating_mul(u64::from(mip_height))
|
||||
.saturating_mul(4);
|
||||
regions.push(
|
||||
vk::BufferImageCopy::default()
|
||||
.buffer_offset(buffer_offset)
|
||||
.image_subresource(
|
||||
vk::ImageSubresourceLayers::default()
|
||||
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
||||
.mip_level(mip_level)
|
||||
.base_array_layer(0)
|
||||
.layer_count(1),
|
||||
)
|
||||
.image_extent(vk::Extent3D {
|
||||
width: mip_width,
|
||||
height: mip_height,
|
||||
depth: 1,
|
||||
}),
|
||||
);
|
||||
buffer_offset = buffer_offset.saturating_add(mip_bytes);
|
||||
mip_width = (mip_width / 2).max(1);
|
||||
mip_height = (mip_height / 2).max(1);
|
||||
}
|
||||
let to_sampled = vk::ImageMemoryBarrier::default()
|
||||
.old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
|
||||
.new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
|
||||
@@ -451,7 +807,7 @@ fn upload_static_texture(
|
||||
staging_buffer,
|
||||
image,
|
||||
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
||||
&[region],
|
||||
®ions,
|
||||
);
|
||||
device.device().cmd_pipeline_barrier(
|
||||
command_buffer,
|
||||
@@ -500,6 +856,246 @@ fn upload_static_texture(
|
||||
})
|
||||
}
|
||||
|
||||
/// Replaces the contents of an already sampled texture without changing its
|
||||
/// image view or descriptor set. The caller must wait for the renderer's
|
||||
/// submitted work before invoking this function.
|
||||
pub(super) fn update_static_texture_image(
|
||||
instance: &VulkanInstanceProbe,
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
command_pool: vk::CommandPool,
|
||||
image: &VulkanAllocatedImage,
|
||||
texture: &VulkanStaticTexture,
|
||||
) -> Result<(), VulkanSmokeRendererError> {
|
||||
texture
|
||||
.validate()
|
||||
.map_err(|context| VulkanSmokeRendererError::InvalidStaticTexture { context })?;
|
||||
let (mip_bytes, mip_levels) = build_static_texture_mip_chain(texture);
|
||||
if texture.width != image.width
|
||||
|| texture.height != image.height
|
||||
|| mip_levels != image.mip_levels
|
||||
{
|
||||
return Err(VulkanSmokeRendererError::InvalidStaticTexture {
|
||||
context: "dynamic texture extent or mip count differs from allocation",
|
||||
});
|
||||
}
|
||||
let staging = create_host_visible_buffer(
|
||||
instance,
|
||||
device,
|
||||
&mip_bytes,
|
||||
vk::BufferUsageFlags::TRANSFER_SRC,
|
||||
"dynamic texture staging buffer",
|
||||
)?;
|
||||
let allocate_info = vk::CommandBufferAllocateInfo::default()
|
||||
.command_pool(command_pool)
|
||||
.level(vk::CommandBufferLevel::PRIMARY)
|
||||
.command_buffer_count(1);
|
||||
// SAFETY: The command pool is live and owned by the current logical device.
|
||||
let command_buffer = unsafe { device.device().allocate_command_buffers(&allocate_info) }
|
||||
.map_err(|result| {
|
||||
destroy_allocated_buffer(device, &staging);
|
||||
VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkAllocateCommandBuffers(dynamic texture upload)",
|
||||
result,
|
||||
}
|
||||
})?[0];
|
||||
let begin =
|
||||
vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
|
||||
// SAFETY: The command buffer is freshly allocated from the current pool.
|
||||
if let Err(result) = unsafe { device.device().begin_command_buffer(command_buffer, &begin) } {
|
||||
// SAFETY: The command buffer belongs to this pool and is released on failure.
|
||||
unsafe {
|
||||
device
|
||||
.device()
|
||||
.free_command_buffers(command_pool, &[command_buffer]);
|
||||
};
|
||||
destroy_allocated_buffer(device, &staging);
|
||||
return Err(VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkBeginCommandBuffer(dynamic texture upload)",
|
||||
result,
|
||||
});
|
||||
}
|
||||
let range = color_subresource_range(mip_levels);
|
||||
let to_transfer = vk::ImageMemoryBarrier::default()
|
||||
.old_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
|
||||
.new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
|
||||
.src_access_mask(vk::AccessFlags::SHADER_READ)
|
||||
.dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
|
||||
.image(image.image)
|
||||
.subresource_range(range);
|
||||
let mut regions = Vec::with_capacity(usize::try_from(mip_levels).unwrap_or(0));
|
||||
let mut buffer_offset = 0_u64;
|
||||
let mut mip_width = texture.width;
|
||||
let mut mip_height = texture.height;
|
||||
for mip_level in 0..mip_levels {
|
||||
let mip_bytes = u64::from(mip_width)
|
||||
.saturating_mul(u64::from(mip_height))
|
||||
.saturating_mul(4);
|
||||
regions.push(
|
||||
vk::BufferImageCopy::default()
|
||||
.buffer_offset(buffer_offset)
|
||||
.image_subresource(
|
||||
vk::ImageSubresourceLayers::default()
|
||||
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
||||
.mip_level(mip_level)
|
||||
.base_array_layer(0)
|
||||
.layer_count(1),
|
||||
)
|
||||
.image_extent(vk::Extent3D {
|
||||
width: mip_width,
|
||||
height: mip_height,
|
||||
depth: 1,
|
||||
}),
|
||||
);
|
||||
buffer_offset = buffer_offset.saturating_add(mip_bytes);
|
||||
mip_width = (mip_width / 2).max(1);
|
||||
mip_height = (mip_height / 2).max(1);
|
||||
}
|
||||
let to_sampled = vk::ImageMemoryBarrier::default()
|
||||
.old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
|
||||
.new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
|
||||
.src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
|
||||
.dst_access_mask(vk::AccessFlags::SHADER_READ)
|
||||
.image(image.image)
|
||||
.subresource_range(range);
|
||||
// SAFETY: The caller has made all prior shader reads idle, and the image
|
||||
// remains exclusively owned by this graphics queue.
|
||||
unsafe {
|
||||
device.device().cmd_pipeline_barrier(
|
||||
command_buffer,
|
||||
vk::PipelineStageFlags::FRAGMENT_SHADER,
|
||||
vk::PipelineStageFlags::TRANSFER,
|
||||
vk::DependencyFlags::empty(),
|
||||
&[],
|
||||
&[],
|
||||
&[to_transfer],
|
||||
);
|
||||
device.device().cmd_copy_buffer_to_image(
|
||||
command_buffer,
|
||||
staging.buffer,
|
||||
image.image,
|
||||
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
||||
®ions,
|
||||
);
|
||||
device.device().cmd_pipeline_barrier(
|
||||
command_buffer,
|
||||
vk::PipelineStageFlags::TRANSFER,
|
||||
vk::PipelineStageFlags::FRAGMENT_SHADER,
|
||||
vk::DependencyFlags::empty(),
|
||||
&[],
|
||||
&[],
|
||||
&[to_sampled],
|
||||
);
|
||||
}
|
||||
// SAFETY: Command recording is complete on the current command buffer.
|
||||
if let Err(result) = unsafe { device.device().end_command_buffer(command_buffer) } {
|
||||
// SAFETY: The command buffer belongs to this pool and is released on failure.
|
||||
unsafe {
|
||||
device
|
||||
.device()
|
||||
.free_command_buffers(command_pool, &[command_buffer]);
|
||||
};
|
||||
destroy_allocated_buffer(device, &staging);
|
||||
return Err(VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkEndCommandBuffer(dynamic texture upload)",
|
||||
result,
|
||||
});
|
||||
}
|
||||
let command_buffers = [command_buffer];
|
||||
let submit = [vk::SubmitInfo::default().command_buffers(&command_buffers)];
|
||||
// SAFETY: The graphics queue and submitted command buffer remain live
|
||||
// until this synchronous wait completes.
|
||||
let submit_result = unsafe {
|
||||
device
|
||||
.device()
|
||||
.queue_submit(device.graphics_queue(), &submit, vk::Fence::null())
|
||||
};
|
||||
let result = submit_result
|
||||
.and_then(|()| unsafe { device.device().queue_wait_idle(device.graphics_queue()) });
|
||||
// SAFETY: Submission completed or failed synchronously.
|
||||
unsafe {
|
||||
device
|
||||
.device()
|
||||
.free_command_buffers(command_pool, &[command_buffer]);
|
||||
}
|
||||
destroy_allocated_buffer(device, &staging);
|
||||
result.map_err(|result| VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkQueueSubmit/WaitIdle(dynamic texture upload)",
|
||||
result,
|
||||
})
|
||||
}
|
||||
|
||||
/// Builds an RGBA8 mip chain on the CPU so texture upload does not depend on
|
||||
/// optional linear-blit support for the device's UNORM format.
|
||||
fn build_static_texture_mip_chain(texture: &VulkanStaticTexture) -> (Vec<u8>, u32) {
|
||||
if !texture.mip_levels.is_empty() {
|
||||
let mut bytes = Vec::new();
|
||||
for mip in &texture.mip_levels {
|
||||
bytes.extend_from_slice(&mip.rgba8);
|
||||
}
|
||||
return (
|
||||
bytes,
|
||||
u32::try_from(texture.mip_levels.len()).unwrap_or(u32::MAX),
|
||||
);
|
||||
}
|
||||
let mut bytes = texture.rgba8.clone();
|
||||
let mut level = texture.rgba8.clone();
|
||||
let mut width = texture.width;
|
||||
let mut height = texture.height;
|
||||
let mut mip_levels = 1_u32;
|
||||
|
||||
while width > 1 || height > 1 {
|
||||
let next_width = (width / 2).max(1);
|
||||
let next_height = (height / 2).max(1);
|
||||
let mut next = vec![
|
||||
0_u8;
|
||||
usize::try_from(next_width)
|
||||
.unwrap_or(0)
|
||||
.saturating_mul(usize::try_from(next_height).unwrap_or(0))
|
||||
.saturating_mul(4)
|
||||
];
|
||||
for y in 0..next_height {
|
||||
for x in 0..next_width {
|
||||
let dst = (usize::try_from(y).unwrap_or(0)
|
||||
* usize::try_from(next_width).unwrap_or(0)
|
||||
+ usize::try_from(x).unwrap_or(0))
|
||||
* 4;
|
||||
let x0 = x.saturating_mul(2).min(width.saturating_sub(1));
|
||||
let y0 = y.saturating_mul(2).min(height.saturating_sub(1));
|
||||
let x1 = x0.saturating_add(1).min(width.saturating_sub(1));
|
||||
let y1 = y0.saturating_add(1).min(height.saturating_sub(1));
|
||||
let source_width = usize::try_from(width).unwrap_or(0);
|
||||
let samples = [
|
||||
(usize::try_from(y0).unwrap_or(0) * source_width
|
||||
+ usize::try_from(x0).unwrap_or(0))
|
||||
* 4,
|
||||
(usize::try_from(y0).unwrap_or(0) * source_width
|
||||
+ usize::try_from(x1).unwrap_or(0))
|
||||
* 4,
|
||||
(usize::try_from(y1).unwrap_or(0) * source_width
|
||||
+ usize::try_from(x0).unwrap_or(0))
|
||||
* 4,
|
||||
(usize::try_from(y1).unwrap_or(0) * source_width
|
||||
+ usize::try_from(x1).unwrap_or(0))
|
||||
* 4,
|
||||
];
|
||||
for channel in 0..4 {
|
||||
let sum: u16 = samples
|
||||
.iter()
|
||||
.map(|&offset| u16::from(level[offset + channel]))
|
||||
.sum();
|
||||
next[dst + channel] = u8::try_from((sum + 2) / 4).unwrap_or(u8::MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
bytes.extend_from_slice(&next);
|
||||
level = next;
|
||||
width = next_width;
|
||||
height = next_height;
|
||||
mip_levels = mip_levels.saturating_add(1);
|
||||
}
|
||||
(bytes, mip_levels)
|
||||
}
|
||||
|
||||
pub(super) fn destroy_allocated_image(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
image: &VulkanAllocatedImage,
|
||||
@@ -624,36 +1220,24 @@ pub(super) fn create_frame_sync(
|
||||
let mut sync = Vec::with_capacity(2);
|
||||
for _ in 0..2 {
|
||||
// SAFETY: The sync objects belong to this live logical device and are destroyed at teardown.
|
||||
let image_available = unsafe { device.device().create_semaphore(&semaphore_info, None) }
|
||||
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkCreateSemaphore(image_available)",
|
||||
result: error,
|
||||
})?;
|
||||
let render_finished = {
|
||||
// SAFETY: The sync objects belong to this live logical device and are destroyed at teardown.
|
||||
let image_available =
|
||||
match unsafe { device.device().create_semaphore(&semaphore_info, None) } {
|
||||
Ok(render_finished) => render_finished,
|
||||
Ok(image_available) => image_available,
|
||||
Err(error) => {
|
||||
destroy_frame_sync_objects(device, &sync);
|
||||
// SAFETY: The semaphore was created above on this logical device and is destroyed on setup failure.
|
||||
unsafe { device.device().destroy_semaphore(image_available, None) };
|
||||
return Err(VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkCreateSemaphore(render_finished)",
|
||||
context: "vkCreateSemaphore(image_available)",
|
||||
result: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
// SAFETY: The fence belongs to this live logical device and is destroyed at teardown.
|
||||
let fence = match unsafe { device.device().create_fence(&fence_info, None) } {
|
||||
Ok(fence) => fence,
|
||||
Err(error) => {
|
||||
destroy_frame_sync_objects(device, &sync);
|
||||
// SAFETY: These semaphores were created above on this logical device and are destroyed on setup failure.
|
||||
unsafe {
|
||||
device.device().destroy_semaphore(image_available, None);
|
||||
device.device().destroy_semaphore(render_finished, None);
|
||||
}
|
||||
// SAFETY: The semaphore was created above on this logical device and is destroyed on setup failure.
|
||||
unsafe { device.device().destroy_semaphore(image_available, None) };
|
||||
return Err(VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkCreateFence",
|
||||
result: error,
|
||||
@@ -662,7 +1246,6 @@ pub(super) fn create_frame_sync(
|
||||
};
|
||||
sync.push(VulkanFrameSync {
|
||||
image_available,
|
||||
render_finished,
|
||||
fence,
|
||||
});
|
||||
}
|
||||
@@ -676,9 +1259,6 @@ fn destroy_frame_sync_objects(device: &VulkanLogicalDeviceProbe, sync: &[VulkanF
|
||||
device
|
||||
.device()
|
||||
.destroy_semaphore(frame_sync.image_available, None);
|
||||
device
|
||||
.device()
|
||||
.destroy_semaphore(frame_sync.render_finished, None);
|
||||
device.device().destroy_fence(frame_sync.fence, None);
|
||||
}
|
||||
}
|
||||
@@ -738,11 +1318,11 @@ pub(super) fn readback_buffer_bytes(
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub(super) fn color_subresource_range() -> vk::ImageSubresourceRange {
|
||||
pub(super) fn color_subresource_range(level_count: u32) -> vk::ImageSubresourceRange {
|
||||
vk::ImageSubresourceRange::default()
|
||||
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
||||
.base_mip_level(0)
|
||||
.level_count(1)
|
||||
.level_count(level_count)
|
||||
.base_array_layer(0)
|
||||
.layer_count(1)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -48,8 +48,20 @@ pub struct VulkanStaticVertex {
|
||||
pub position: [f32; 3],
|
||||
/// Linear RGB vertex color.
|
||||
pub color: [f32; 3],
|
||||
/// Unit vertex normal in world/object coordinates.
|
||||
pub normal: [f32; 3],
|
||||
/// Texture coordinate consumed by the static material bridge.
|
||||
pub uv: [f32; 2],
|
||||
/// Repeating detail texture coordinate from terrain type-18 UV data.
|
||||
///
|
||||
/// Non-terrain geometry supplies `[0.0; 2]`; its neutral detail texture
|
||||
/// keeps the additional stage mathematically transparent.
|
||||
pub detail_uv: [f32; 2],
|
||||
/// Interpolated terrain overlay mask from type-14 data.
|
||||
///
|
||||
/// Non-terrain geometry supplies `0.0`, disabling the transparent overlay
|
||||
/// slot while retaining one vertex contract for every static draw.
|
||||
pub overlay_alpha: f32,
|
||||
}
|
||||
|
||||
/// A static geometry camera represented in the shader's matrix memory order.
|
||||
@@ -71,7 +83,9 @@ impl VulkanStaticCamera {
|
||||
projection: LegacyD3d7Projection,
|
||||
) -> Option<Self> {
|
||||
let view = transform.try_direct3d7_view_row_major()?;
|
||||
let projection = projection.try_direct3d7_projection_row_major()?;
|
||||
let mut projection = projection.try_direct3d7_projection_row_major()?;
|
||||
// D3D7's top-down viewport and Vulkan's positive-height viewport invert NDC Y.
|
||||
projection[5] = -projection[5];
|
||||
Self::from_row_major_view_projection(view, projection)
|
||||
}
|
||||
|
||||
@@ -108,6 +122,242 @@ impl Default for VulkanStaticCamera {
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of directional records carried by [`VulkanFrameUniforms`].
|
||||
pub const VULKAN_DIRECTIONAL_LIGHT_COUNT: usize = 4;
|
||||
|
||||
/// One directional light in the frame lighting contract.
|
||||
///
|
||||
/// The direction is the vector from the light toward the surface. The
|
||||
/// coefficients are retained in the same three-component record as the
|
||||
/// native point callback so frame construction can copy the original light
|
||||
/// table without a lossy conversion.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct VulkanDirectionalLight {
|
||||
/// Direction from the light toward the surface.
|
||||
pub direction: [f32; 3],
|
||||
/// Light RGB multiplier.
|
||||
pub rgb: [f32; 3],
|
||||
/// Native light coefficients. Directional callbacks normally use the
|
||||
/// neutral `[0, 0, 1]` record; the field keeps the shared light ABI.
|
||||
pub coefficients: [f32; 3],
|
||||
/// Whether the light participates in this frame.
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
impl Default for VulkanDirectionalLight {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
direction: [0.0, 0.0, 1.0],
|
||||
rgb: [0.0; 3],
|
||||
coefficients: [0.0, 0.0, 1.0],
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One point light in the frame lighting contract.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct VulkanPointLight {
|
||||
/// World-space position.
|
||||
pub position: [f32; 3],
|
||||
/// Light RGB multiplier.
|
||||
pub rgb: [f32; 3],
|
||||
/// Radius used by native normalized distance attenuation.
|
||||
pub range: f32,
|
||||
/// Native attenuation coefficients `a0`, `a1`, and `a2`.
|
||||
pub coefficients: [f32; 3],
|
||||
/// Whether the light participates in this frame.
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
impl Default for VulkanPointLight {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: [0.0; 3],
|
||||
rgb: [0.0; 3],
|
||||
range: 0.0,
|
||||
// A neutral record is explicit. In particular, this is the
|
||||
// proven native environment contract and avoids an accidental
|
||||
// zero polynomial when a caller enables the point slot.
|
||||
coefficients: [0.0, 0.0, 1.0],
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of bytes occupied by one frame-uniform slot.
|
||||
///
|
||||
/// The block is deliberately made entirely from `mat4`/`vec4`-sized records
|
||||
/// so its CPU representation follows Vulkan's `std140` alignment rules on
|
||||
/// every device. Four directional lights and one point light fit in a small
|
||||
/// dynamic UBO while the push-constant range remains available for material
|
||||
/// state.
|
||||
pub const VULKAN_FRAME_UNIFORM_BYTES: usize = 368;
|
||||
|
||||
/// Bytes occupied by one material's base and overlay specular records.
|
||||
///
|
||||
/// Each record is one `vec4`: RGB followed by the native integer power stored
|
||||
/// as an IEEE-754 float for direct shader use. The allocation stride is
|
||||
/// rounded up to the device's `minStorageBufferOffsetAlignment`.
|
||||
pub const VULKAN_MATERIAL_SPECULAR_RECORD_BYTES: usize = 2 * 4 * std::mem::size_of::<f32>();
|
||||
|
||||
/// Per-frame data shared by the vertex and fragment stages.
|
||||
///
|
||||
/// `clip_from_world` is row-major D3D-style storage for the same reason as
|
||||
/// [`VulkanStaticCamera`]. A directional light direction points from the
|
||||
/// light toward the surface; the shader negates it when taking the
|
||||
/// surface-normal dot product. Fog distances are disabled when
|
||||
/// `fog_end <= fog_start`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct VulkanFrameUniforms {
|
||||
/// Row-major clip-from-world transform.
|
||||
pub clip_from_world: [f32; 16],
|
||||
/// Up to four native directional/celestial lights.
|
||||
pub directional_lights: [VulkanDirectionalLight; VULKAN_DIRECTIONAL_LIGHT_COUNT],
|
||||
/// Native point light slot.
|
||||
pub point_light: VulkanPointLight,
|
||||
/// Per-channel lighting floor applied before native range compression.
|
||||
pub lighting_floor: [f32; 3],
|
||||
/// Linear fog RGB color.
|
||||
pub fog_color: [f32; 3],
|
||||
/// Fog start distance.
|
||||
pub fog_start: f32,
|
||||
/// Fog end distance.
|
||||
pub fog_end: f32,
|
||||
/// World-space camera position used for fog distance.
|
||||
pub camera_position: [f32; 3],
|
||||
}
|
||||
|
||||
impl VulkanFrameUniforms {
|
||||
/// Builds neutral frame state from a static camera.
|
||||
#[must_use]
|
||||
pub fn from_camera(camera: VulkanStaticCamera) -> Self {
|
||||
let mut directional_lights =
|
||||
[VulkanDirectionalLight::default(); VULKAN_DIRECTIONAL_LIGHT_COUNT];
|
||||
directional_lights[0] = VulkanDirectionalLight {
|
||||
direction: [0.35, 0.45, 0.82],
|
||||
rgb: [1.0; 3],
|
||||
coefficients: [0.0, 0.0, 1.0],
|
||||
active: true,
|
||||
};
|
||||
Self {
|
||||
clip_from_world: camera.clip_from_world,
|
||||
directional_lights,
|
||||
point_light: VulkanPointLight::default(),
|
||||
lighting_floor: [0.30; 3],
|
||||
fog_color: [0.0; 3],
|
||||
fog_start: f32::MAX,
|
||||
fog_end: f32::MAX,
|
||||
camera_position: [0.0; 3],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether all submitted frame values are finite.
|
||||
#[must_use]
|
||||
pub fn is_finite(self) -> bool {
|
||||
self.clip_from_world
|
||||
.into_iter()
|
||||
.chain(self.directional_lights.into_iter().flat_map(|light| {
|
||||
light
|
||||
.direction
|
||||
.into_iter()
|
||||
.chain(light.rgb)
|
||||
.chain(light.coefficients)
|
||||
.chain([if light.active { 1.0 } else { 0.0 }])
|
||||
}))
|
||||
.chain(self.point_light.position)
|
||||
.chain(self.point_light.rgb)
|
||||
.chain([self.point_light.range])
|
||||
.chain(self.point_light.coefficients)
|
||||
.chain([if self.point_light.active { 1.0 } else { 0.0 }])
|
||||
.chain(self.lighting_floor)
|
||||
.chain(self.fog_color)
|
||||
.chain([self.fog_start, self.fog_end])
|
||||
.chain(self.camera_position)
|
||||
.all(f32::is_finite)
|
||||
}
|
||||
|
||||
/// Serializes one frame slot in the exact `std140` member order.
|
||||
#[must_use]
|
||||
pub fn to_ne_bytes(self) -> [u8; VULKAN_FRAME_UNIFORM_BYTES] {
|
||||
let mut values = [0.0_f32; VULKAN_FRAME_UNIFORM_BYTES / 4];
|
||||
values[..16].copy_from_slice(&self.clip_from_world);
|
||||
for (index, light) in self.directional_lights.into_iter().enumerate() {
|
||||
let direction_offset = 16 + index * 4;
|
||||
values[direction_offset..direction_offset + 4].copy_from_slice(&[
|
||||
light.direction[0],
|
||||
light.direction[1],
|
||||
light.direction[2],
|
||||
if light.active { 1.0 } else { 0.0 },
|
||||
]);
|
||||
let rgb_offset = 32 + index * 4;
|
||||
values[rgb_offset..rgb_offset + 4].copy_from_slice(&[
|
||||
light.rgb[0],
|
||||
light.rgb[1],
|
||||
light.rgb[2],
|
||||
if light.active { 1.0 } else { 0.0 },
|
||||
]);
|
||||
let coefficient_offset = 48 + index * 4;
|
||||
values[coefficient_offset..coefficient_offset + 4].copy_from_slice(&[
|
||||
light.coefficients[0],
|
||||
light.coefficients[1],
|
||||
light.coefficients[2],
|
||||
if light.active { 1.0 } else { 0.0 },
|
||||
]);
|
||||
}
|
||||
values[64..68].copy_from_slice(&[
|
||||
self.point_light.position[0],
|
||||
self.point_light.position[1],
|
||||
self.point_light.position[2],
|
||||
self.point_light.range,
|
||||
]);
|
||||
values[68..72].copy_from_slice(&[
|
||||
self.point_light.rgb[0],
|
||||
self.point_light.rgb[1],
|
||||
self.point_light.rgb[2],
|
||||
if self.point_light.active { 1.0 } else { 0.0 },
|
||||
]);
|
||||
values[72..76].copy_from_slice(&[
|
||||
self.point_light.coefficients[0],
|
||||
self.point_light.coefficients[1],
|
||||
self.point_light.coefficients[2],
|
||||
if self.point_light.active { 1.0 } else { 0.0 },
|
||||
]);
|
||||
values[76..80].copy_from_slice(&[
|
||||
self.lighting_floor[0],
|
||||
self.lighting_floor[1],
|
||||
self.lighting_floor[2],
|
||||
0.0,
|
||||
]);
|
||||
values[80..84].copy_from_slice(&[
|
||||
self.fog_color[0],
|
||||
self.fog_color[1],
|
||||
self.fog_color[2],
|
||||
0.0,
|
||||
]);
|
||||
values[84..88].copy_from_slice(&[self.fog_start, self.fog_end, 0.0, 0.0]);
|
||||
values[88..92].copy_from_slice(&[
|
||||
self.camera_position[0],
|
||||
self.camera_position[1],
|
||||
self.camera_position[2],
|
||||
0.0,
|
||||
]);
|
||||
let mut bytes = [0_u8; VULKAN_FRAME_UNIFORM_BYTES];
|
||||
for (index, value) in values.into_iter().enumerate() {
|
||||
let offset = index * std::mem::size_of::<f32>();
|
||||
bytes[offset..offset + std::mem::size_of::<f32>()]
|
||||
.copy_from_slice(&value.to_ne_bytes());
|
||||
}
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VulkanFrameUniforms {
|
||||
fn default() -> Self {
|
||||
Self::from_camera(VulkanStaticCamera::default())
|
||||
}
|
||||
}
|
||||
|
||||
fn multiply_row_major(left: [f32; 16], right: [f32; 16]) -> [f32; 16] {
|
||||
let mut result = [0.0; 16];
|
||||
for row in 0..4 {
|
||||
@@ -131,6 +381,23 @@ pub struct VulkanStaticMesh {
|
||||
pub draw_ranges: Vec<VulkanStaticDrawRange>,
|
||||
}
|
||||
|
||||
/// Geometry-only changes applied to one retained draw range.
|
||||
///
|
||||
/// The renderer owns the material, blend/depth state, active count changes,
|
||||
/// and queue classification for every range. A frame builder uses this
|
||||
/// record only when a variable-size geometry stream (such as projected
|
||||
/// shadows) moves its index segment or changes its triangle count.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct VulkanDynamicDrawRange {
|
||||
/// Stable draw-range handle returned by the initial mesh layout.
|
||||
pub range_index: usize,
|
||||
/// First index in the current shared index upload.
|
||||
pub first_index: u32,
|
||||
/// Active triangle-list index count. Zero disables this range for the
|
||||
/// current frame without allocating a zero-sized Vulkan buffer.
|
||||
pub index_count: u32,
|
||||
}
|
||||
|
||||
/// One indexed triangle-list draw retained from an original mesh batch.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct VulkanStaticDrawRange {
|
||||
@@ -140,6 +407,12 @@ pub struct VulkanStaticDrawRange {
|
||||
pub index_count: u32,
|
||||
/// Original positional `Batch20.material_index` selector.
|
||||
pub material_index: u16,
|
||||
/// Original positional `Batch20.lightmap_index` selector.
|
||||
pub lightmap_index: u8,
|
||||
/// Original MSH Batch20 flag word used by the native transparent queue.
|
||||
/// Bits `0x100` and `0x008` opt a range into that queue independently of
|
||||
/// the material blend category.
|
||||
pub batch_flags: u16,
|
||||
/// Backend-neutral fixed-function state for this source range.
|
||||
pub pipeline_state: LegacyPipelineState,
|
||||
/// Alpha-test reference in legacy 0..=255 units; dynamic material data.
|
||||
@@ -147,6 +420,13 @@ pub struct VulkanStaticDrawRange {
|
||||
}
|
||||
|
||||
impl VulkanStaticDrawRange {
|
||||
/// The value installed by the native renderer before a material-specific
|
||||
/// alpha reference is written. The native Ngi32 initialiser stores one
|
||||
/// in `D3DRS_ALPHAREF`; callers that do not have a material-specific value
|
||||
/// should pass this constant explicitly. Zero remains a valid explicit
|
||||
/// alpha reference.
|
||||
pub const NATIVE_DEFAULT_ALPHA_TEST_REFERENCE: u8 = 1;
|
||||
|
||||
/// Returns the canonical key used for Vulkan pipeline selection.
|
||||
#[must_use]
|
||||
pub fn pipeline_key(self) -> PipelineKey {
|
||||
@@ -165,12 +445,75 @@ impl VulkanStaticDrawRange {
|
||||
}
|
||||
|
||||
/// One diffuse material texture keyed by an original MSH batch selector.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VulkanStaticMaterial {
|
||||
/// Positional material selector used by one or more source batches.
|
||||
pub material_index: u16,
|
||||
/// Decoded RGBA8 diffuse texture for this selector.
|
||||
pub texture: VulkanStaticTexture,
|
||||
/// Phase parameters 4..6, normalized directional light RGB coefficient.
|
||||
pub directional_rgb: [f32; 3],
|
||||
/// Phase parameters 0..2, normalized additive RGB coefficient.
|
||||
pub additive_rgb: [f32; 3],
|
||||
/// Normalized atlas transform for base texture `[x, y, w, h]`.
|
||||
pub uv_transform: [f32; 4],
|
||||
/// Optional Land2 detail texture paired with the base texture.
|
||||
pub detail_texture: Option<VulkanStaticTexture>,
|
||||
/// Optional Land1 overlay texture selected by the face tag high byte.
|
||||
pub overlay_texture: Option<VulkanStaticTexture>,
|
||||
/// Optional Land2 detail texture paired with the overlay texture.
|
||||
pub overlay_detail_texture: Option<VulkanStaticTexture>,
|
||||
/// MAT0 phase diffuse alpha, normalized from the legacy 0..=255 byte.
|
||||
pub diffuse_alpha: f32,
|
||||
/// MAT0 phase alpha for the optional overlay material.
|
||||
pub overlay_diffuse_alpha: Option<f32>,
|
||||
/// Phase parameters 4..6 for the optional overlay material.
|
||||
pub overlay_directional_rgb: Option<[f32; 3]>,
|
||||
/// Phase parameters 0..2 for the optional overlay material.
|
||||
pub overlay_additive_rgb: Option<[f32; 3]>,
|
||||
/// Phase parameters 8..10, normalized base specular RGB coefficient.
|
||||
///
|
||||
/// Specular data is kept in a storage buffer instead of push constants so
|
||||
/// the vertex stage can evaluate the native Gouraud highlight while the
|
||||
/// material's other dynamic values retain their existing ABI.
|
||||
pub specular_rgb: [f32; 3],
|
||||
/// Phase parameter 16 for the base material, carried as the native byte.
|
||||
pub specular_power: u8,
|
||||
/// Phase parameters 8..10 for the optional overlay material.
|
||||
pub overlay_specular_rgb: Option<[f32; 3]>,
|
||||
/// Phase parameter 16 for the optional overlay material.
|
||||
pub overlay_specular_power: Option<u8>,
|
||||
/// Normalized atlas transform for the optional detail texture.
|
||||
pub detail_uv_transform: [f32; 4],
|
||||
/// Normalized atlas transform for the optional overlay texture.
|
||||
pub overlay_uv_transform: Option<[f32; 4]>,
|
||||
/// Normalized atlas transform for the optional overlay detail texture.
|
||||
pub overlay_detail_uv_transform: Option<[f32; 4]>,
|
||||
/// Environment layers bypass terrain lighting and fog while retaining the
|
||||
/// same descriptor/pipeline path.
|
||||
pub unlit: bool,
|
||||
/// Native sky mode 4: blend base and secondary textures by the secondary
|
||||
/// texture alpha before multiplying by the vertex diffuse color.
|
||||
pub sky_nebula_stars: bool,
|
||||
/// Native terrain mode 3: combine base and secondary lightmap textures
|
||||
/// without the regular terrain `2 * TEX0 * TEX1` combiner or NdotL.
|
||||
pub lightmap_mode: bool,
|
||||
/// Push material mode bit 4 and map this sky layer to the Vulkan far
|
||||
/// plane without changing its world-space vertices or UVs. This is a
|
||||
/// renderer projection choice; it is not a claim about the original D3D
|
||||
/// depth-bit encoding.
|
||||
pub sky_far_depth: bool,
|
||||
}
|
||||
|
||||
/// One authored RGBA8 mip level accepted by the Vulkan texture upload path.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct VulkanStaticTextureMip {
|
||||
/// Mip width in texels.
|
||||
pub width: u32,
|
||||
/// Mip height in texels.
|
||||
pub height: u32,
|
||||
/// Row-major RGBA8 pixels.
|
||||
pub rgba8: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Decoded RGBA8 image accepted by the initial Vulkan texture upload path.
|
||||
@@ -182,6 +525,10 @@ pub struct VulkanStaticTexture {
|
||||
pub height: u32,
|
||||
/// Row-major RGBA8 pixels.
|
||||
pub rgba8: Vec<u8>,
|
||||
/// Authored mip levels, including mip zero, when available.
|
||||
///
|
||||
/// An empty vector requests deterministic CPU generation from `rgba8`.
|
||||
pub mip_levels: Vec<VulkanStaticTextureMip>,
|
||||
}
|
||||
|
||||
impl VulkanStaticTexture {
|
||||
@@ -200,7 +547,46 @@ impl VulkanStaticTexture {
|
||||
Some(expected) if expected != self.rgba8.len() => {
|
||||
Err("static texture rgba8 byte count does not match extent")
|
||||
}
|
||||
Some(_) => Ok(()),
|
||||
Some(_) => {
|
||||
if self.mip_levels.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// TEXM files commonly stop at the authored 4x4 level. Vulkan
|
||||
// permits a partial mip chain; only the supplied level
|
||||
// extents and bytes need validation.
|
||||
if self.mip_levels.len()
|
||||
> usize::try_from(self.width.max(self.height))
|
||||
.unwrap_or(usize::MAX)
|
||||
.ilog2() as usize
|
||||
+ 1
|
||||
{
|
||||
return Err("static texture authored mip count exceeds extent");
|
||||
}
|
||||
let mut width = self.width;
|
||||
let mut height = self.height;
|
||||
for (level, mip) in self.mip_levels.iter().enumerate() {
|
||||
if mip.width != width || mip.height != height {
|
||||
return Err("static texture authored mip extent does not match level");
|
||||
}
|
||||
let expected = usize::try_from(width)
|
||||
.ok()
|
||||
.and_then(|width| {
|
||||
usize::try_from(height)
|
||||
.ok()
|
||||
.and_then(|height| width.checked_mul(height))
|
||||
})
|
||||
.and_then(|pixels| pixels.checked_mul(4));
|
||||
if expected != Some(mip.rgba8.len()) {
|
||||
return Err("static texture authored mip byte count does not match extent");
|
||||
}
|
||||
if level == 0 && mip.rgba8 != self.rgba8 {
|
||||
return Err("static texture authored mip zero does not match rgba8");
|
||||
}
|
||||
width = (width / 2).max(1);
|
||||
height = (height / 2).max(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +602,60 @@ pub(super) fn resolve_draw_texture_indices(
|
||||
) -> Result<Vec<usize>, &'static str> {
|
||||
for (index, material) in materials.iter().enumerate() {
|
||||
material.texture.validate()?;
|
||||
if let Some(texture) = &material.detail_texture {
|
||||
texture.validate()?;
|
||||
}
|
||||
if let Some(texture) = &material.overlay_texture {
|
||||
texture.validate()?;
|
||||
}
|
||||
if let Some(texture) = &material.overlay_detail_texture {
|
||||
texture.validate()?;
|
||||
}
|
||||
if !material.diffuse_alpha.is_finite() || !(0.0..=1.0).contains(&material.diffuse_alpha) {
|
||||
return Err("static material diffuse alpha must be finite and 0..1");
|
||||
}
|
||||
if material
|
||||
.directional_rgb
|
||||
.into_iter()
|
||||
.chain(material.additive_rgb)
|
||||
.chain(material.specular_rgb)
|
||||
.chain(material.overlay_specular_rgb.into_iter().flatten())
|
||||
.any(|value| !value.is_finite() || value < 0.0)
|
||||
{
|
||||
return Err("static material lighting and specular RGB parameters must be finite and non-negative");
|
||||
}
|
||||
if material
|
||||
.uv_transform
|
||||
.into_iter()
|
||||
.chain(material.detail_uv_transform)
|
||||
.any(|value| !value.is_finite())
|
||||
{
|
||||
return Err("static material UV transforms must be finite");
|
||||
}
|
||||
if material
|
||||
.overlay_diffuse_alpha
|
||||
.is_some_and(|alpha| !alpha.is_finite() || !(0.0..=1.0).contains(&alpha))
|
||||
{
|
||||
return Err("static material overlay diffuse alpha must be finite and 0..1");
|
||||
}
|
||||
if material
|
||||
.overlay_directional_rgb
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(material.overlay_additive_rgb.into_iter().flatten())
|
||||
.any(|value| !value.is_finite() || value < 0.0)
|
||||
{
|
||||
return Err("static material overlay RGB parameters must be finite and non-negative");
|
||||
}
|
||||
if material
|
||||
.overlay_uv_transform
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(material.overlay_detail_uv_transform.into_iter().flatten())
|
||||
.any(|value| !value.is_finite())
|
||||
{
|
||||
return Err("static material overlay UV transforms must be finite");
|
||||
}
|
||||
if materials[..index]
|
||||
.iter()
|
||||
.any(|previous| previous.material_index == material.material_index)
|
||||
@@ -247,17 +687,26 @@ impl VulkanStaticMesh {
|
||||
VulkanStaticVertex {
|
||||
position: [0.0, -0.55, 0.0],
|
||||
color: [1.0, 0.2, 0.2],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.5, 0.0],
|
||||
detail_uv: [0.0, 0.0],
|
||||
overlay_alpha: 0.0,
|
||||
},
|
||||
VulkanStaticVertex {
|
||||
position: [0.55, 0.55, 0.0],
|
||||
color: [0.2, 1.0, 0.2],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 1.0],
|
||||
detail_uv: [0.0, 0.0],
|
||||
overlay_alpha: 0.0,
|
||||
},
|
||||
VulkanStaticVertex {
|
||||
position: [-0.55, 0.55, 0.0],
|
||||
color: [0.2, 0.4, 1.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 1.0],
|
||||
detail_uv: [0.0, 0.0],
|
||||
overlay_alpha: 0.0,
|
||||
},
|
||||
],
|
||||
indices: vec![0, 1, 2],
|
||||
@@ -265,6 +714,8 @@ impl VulkanStaticMesh {
|
||||
first_index: 0,
|
||||
index_count: 3,
|
||||
material_index: 0,
|
||||
lightmap_index: u8::MAX,
|
||||
batch_flags: 0,
|
||||
pipeline_state: LegacyPipelineState::default(),
|
||||
alpha_test_reference: 0,
|
||||
}],
|
||||
@@ -275,6 +726,22 @@ impl VulkanStaticMesh {
|
||||
if self.vertices.is_empty() {
|
||||
return Err("static mesh has no vertices");
|
||||
}
|
||||
if self.vertices.iter().any(|vertex| {
|
||||
!vertex
|
||||
.position
|
||||
.iter()
|
||||
.chain(vertex.color.iter())
|
||||
.chain(vertex.normal.iter())
|
||||
.chain(vertex.uv.iter())
|
||||
.chain(vertex.detail_uv.iter())
|
||||
.chain(std::iter::once(&vertex.overlay_alpha))
|
||||
.all(|value| value.is_finite())
|
||||
|| !(0.0..=1.0).contains(&vertex.overlay_alpha)
|
||||
}) {
|
||||
return Err(
|
||||
"static mesh vertex attributes must be finite and overlay alpha must be 0..1",
|
||||
);
|
||||
}
|
||||
if self.indices.is_empty() || !self.indices.len().is_multiple_of(3) {
|
||||
return Err("static mesh indices must contain complete triangles");
|
||||
}
|
||||
@@ -311,6 +778,53 @@ impl VulkanStaticMesh {
|
||||
mod static_mesh_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn frame_uniforms_use_std140_sized_slots_and_preserve_dynamic_values() {
|
||||
let frame = VulkanFrameUniforms {
|
||||
clip_from_world: std::array::from_fn(|index| index as f32),
|
||||
directional_lights: [
|
||||
VulkanDirectionalLight {
|
||||
direction: [1.0, 2.0, 3.0],
|
||||
rgb: [4.0, 5.0, 6.0],
|
||||
coefficients: [7.0, 8.0, 9.0],
|
||||
active: true,
|
||||
},
|
||||
VulkanDirectionalLight::default(),
|
||||
VulkanDirectionalLight::default(),
|
||||
VulkanDirectionalLight::default(),
|
||||
],
|
||||
point_light: VulkanPointLight {
|
||||
position: [28.0, 29.0, 30.0],
|
||||
rgb: [31.0, 32.0, 33.0],
|
||||
range: 34.0,
|
||||
coefficients: [35.0, 36.0, 37.0],
|
||||
active: true,
|
||||
},
|
||||
lighting_floor: [7.0, 8.0, 9.0],
|
||||
fog_color: [10.0, 11.0, 12.0],
|
||||
fog_start: 13.0,
|
||||
fog_end: 14.0,
|
||||
camera_position: [15.0, 16.0, 17.0],
|
||||
};
|
||||
let bytes = frame.to_ne_bytes();
|
||||
assert_eq!(bytes.len(), VULKAN_FRAME_UNIFORM_BYTES);
|
||||
let read =
|
||||
|offset: usize| f32::from_ne_bytes(bytes[offset..offset + 4].try_into().expect("f32"));
|
||||
assert_eq!(read(0), 0.0);
|
||||
assert_eq!(read(60), 15.0);
|
||||
assert_eq!(read(64), 1.0);
|
||||
assert_eq!(read(76), 1.0);
|
||||
assert_eq!(read(128), 4.0);
|
||||
assert_eq!(read(192), 7.0);
|
||||
assert_eq!(read(256), 28.0);
|
||||
assert_eq!(read(272), 31.0);
|
||||
assert_eq!(read(288), 35.0);
|
||||
assert_eq!(read(304), 7.0);
|
||||
assert_eq!(read(320), 10.0);
|
||||
assert_eq!(read(336), 13.0);
|
||||
assert_eq!(read(352), 15.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smoke_triangle_is_valid_complete_geometry() {
|
||||
let mesh = VulkanStaticMesh::smoke_triangle();
|
||||
@@ -357,6 +871,54 @@ mod static_mesh_tests {
|
||||
assert_eq!(camera.clip_from_world[7], 0.65_f32.sin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_camera_maps_world_up_to_top_of_positive_height_vulkan_viewport() {
|
||||
let transform = RawCameraTransform {
|
||||
words: [
|
||||
1.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
1.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
1.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
0.0_f32.to_bits(),
|
||||
1.0_f32.to_bits(),
|
||||
],
|
||||
};
|
||||
let projection = LegacyD3d7Projection {
|
||||
viewport: [0, 0, 1024, 768],
|
||||
near_plane: 0.5,
|
||||
far_plane: 700.0,
|
||||
field_of_view_radians: 1.3,
|
||||
};
|
||||
|
||||
let camera = VulkanStaticCamera::from_legacy_d3d7(transform, projection)
|
||||
.expect("recovered camera inputs are valid");
|
||||
// The raw identity camera's recovered view maps world +X to depth
|
||||
// and world +Z to camera up.
|
||||
let world_up_at_positive_depth = [10.0, 0.0, 1.0, 1.0];
|
||||
let clip_y = (0..4)
|
||||
.map(|row| world_up_at_positive_depth[row] * camera.clip_from_world[row * 4 + 1])
|
||||
.sum::<f32>();
|
||||
let clip_w = (0..4)
|
||||
.map(|row| world_up_at_positive_depth[row] * camera.clip_from_world[row * 4 + 3])
|
||||
.sum::<f32>();
|
||||
|
||||
assert!(clip_w > 0.0);
|
||||
assert!(
|
||||
clip_y / clip_w < 0.0,
|
||||
"world up must map to the viewport top"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_camera_accepts_finite_runtime_matrices_and_rejects_nan() {
|
||||
let identity = VulkanStaticCamera::default().clip_from_world;
|
||||
@@ -387,6 +949,8 @@ mod static_mesh_tests {
|
||||
first_index: 0,
|
||||
index_count: 2,
|
||||
material_index: 0,
|
||||
lightmap_index: u8::MAX,
|
||||
batch_flags: 0,
|
||||
pipeline_state: LegacyPipelineState::default(),
|
||||
alpha_test_reference: 0,
|
||||
}],
|
||||
@@ -415,6 +979,8 @@ mod static_mesh_tests {
|
||||
first_index: 0,
|
||||
index_count: 3,
|
||||
material_index: 7,
|
||||
lightmap_index: u8::MAX,
|
||||
batch_flags: 0,
|
||||
pipeline_state: LegacyPipelineState::default(),
|
||||
alpha_test_reference: 0,
|
||||
},
|
||||
@@ -422,6 +988,8 @@ mod static_mesh_tests {
|
||||
first_index: 3,
|
||||
index_count: 3,
|
||||
material_index: 2,
|
||||
lightmap_index: u8::MAX,
|
||||
batch_flags: 0,
|
||||
pipeline_state: LegacyPipelineState::default(),
|
||||
alpha_test_reference: 0,
|
||||
},
|
||||
@@ -430,15 +998,58 @@ mod static_mesh_tests {
|
||||
width: 1,
|
||||
height: 1,
|
||||
rgba8: vec![255; 4],
|
||||
mip_levels: Vec::new(),
|
||||
};
|
||||
let materials = [
|
||||
VulkanStaticMaterial {
|
||||
material_index: 2,
|
||||
texture: texture(),
|
||||
detail_texture: None,
|
||||
overlay_texture: None,
|
||||
overlay_detail_texture: None,
|
||||
diffuse_alpha: 1.0,
|
||||
overlay_diffuse_alpha: None,
|
||||
directional_rgb: [1.0; 3],
|
||||
additive_rgb: [0.0; 3],
|
||||
specular_rgb: [0.0; 3],
|
||||
specular_power: 0,
|
||||
overlay_specular_rgb: None,
|
||||
overlay_specular_power: None,
|
||||
uv_transform: [0.0, 0.0, 1.0, 1.0],
|
||||
overlay_directional_rgb: None,
|
||||
overlay_additive_rgb: None,
|
||||
detail_uv_transform: [0.0, 0.0, 1.0, 1.0],
|
||||
overlay_uv_transform: None,
|
||||
overlay_detail_uv_transform: None,
|
||||
unlit: false,
|
||||
sky_nebula_stars: false,
|
||||
lightmap_mode: false,
|
||||
sky_far_depth: false,
|
||||
},
|
||||
VulkanStaticMaterial {
|
||||
material_index: 7,
|
||||
texture: texture(),
|
||||
detail_texture: None,
|
||||
overlay_texture: None,
|
||||
overlay_detail_texture: None,
|
||||
diffuse_alpha: 1.0,
|
||||
overlay_diffuse_alpha: None,
|
||||
directional_rgb: [1.0; 3],
|
||||
additive_rgb: [0.0; 3],
|
||||
specular_rgb: [0.0; 3],
|
||||
specular_power: 0,
|
||||
overlay_specular_rgb: None,
|
||||
overlay_specular_power: None,
|
||||
uv_transform: [0.0, 0.0, 1.0, 1.0],
|
||||
overlay_directional_rgb: None,
|
||||
overlay_additive_rgb: None,
|
||||
detail_uv_transform: [0.0, 0.0, 1.0, 1.0],
|
||||
overlay_uv_transform: None,
|
||||
overlay_detail_uv_transform: None,
|
||||
unlit: false,
|
||||
sky_nebula_stars: false,
|
||||
lightmap_mode: false,
|
||||
sky_far_depth: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -480,6 +1091,24 @@ mod static_mesh_tests {
|
||||
};
|
||||
assert_eq!(disabled.alpha_test_cutoff(), 0.0);
|
||||
assert_eq!(enabled.alpha_test_cutoff(), 128.0 / 255.0);
|
||||
let explicit_zero = VulkanStaticDrawRange {
|
||||
pipeline_state: LegacyPipelineState {
|
||||
alpha_test: true,
|
||||
..LegacyPipelineState::default()
|
||||
},
|
||||
alpha_test_reference: 0,
|
||||
..base
|
||||
};
|
||||
assert_eq!(explicit_zero.alpha_test_cutoff(), 0.0);
|
||||
let native_default = VulkanStaticDrawRange {
|
||||
pipeline_state: LegacyPipelineState {
|
||||
alpha_test: true,
|
||||
..LegacyPipelineState::default()
|
||||
},
|
||||
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
||||
..base
|
||||
};
|
||||
assert_eq!(native_default.alpha_test_cutoff(), 1.0 / 255.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,10 +1356,46 @@ pub struct VulkanSmokeRenderer {
|
||||
pub(super) swapchain_resources: Option<VulkanSwapchainResources>,
|
||||
pub(super) vertex_buffer: Option<VulkanAllocatedBuffer>,
|
||||
pub(super) index_buffer: Option<VulkanAllocatedBuffer>,
|
||||
/// Positional material slots mapped to the owned image list. Fallback
|
||||
/// stages may share one image without coupling independently animated
|
||||
/// material textures.
|
||||
pub(super) texture_slot_indices: Vec<usize>,
|
||||
pub(super) textures: Vec<VulkanAllocatedImage>,
|
||||
pub(super) vertex_count: usize,
|
||||
/// Retained index allocation length. The upload path grows this only when
|
||||
/// a frame actually needs more shared indices and never shrinks it.
|
||||
pub(super) index_capacity: usize,
|
||||
pub(super) draw_range_capacities: Vec<u32>,
|
||||
pub(super) draw_ranges: Vec<VulkanStaticDrawRange>,
|
||||
pub(super) draw_texture_indices: Vec<usize>,
|
||||
/// Stable range handles used when recording the native pass order.
|
||||
pub(super) draw_order: Vec<usize>,
|
||||
/// Native camera-sort values returned by the draw-item camera callback.
|
||||
pub(super) draw_range_sort_keys: Vec<Option<f32>>,
|
||||
/// Whether the stable order must be rebuilt before the next command
|
||||
/// buffer. Range setters are called in batches by the game loop.
|
||||
pub(super) draw_order_dirty: bool,
|
||||
/// World queue classification for each stable range handle. `None` keeps
|
||||
/// 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>>,
|
||||
/// Pipeline state before transparent queue depth policy is applied.
|
||||
pub(super) draw_range_base_pipeline_states: Vec<LegacyPipelineState>,
|
||||
pub(super) material_alphas: Vec<[f32; 2]>,
|
||||
pub(super) material_lighting: Vec<[[f32; 3]; 4]>,
|
||||
pub(super) material_uv_transforms: Vec<[[f32; 4]; 4]>,
|
||||
/// Base and overlay specular records uploaded to the dynamic SSBO.
|
||||
pub(super) material_specular: Vec<[[f32; 4]; 2]>,
|
||||
/// Original selector for each positional material record.
|
||||
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.
|
||||
pub(super) material_unlit: Vec<f32>,
|
||||
pub(super) readback_enabled: bool,
|
||||
pub(super) camera: VulkanStaticCamera,
|
||||
pub(super) frame_uniforms: VulkanFrameUniforms,
|
||||
pub(super) frame_sync: Vec<VulkanFrameSync>,
|
||||
pub(super) images_in_flight: Vec<vk::Fence>,
|
||||
pub(super) current_frame: usize,
|
||||
|
||||
@@ -8,38 +8,85 @@ use fparkan_render::{
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::{
|
||||
color_subresource_range, create_depth_attachment, destroy_depth_attachment,
|
||||
VulkanAllocatedBuffer, VulkanAllocatedImage, VulkanDepthAttachment, VulkanInstanceProbe,
|
||||
VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanSwapchainProbe,
|
||||
TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS,
|
||||
color_subresource_range, create_depth_attachment, create_material_specular_buffer,
|
||||
destroy_depth_attachment, VulkanAllocatedBuffer, VulkanAllocatedImage, VulkanDepthAttachment,
|
||||
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanSwapchainProbe,
|
||||
TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS, VULKAN_FRAME_UNIFORM_BYTES,
|
||||
VULKAN_MATERIAL_SPECULAR_RECORD_BYTES,
|
||||
};
|
||||
|
||||
pub(super) struct VulkanSwapchainResources {
|
||||
pub(super) image_views: Vec<vk::ImageView>,
|
||||
pub(super) images: Vec<vk::Image>,
|
||||
pub(super) readback_buffers: Vec<VulkanAllocatedBuffer>,
|
||||
pub(super) depth_attachment: VulkanDepthAttachment,
|
||||
/// One depth image per swapchain image so frames can overlap safely.
|
||||
pub(super) depth_attachments: Vec<VulkanDepthAttachment>,
|
||||
/// A render-finished semaphore dedicated to each swapchain image.
|
||||
pub(super) render_finished: Vec<vk::Semaphore>,
|
||||
pub(super) render_pass: vk::RenderPass,
|
||||
pub(super) pipeline_layout: vk::PipelineLayout,
|
||||
pub(super) descriptor_set_layout: vk::DescriptorSetLayout,
|
||||
pub(super) descriptor_pool: vk::DescriptorPool,
|
||||
pub(super) descriptor_sets: Vec<vk::DescriptorSet>,
|
||||
pub(super) sampler: vk::Sampler,
|
||||
pub(super) frame_uniform_buffer: VulkanAllocatedBuffer,
|
||||
pub(super) frame_uniform_stride: u64,
|
||||
pub(super) material_specular_buffer: VulkanAllocatedBuffer,
|
||||
pub(super) material_specular_stride: u64,
|
||||
/// Live graphics pipelines indexed by canonical backend-neutral state.
|
||||
pub(super) pipelines: BTreeMap<PipelineKey, vk::Pipeline>,
|
||||
pub(super) framebuffers: Vec<vk::Framebuffer>,
|
||||
pub(super) command_buffers: Vec<vk::CommandBuffer>,
|
||||
}
|
||||
|
||||
pub(super) fn update_material_texture_descriptor(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
resources: &VulkanSwapchainResources,
|
||||
material_index: usize,
|
||||
texture_slot: usize,
|
||||
texture: &VulkanAllocatedImage,
|
||||
) -> Result<(), VulkanSmokeRendererError> {
|
||||
if texture_slot >= 4 {
|
||||
return Err(VulkanSmokeRendererError::InvalidStaticTexture {
|
||||
context: "dynamic material texture slot is out of bounds",
|
||||
});
|
||||
}
|
||||
let descriptor_set = resources
|
||||
.descriptor_sets
|
||||
.get(material_index)
|
||||
.copied()
|
||||
.ok_or(VulkanSmokeRendererError::InvalidStaticTexture {
|
||||
context: "dynamic material descriptor set is out of bounds",
|
||||
})?;
|
||||
let image_info = vk::DescriptorImageInfo::default()
|
||||
.sampler(resources.sampler)
|
||||
.image_view(texture.view)
|
||||
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
|
||||
let write = vk::WriteDescriptorSet::default()
|
||||
.dst_set(descriptor_set)
|
||||
.dst_binding(u32::try_from(texture_slot).unwrap_or(u32::MAX))
|
||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||
.image_info(std::slice::from_ref(&image_info));
|
||||
// SAFETY: the renderer idles the device before changing a descriptor that
|
||||
// may be referenced by an in-flight command buffer.
|
||||
unsafe { device.device().update_descriptor_sets(&[write], &[]) };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct PartialSwapchainResources {
|
||||
image_views: Vec<vk::ImageView>,
|
||||
readback_buffers: Vec<VulkanAllocatedBuffer>,
|
||||
depth_attachment: Option<VulkanDepthAttachment>,
|
||||
depth_attachments: Vec<VulkanDepthAttachment>,
|
||||
render_finished: Vec<vk::Semaphore>,
|
||||
render_pass: Option<vk::RenderPass>,
|
||||
pipeline_layout: Option<vk::PipelineLayout>,
|
||||
descriptor_set_layout: Option<vk::DescriptorSetLayout>,
|
||||
descriptor_pool: Option<vk::DescriptorPool>,
|
||||
sampler: Option<vk::Sampler>,
|
||||
frame_uniform_buffer: Option<VulkanAllocatedBuffer>,
|
||||
frame_uniform_stride: u64,
|
||||
material_specular_buffer: Option<VulkanAllocatedBuffer>,
|
||||
material_specular_stride: u64,
|
||||
pipelines: BTreeMap<PipelineKey, vk::Pipeline>,
|
||||
framebuffers: Vec<vk::Framebuffer>,
|
||||
command_buffers: Vec<vk::CommandBuffer>,
|
||||
@@ -53,6 +100,7 @@ pub(super) fn create_swapchain_resources(
|
||||
vertex_buffer: &VulkanAllocatedBuffer,
|
||||
index_buffer: &VulkanAllocatedBuffer,
|
||||
textures: &[VulkanAllocatedImage],
|
||||
texture_slot_indices: &[usize],
|
||||
draw_ranges: &[super::VulkanStaticDrawRange],
|
||||
depth_request: DepthStencilSupport,
|
||||
reuse_command_pool: bool,
|
||||
@@ -74,16 +122,57 @@ pub(super) fn create_swapchain_resources(
|
||||
swapchain.report.plan.format.format,
|
||||
)?,
|
||||
readback_buffers: Vec::new(),
|
||||
depth_attachment: None,
|
||||
depth_attachments: Vec::new(),
|
||||
render_finished: Vec::new(),
|
||||
render_pass: None,
|
||||
pipeline_layout: None,
|
||||
descriptor_set_layout: None,
|
||||
descriptor_pool: None,
|
||||
sampler: None,
|
||||
frame_uniform_buffer: None,
|
||||
frame_uniform_stride: 0,
|
||||
material_specular_buffer: None,
|
||||
material_specular_stride: 0,
|
||||
pipelines: BTreeMap::new(),
|
||||
framebuffers: Vec::new(),
|
||||
command_buffers: Vec::new(),
|
||||
};
|
||||
let semaphore_info = vk::SemaphoreCreateInfo::default();
|
||||
for _ in &images {
|
||||
// SAFETY: The semaphore belongs to this live logical device and is
|
||||
// destroyed with the swapchain resources.
|
||||
match unsafe { device.device().create_semaphore(&semaphore_info, None) } {
|
||||
Ok(semaphore) => partial.render_finished.push(semaphore),
|
||||
Err(result) => {
|
||||
destroy_partial_swapchain_resources(device, command_pool, partial);
|
||||
return Err(VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkCreateSemaphore(render_finished)",
|
||||
result,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let (frame_uniform_buffer, frame_uniform_stride) =
|
||||
match super::create_frame_uniform_buffer(instance, device) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(error) => {
|
||||
destroy_partial_swapchain_resources(device, command_pool, partial);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
partial.frame_uniform_stride = frame_uniform_stride;
|
||||
partial.frame_uniform_buffer = Some(frame_uniform_buffer);
|
||||
let material_count = texture_slot_indices.len() / 4;
|
||||
let (material_specular_buffer, material_specular_stride) =
|
||||
match create_material_specular_buffer(instance, device, material_count) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(error) => {
|
||||
destroy_partial_swapchain_resources(device, command_pool, partial);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
partial.material_specular_stride = material_specular_stride;
|
||||
partial.material_specular_buffer = Some(material_specular_buffer);
|
||||
let (
|
||||
depth_attachment,
|
||||
render_pass,
|
||||
@@ -99,8 +188,17 @@ pub(super) fn create_swapchain_resources(
|
||||
swapchain.report.plan.format.format,
|
||||
swapchain.report.plan.extent,
|
||||
textures,
|
||||
texture_slot_indices,
|
||||
draw_ranges,
|
||||
depth_request,
|
||||
partial
|
||||
.frame_uniform_buffer
|
||||
.as_ref()
|
||||
.expect("frame uniform buffer is present before descriptor creation"),
|
||||
partial
|
||||
.material_specular_buffer
|
||||
.as_ref()
|
||||
.expect("material specular buffer is present before descriptor creation"),
|
||||
) {
|
||||
Ok(bundle) => bundle,
|
||||
Err(error) => {
|
||||
@@ -108,19 +206,35 @@ pub(super) fn create_swapchain_resources(
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let depth_view = depth_attachment.image.view;
|
||||
partial.depth_attachment = Some(depth_attachment);
|
||||
// Keep every handle from the bundle in the partial owner before creating
|
||||
// the remaining per-image depth attachments. That loop can fail after the
|
||||
// first attachment, and its error path must destroy the whole bundle.
|
||||
partial.depth_attachments.push(depth_attachment);
|
||||
partial.render_pass = Some(render_pass);
|
||||
partial.pipeline_layout = Some(pipeline_layout);
|
||||
partial.descriptor_set_layout = Some(descriptor_set_layout);
|
||||
partial.descriptor_pool = Some(descriptor_pool);
|
||||
partial.sampler = Some(sampler);
|
||||
partial.pipelines = pipelines;
|
||||
for _ in 1..images.len() {
|
||||
match create_depth_attachment(
|
||||
instance,
|
||||
device,
|
||||
swapchain.report.plan.extent,
|
||||
depth_request,
|
||||
) {
|
||||
Ok(attachment) => partial.depth_attachments.push(attachment),
|
||||
Err(error) => {
|
||||
destroy_partial_swapchain_resources(device, command_pool, partial);
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
let framebuffers = match create_swapchain_framebuffers(
|
||||
device,
|
||||
render_pass,
|
||||
&partial.image_views,
|
||||
depth_view,
|
||||
&partial.depth_attachments,
|
||||
swapchain.report.plan.extent,
|
||||
) {
|
||||
Ok(framebuffers) => framebuffers,
|
||||
@@ -168,24 +282,44 @@ pub(super) fn create_swapchain_resources(
|
||||
};
|
||||
partial.command_buffers = command_buffers;
|
||||
let _ = (vertex_buffer, index_buffer);
|
||||
let depth_attachment =
|
||||
partial
|
||||
.depth_attachment
|
||||
.take()
|
||||
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "depth attachment ownership after swapchain setup",
|
||||
})?;
|
||||
if partial.depth_attachments.len() != images.len()
|
||||
|| partial.render_finished.len() != images.len()
|
||||
{
|
||||
destroy_partial_swapchain_resources(device, command_pool, partial);
|
||||
return Err(VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "swapchain per-image resource count after setup",
|
||||
});
|
||||
}
|
||||
Ok(VulkanSwapchainResources {
|
||||
image_views: partial.image_views,
|
||||
images,
|
||||
readback_buffers: partial.readback_buffers,
|
||||
depth_attachment,
|
||||
render_pass,
|
||||
pipeline_layout,
|
||||
descriptor_set_layout,
|
||||
descriptor_pool,
|
||||
depth_attachments: partial.depth_attachments,
|
||||
render_finished: partial.render_finished,
|
||||
render_pass: partial
|
||||
.render_pass
|
||||
.expect("render pass is present after successful swapchain setup"),
|
||||
pipeline_layout: partial
|
||||
.pipeline_layout
|
||||
.expect("pipeline layout is present after successful swapchain setup"),
|
||||
descriptor_set_layout: partial
|
||||
.descriptor_set_layout
|
||||
.expect("descriptor set layout is present after successful swapchain setup"),
|
||||
descriptor_pool: partial
|
||||
.descriptor_pool
|
||||
.expect("descriptor pool is present after successful swapchain setup"),
|
||||
descriptor_sets,
|
||||
sampler,
|
||||
sampler: partial
|
||||
.sampler
|
||||
.expect("sampler is present after successful swapchain setup"),
|
||||
frame_uniform_buffer: partial
|
||||
.frame_uniform_buffer
|
||||
.expect("frame uniform buffer is present after successful swapchain setup"),
|
||||
frame_uniform_stride: partial.frame_uniform_stride,
|
||||
material_specular_buffer: partial
|
||||
.material_specular_buffer
|
||||
.expect("material specular buffer is present after successful swapchain setup"),
|
||||
material_specular_stride: partial.material_specular_stride,
|
||||
pipelines: partial.pipelines,
|
||||
framebuffers: partial.framebuffers,
|
||||
command_buffers: partial.command_buffers,
|
||||
@@ -210,8 +344,11 @@ fn create_swapchain_pipeline_bundle(
|
||||
format: i32,
|
||||
extent: (u32, u32),
|
||||
textures: &[VulkanAllocatedImage],
|
||||
texture_slot_indices: &[usize],
|
||||
draw_ranges: &[super::VulkanStaticDrawRange],
|
||||
depth_request: DepthStencilSupport,
|
||||
frame_uniform_buffer: &VulkanAllocatedBuffer,
|
||||
material_specular_buffer: &VulkanAllocatedBuffer,
|
||||
) -> Result<
|
||||
(
|
||||
VulkanDepthAttachment,
|
||||
@@ -234,7 +371,14 @@ fn create_swapchain_pipeline_bundle(
|
||||
}
|
||||
};
|
||||
let (descriptor_set_layout, descriptor_pool, descriptor_sets, sampler) =
|
||||
create_texture_descriptor_bundle(device, textures).inspect_err(|_| {
|
||||
create_texture_descriptor_bundle(
|
||||
device,
|
||||
textures,
|
||||
texture_slot_indices,
|
||||
frame_uniform_buffer,
|
||||
material_specular_buffer,
|
||||
)
|
||||
.inspect_err(|_| {
|
||||
// SAFETY: The render pass was created above on this live logical device and is destroyed on setup failure.
|
||||
unsafe { device.device().destroy_render_pass(render_pass, None) };
|
||||
destroy_depth_attachment(device, &depth_attachment);
|
||||
@@ -289,12 +433,23 @@ fn create_swapchain_framebuffers(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
render_pass: vk::RenderPass,
|
||||
image_views: &[vk::ImageView],
|
||||
depth_view: vk::ImageView,
|
||||
depth_attachments: &[VulkanDepthAttachment],
|
||||
extent: (u32, u32),
|
||||
) -> Result<Vec<vk::Framebuffer>, VulkanSmokeRendererError> {
|
||||
if image_views.len() != depth_attachments.len() {
|
||||
return Err(VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "swapchain color/depth attachment count",
|
||||
});
|
||||
}
|
||||
let mut framebuffers = Vec::with_capacity(image_views.len());
|
||||
for image_view in image_views.iter().copied() {
|
||||
match create_framebuffer(device, render_pass, image_view, depth_view, extent) {
|
||||
for (image_view, depth_attachment) in image_views.iter().copied().zip(depth_attachments) {
|
||||
match create_framebuffer(
|
||||
device,
|
||||
render_pass,
|
||||
image_view,
|
||||
depth_attachment.image.view,
|
||||
extent,
|
||||
) {
|
||||
Ok(framebuffer) => framebuffers.push(framebuffer),
|
||||
Err(error) => {
|
||||
// SAFETY: These framebuffers were created above on this live logical device and are destroyed on setup failure.
|
||||
@@ -339,7 +494,7 @@ fn create_image_view(
|
||||
.image(image)
|
||||
.view_type(vk::ImageViewType::TYPE_2D)
|
||||
.format(vk::Format::from_raw(format))
|
||||
.subresource_range(color_subresource_range());
|
||||
.subresource_range(color_subresource_range(1));
|
||||
// SAFETY: The image comes from the live swapchain and the subresource range covers its color aspect.
|
||||
unsafe { device.device().create_image_view(&create_info, None) }.map_err(|error| {
|
||||
VulkanSmokeRendererError::VulkanOperation {
|
||||
@@ -384,10 +539,21 @@ fn create_render_pass(
|
||||
let dependency = vk::SubpassDependency::default()
|
||||
.src_subpass(vk::SUBPASS_EXTERNAL)
|
||||
.dst_subpass(0)
|
||||
.src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
|
||||
.dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
|
||||
.src_stage_mask(
|
||||
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
|
||||
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
|
||||
| vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
|
||||
)
|
||||
.dst_stage_mask(
|
||||
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
|
||||
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
|
||||
| vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
|
||||
)
|
||||
.src_access_mask(vk::AccessFlags::empty())
|
||||
.dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
|
||||
.dst_access_mask(
|
||||
vk::AccessFlags::COLOR_ATTACHMENT_WRITE
|
||||
| vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
|
||||
);
|
||||
let attachments = [color_attachment, depth_attachment];
|
||||
let subpasses = [subpass];
|
||||
let dependencies = [dependency];
|
||||
@@ -409,16 +575,16 @@ fn create_pipeline_layout(
|
||||
descriptor_set_layout: vk::DescriptorSetLayout,
|
||||
) -> Result<vk::PipelineLayout, VulkanSmokeRendererError> {
|
||||
let set_layouts = [descriptor_set_layout];
|
||||
let push_constant_ranges = [
|
||||
vk::PushConstantRange::default()
|
||||
.stage_flags(vk::ShaderStageFlags::VERTEX)
|
||||
.offset(0)
|
||||
.size(64),
|
||||
vk::PushConstantRange::default()
|
||||
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
|
||||
.offset(64)
|
||||
.size(u32::try_from(std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
||||
];
|
||||
// Material data occupies one 128-byte push-constant allocation shared by
|
||||
// both stages; the vertex stage also reads its specular pair from the
|
||||
// dynamic storage buffer at descriptor binding 5. Camera data is supplied
|
||||
// by the dynamic frame UBO at descriptor binding 4.
|
||||
// Expose the complete block from offset zero so drivers validate the
|
||||
// fragment member's declared offset against the pipeline layout.
|
||||
let push_constant_ranges = [vk::PushConstantRange::default()
|
||||
.stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT)
|
||||
.offset(0)
|
||||
.size(32 * u32::try_from(std::mem::size_of::<f32>()).unwrap_or(u32::MAX))];
|
||||
let create_info = vk::PipelineLayoutCreateInfo::default()
|
||||
.set_layouts(&set_layouts)
|
||||
.push_constant_ranges(&push_constant_ranges);
|
||||
@@ -434,6 +600,9 @@ fn create_pipeline_layout(
|
||||
fn create_texture_descriptor_bundle(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
textures: &[VulkanAllocatedImage],
|
||||
texture_slot_indices: &[usize],
|
||||
frame_uniform_buffer: &VulkanAllocatedBuffer,
|
||||
material_specular_buffer: &VulkanAllocatedBuffer,
|
||||
) -> Result<
|
||||
(
|
||||
vk::DescriptorSetLayout,
|
||||
@@ -443,12 +612,32 @@ fn create_texture_descriptor_bundle(
|
||||
),
|
||||
VulkanSmokeRendererError,
|
||||
> {
|
||||
let binding = vk::DescriptorSetLayoutBinding::default()
|
||||
.binding(0)
|
||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||
.descriptor_count(1)
|
||||
.stage_flags(vk::ShaderStageFlags::FRAGMENT);
|
||||
let bindings = [binding];
|
||||
const TEXTURES_PER_MATERIAL: usize = 4;
|
||||
const FRAME_BINDING: u32 = 4;
|
||||
const MATERIAL_SPECULAR_BINDING: u32 = 5;
|
||||
let mut bindings = (0..TEXTURES_PER_MATERIAL)
|
||||
.map(|binding| {
|
||||
vk::DescriptorSetLayoutBinding::default()
|
||||
.binding(u32::try_from(binding).unwrap_or(u32::MAX))
|
||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||
.descriptor_count(1)
|
||||
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
bindings.push(
|
||||
vk::DescriptorSetLayoutBinding::default()
|
||||
.binding(FRAME_BINDING)
|
||||
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
|
||||
.descriptor_count(1)
|
||||
.stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT),
|
||||
);
|
||||
bindings.push(
|
||||
vk::DescriptorSetLayoutBinding::default()
|
||||
.binding(MATERIAL_SPECULAR_BINDING)
|
||||
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
|
||||
.descriptor_count(1)
|
||||
.stage_flags(vk::ShaderStageFlags::VERTEX),
|
||||
);
|
||||
let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings);
|
||||
// SAFETY: The layout description is stack-owned and references no external memory.
|
||||
let layout = unsafe {
|
||||
@@ -460,23 +649,40 @@ fn create_texture_descriptor_bundle(
|
||||
context: "vkCreateDescriptorSetLayout",
|
||||
result,
|
||||
})?;
|
||||
let texture_count = u32::try_from(textures.len()).map_err(|_| {
|
||||
let texture_count = u32::try_from(texture_slot_indices.len()).map_err(|_| {
|
||||
VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "static material texture count exceeds Vulkan descriptor limit",
|
||||
}
|
||||
})?;
|
||||
if texture_count == 0 {
|
||||
if texture_count == 0
|
||||
|| !texture_slot_indices
|
||||
.len()
|
||||
.is_multiple_of(TEXTURES_PER_MATERIAL)
|
||||
|| texture_slot_indices
|
||||
.iter()
|
||||
.any(|texture_index| *texture_index >= textures.len())
|
||||
{
|
||||
// SAFETY: The layout was created above on this device and is rolled
|
||||
// back before reporting malformed material-slot input.
|
||||
unsafe { device.device().destroy_descriptor_set_layout(layout, None) };
|
||||
return Err(VulkanSmokeRendererError::InvariantViolation {
|
||||
context: "static material texture list is empty",
|
||||
context: "static material texture slots are invalid",
|
||||
});
|
||||
}
|
||||
let material_count = texture_count / u32::try_from(TEXTURES_PER_MATERIAL).unwrap_or(1);
|
||||
let pool_size = vk::DescriptorPoolSize::default()
|
||||
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||
.descriptor_count(texture_count);
|
||||
let pool_sizes = [pool_size];
|
||||
let frame_pool_size = vk::DescriptorPoolSize::default()
|
||||
.ty(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
|
||||
.descriptor_count(material_count);
|
||||
let material_specular_pool_size = vk::DescriptorPoolSize::default()
|
||||
.ty(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
|
||||
.descriptor_count(material_count);
|
||||
let pool_sizes = [pool_size, frame_pool_size, material_specular_pool_size];
|
||||
let pool_info = vk::DescriptorPoolCreateInfo::default()
|
||||
.pool_sizes(&pool_sizes)
|
||||
.max_sets(texture_count);
|
||||
.max_sets(material_count);
|
||||
let pool =
|
||||
// SAFETY: The pool description is stack-owned and reserves exactly one descriptor.
|
||||
unsafe { device.device().create_descriptor_pool(&pool_info, None) }.map_err(|result| {
|
||||
@@ -487,7 +693,7 @@ fn create_texture_descriptor_bundle(
|
||||
result,
|
||||
}
|
||||
})?;
|
||||
let layouts = vec![layout; textures.len()];
|
||||
let layouts = vec![layout; usize::try_from(material_count).unwrap_or(0)];
|
||||
let allocate_info = vk::DescriptorSetAllocateInfo::default()
|
||||
.descriptor_pool(pool)
|
||||
.set_layouts(&layouts);
|
||||
@@ -511,7 +717,9 @@ fn create_texture_descriptor_bundle(
|
||||
.address_mode_u(vk::SamplerAddressMode::REPEAT)
|
||||
.address_mode_v(vk::SamplerAddressMode::REPEAT)
|
||||
.address_mode_w(vk::SamplerAddressMode::REPEAT)
|
||||
.max_lod(0.0);
|
||||
// The uploaded images carry a complete CPU-generated mip chain; the
|
||||
// sampler clamps to the largest legal level for each image.
|
||||
.max_lod(16.0);
|
||||
let sampler =
|
||||
// SAFETY: The sampler create info is stack-owned and has no unsupported optional features.
|
||||
unsafe { device.device().create_sampler(&sampler_info, None) }.map_err(|result| {
|
||||
@@ -525,27 +733,51 @@ fn create_texture_descriptor_bundle(
|
||||
result,
|
||||
}
|
||||
})?;
|
||||
let image_infos = textures
|
||||
let image_infos = texture_slot_indices
|
||||
.iter()
|
||||
.map(|texture| {
|
||||
.map(|texture_index| {
|
||||
let texture = &textures[*texture_index];
|
||||
vk::DescriptorImageInfo::default()
|
||||
.sampler(sampler)
|
||||
.image_view(texture.view)
|
||||
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let writes = descriptor_sets
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(image_infos.iter())
|
||||
.map(|(descriptor_set, image_info)| {
|
||||
let frame_info = vk::DescriptorBufferInfo::default()
|
||||
.buffer(frame_uniform_buffer.buffer)
|
||||
.offset(0)
|
||||
.range(VULKAN_FRAME_UNIFORM_BYTES as u64);
|
||||
let material_specular_info = vk::DescriptorBufferInfo::default()
|
||||
.buffer(material_specular_buffer.buffer)
|
||||
.offset(0)
|
||||
.range(VULKAN_MATERIAL_SPECULAR_RECORD_BYTES as u64);
|
||||
let mut writes = Vec::with_capacity(descriptor_sets.len() * (TEXTURES_PER_MATERIAL + 2));
|
||||
for (set_index, descriptor_set) in descriptor_sets.iter().copied().enumerate() {
|
||||
for binding in 0..TEXTURES_PER_MATERIAL {
|
||||
let image_info = &image_infos[set_index * TEXTURES_PER_MATERIAL + binding];
|
||||
writes.push(
|
||||
vk::WriteDescriptorSet::default()
|
||||
.dst_set(descriptor_set)
|
||||
.dst_binding(u32::try_from(binding).unwrap_or(u32::MAX))
|
||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||
.image_info(std::slice::from_ref(image_info)),
|
||||
);
|
||||
}
|
||||
writes.push(
|
||||
vk::WriteDescriptorSet::default()
|
||||
.dst_set(descriptor_set)
|
||||
.dst_binding(0)
|
||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||
.image_info(std::slice::from_ref(image_info))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.dst_binding(FRAME_BINDING)
|
||||
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
|
||||
.buffer_info(std::slice::from_ref(&frame_info)),
|
||||
);
|
||||
writes.push(
|
||||
vk::WriteDescriptorSet::default()
|
||||
.dst_set(descriptor_set)
|
||||
.dst_binding(MATERIAL_SPECULAR_BINDING)
|
||||
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
|
||||
.buffer_info(std::slice::from_ref(&material_specular_info)),
|
||||
);
|
||||
}
|
||||
// SAFETY: Descriptor sets, sampler and image views are live; every texture upload completed its shader-read transition.
|
||||
unsafe { device.device().update_descriptor_sets(&writes, &[]) };
|
||||
Ok((layout, pool, descriptor_sets, sampler))
|
||||
@@ -598,15 +830,16 @@ fn create_graphics_pipeline(
|
||||
extent: (u32, u32),
|
||||
state: LegacyPipelineState,
|
||||
) -> Result<vk::Pipeline, VulkanSmokeRendererError> {
|
||||
let vertex_shader = create_shader_module(device, TRIANGLE_VERTEX_SHADER_WORDS)?;
|
||||
let fragment_shader = match create_shader_module(device, TRIANGLE_FRAGMENT_SHADER_WORDS) {
|
||||
Ok(module) => module,
|
||||
Err(error) => {
|
||||
// SAFETY: The shader module was created above on this live logical device and is destroyed on setup failure.
|
||||
unsafe { device.device().destroy_shader_module(vertex_shader, None) };
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let vertex_shader = create_shader_module(device, TRIANGLE_VERTEX_SHADER_WORDS, "vertex")?;
|
||||
let fragment_shader =
|
||||
match create_shader_module(device, TRIANGLE_FRAGMENT_SHADER_WORDS, "fragment") {
|
||||
Ok(module) => module,
|
||||
Err(error) => {
|
||||
// SAFETY: The shader module was created above on this live logical device and is destroyed on setup failure.
|
||||
unsafe { device.device().destroy_shader_module(vertex_shader, None) };
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let entry_point = c"main";
|
||||
let shader_stages = [
|
||||
vk::PipelineShaderStageCreateInfo::default()
|
||||
@@ -620,7 +853,7 @@ fn create_graphics_pipeline(
|
||||
];
|
||||
let vertex_binding = vk::VertexInputBindingDescription::default()
|
||||
.binding(0)
|
||||
.stride(u32::try_from(8 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX))
|
||||
.stride(u32::try_from(14 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX))
|
||||
.input_rate(vk::VertexInputRate::VERTEX);
|
||||
let vertex_attributes = [
|
||||
vk::VertexInputAttributeDescription::default()
|
||||
@@ -636,8 +869,23 @@ fn create_graphics_pipeline(
|
||||
vk::VertexInputAttributeDescription::default()
|
||||
.binding(0)
|
||||
.location(2)
|
||||
.format(vk::Format::R32G32_SFLOAT)
|
||||
.format(vk::Format::R32G32B32_SFLOAT)
|
||||
.offset(u32::try_from(6 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
||||
vk::VertexInputAttributeDescription::default()
|
||||
.binding(0)
|
||||
.location(3)
|
||||
.format(vk::Format::R32G32_SFLOAT)
|
||||
.offset(u32::try_from(9 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
||||
vk::VertexInputAttributeDescription::default()
|
||||
.binding(0)
|
||||
.location(4)
|
||||
.format(vk::Format::R32G32_SFLOAT)
|
||||
.offset(u32::try_from(11 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
||||
vk::VertexInputAttributeDescription::default()
|
||||
.binding(0)
|
||||
.location(5)
|
||||
.format(vk::Format::R32_SFLOAT)
|
||||
.offset(u32::try_from(13 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
||||
];
|
||||
let vertex_bindings = [vertex_binding];
|
||||
let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::default()
|
||||
@@ -685,6 +933,21 @@ fn create_graphics_pipeline(
|
||||
.depth_compare_op(vk::CompareOp::LESS_OR_EQUAL)
|
||||
.depth_bounds_test_enable(false)
|
||||
.stencil_test_enable(false);
|
||||
let (blend_enable, src_color_blend_factor, dst_color_blend_factor) = match state.blend {
|
||||
LegacyBlendMode::Opaque => (false, vk::BlendFactor::ONE, vk::BlendFactor::ZERO),
|
||||
LegacyBlendMode::SourceAlpha => (
|
||||
true,
|
||||
vk::BlendFactor::SRC_ALPHA,
|
||||
vk::BlendFactor::ONE_MINUS_SRC_ALPHA,
|
||||
),
|
||||
LegacyBlendMode::Additive => (true, vk::BlendFactor::SRC_ALPHA, vk::BlendFactor::ONE),
|
||||
LegacyBlendMode::ZeroSourceColor => {
|
||||
(true, vk::BlendFactor::ZERO, vk::BlendFactor::SRC_COLOR)
|
||||
}
|
||||
LegacyBlendMode::DestColorSourceColor => {
|
||||
(true, vk::BlendFactor::DST_COLOR, vk::BlendFactor::SRC_COLOR)
|
||||
}
|
||||
};
|
||||
let color_blend_attachment = vk::PipelineColorBlendAttachmentState::default()
|
||||
.color_write_mask(
|
||||
vk::ColorComponentFlags::R
|
||||
@@ -692,12 +955,12 @@ fn create_graphics_pipeline(
|
||||
| vk::ColorComponentFlags::B
|
||||
| vk::ColorComponentFlags::A,
|
||||
)
|
||||
.blend_enable(state.blend == LegacyBlendMode::SourceAlpha)
|
||||
.src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
|
||||
.dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
|
||||
.blend_enable(blend_enable)
|
||||
.src_color_blend_factor(src_color_blend_factor)
|
||||
.dst_color_blend_factor(dst_color_blend_factor)
|
||||
.color_blend_op(vk::BlendOp::ADD)
|
||||
.src_alpha_blend_factor(vk::BlendFactor::ONE)
|
||||
.dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
|
||||
.src_alpha_blend_factor(src_color_blend_factor)
|
||||
.dst_alpha_blend_factor(dst_color_blend_factor)
|
||||
.alpha_blend_op(vk::BlendOp::ADD);
|
||||
let color_blend_attachments = [color_blend_attachment];
|
||||
let color_blend_state = vk::PipelineColorBlendStateCreateInfo::default()
|
||||
@@ -738,12 +1001,17 @@ fn create_graphics_pipeline(
|
||||
fn create_shader_module(
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
words: &[u32],
|
||||
stage: &'static str,
|
||||
) -> Result<vk::ShaderModule, VulkanSmokeRendererError> {
|
||||
let create_info = vk::ShaderModuleCreateInfo::default().code(words);
|
||||
// SAFETY: The SPIR-V slice points to static checked-in words and lives for the duration of the call.
|
||||
unsafe { device.device().create_shader_module(&create_info, None) }.map_err(|error| {
|
||||
VulkanSmokeRendererError::VulkanOperation {
|
||||
context: "vkCreateShaderModule",
|
||||
context: if stage == "vertex" {
|
||||
"vkCreateShaderModule(vertex)"
|
||||
} else {
|
||||
"vkCreateShaderModule(fragment)"
|
||||
},
|
||||
result: error,
|
||||
}
|
||||
})
|
||||
@@ -821,7 +1089,9 @@ pub(super) fn destroy_swapchain_resources(
|
||||
device
|
||||
.device()
|
||||
.destroy_render_pass(resources.render_pass, None);
|
||||
destroy_depth_attachment(device, &resources.depth_attachment);
|
||||
for depth_attachment in resources.depth_attachments {
|
||||
destroy_depth_attachment(device, &depth_attachment);
|
||||
}
|
||||
for image_view in resources.image_views {
|
||||
device.device().destroy_image_view(image_view, None);
|
||||
}
|
||||
@@ -829,6 +1099,21 @@ pub(super) fn destroy_swapchain_resources(
|
||||
device.device().destroy_buffer(buffer.buffer, None);
|
||||
device.device().free_memory(buffer.memory, None);
|
||||
}
|
||||
for semaphore in resources.render_finished {
|
||||
device.device().destroy_semaphore(semaphore, None);
|
||||
}
|
||||
device
|
||||
.device()
|
||||
.destroy_buffer(resources.frame_uniform_buffer.buffer, None);
|
||||
device
|
||||
.device()
|
||||
.free_memory(resources.frame_uniform_buffer.memory, None);
|
||||
device
|
||||
.device()
|
||||
.destroy_buffer(resources.material_specular_buffer.buffer, None);
|
||||
device
|
||||
.device()
|
||||
.free_memory(resources.material_specular_buffer.memory, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,7 +1156,7 @@ fn destroy_partial_swapchain_resources(
|
||||
if let Some(render_pass) = partial.render_pass {
|
||||
device.device().destroy_render_pass(render_pass, None);
|
||||
}
|
||||
if let Some(depth_attachment) = partial.depth_attachment {
|
||||
for depth_attachment in partial.depth_attachments {
|
||||
destroy_depth_attachment(device, &depth_attachment);
|
||||
}
|
||||
for image_view in partial.image_views {
|
||||
@@ -881,5 +1166,16 @@ fn destroy_partial_swapchain_resources(
|
||||
device.device().destroy_buffer(buffer.buffer, None);
|
||||
device.device().free_memory(buffer.memory, None);
|
||||
}
|
||||
for semaphore in partial.render_finished {
|
||||
device.device().destroy_semaphore(semaphore, None);
|
||||
}
|
||||
if let Some(buffer) = partial.frame_uniform_buffer {
|
||||
device.device().destroy_buffer(buffer.buffer, None);
|
||||
device.device().free_memory(buffer.memory, None);
|
||||
}
|
||||
if let Some(buffer) = partial.material_specular_buffer {
|
||||
device.device().destroy_buffer(buffer.buffer, None);
|
||||
device.device().free_memory(buffer.memory, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,13 +386,13 @@ fn static_surface_extension_name_is_decoded() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swapchain_plan_prefers_srgb_mailbox_and_clamps_extent() {
|
||||
fn swapchain_plan_prefers_unorm_mailbox_and_clamps_extent() {
|
||||
let plan = plan_vulkan_swapchain(&swapchain_request()).expect("swapchain plan");
|
||||
|
||||
assert_eq!(
|
||||
plan.format,
|
||||
VulkanSurfaceFormat {
|
||||
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||
format: vk::Format::R8G8B8A8_UNORM.as_raw(),
|
||||
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||
}
|
||||
);
|
||||
@@ -444,7 +444,7 @@ fn swapchain_plan_accepts_undefined_surface_format_by_picking_stage0_default() {
|
||||
assert_eq!(
|
||||
plan.format,
|
||||
VulkanSurfaceFormat {
|
||||
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||
format: vk::Format::R8G8B8A8_UNORM.as_raw(),
|
||||
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||
}
|
||||
);
|
||||
@@ -479,6 +479,66 @@ fn checked_in_shaders_have_valid_spirv_containers() {
|
||||
assert_eq!(validate_triangle_shaders(), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_shader_dispatches_all_four_material_modes() {
|
||||
let fragment = include_str!("../../shaders/triangle.frag");
|
||||
assert!(fragment.contains("int mode = int(round(material.material_mode));"));
|
||||
assert!(fragment.contains("mode == 0"));
|
||||
assert!(fragment.contains("mode == 1"));
|
||||
assert!(fragment.contains("mode == 2"));
|
||||
assert!(fragment.contains("mode == 3"));
|
||||
assert!(!fragment.contains("material_mode > 1.5"));
|
||||
assert!(!fragment.contains("material_mode > 2.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_specular_reference_uses_squared_power_and_total_overflow_curve() {
|
||||
fn reference_power(cosine: f32, power: u8) -> f32 {
|
||||
if power == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let mut result = cosine;
|
||||
for _ in 0..power.saturating_sub(1) {
|
||||
result *= result;
|
||||
}
|
||||
result
|
||||
}
|
||||
fn reference_curve(value: f32) -> f32 {
|
||||
if value <= 1.0 {
|
||||
0.8 * value
|
||||
} else if value <= 3.0 {
|
||||
0.1 * value + 0.7
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(reference_power(0.5, 0), 0.0);
|
||||
assert!((reference_power(0.5, 1) - 0.5).abs() < 1e-6);
|
||||
assert!((reference_power(0.5, 2) - 0.25).abs() < 1e-6);
|
||||
assert!((reference_power(0.5, 3) - 0.0625).abs() < 1e-6);
|
||||
let compressed_diffuse = 1.5;
|
||||
let material_specular = 0.75;
|
||||
assert!((reference_curve(material_specular + (compressed_diffuse - 1.0)) - 0.825).abs() < 1e-6);
|
||||
|
||||
let vertex = include_str!("../../shaders/triangle.vert");
|
||||
assert!(vertex.contains("result *= result;"));
|
||||
assert!(
|
||||
vertex.contains("vec3 total_specular = specular + max(compressed - vec3(1.0), vec3(0.0));")
|
||||
);
|
||||
assert!(vertex.contains("if (power <= 0.0 || cosine <= 0.0)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shader_computes_radial_fog_per_vertex_and_interpolates_it() {
|
||||
let vertex = include_str!("../../shaders/triangle.vert");
|
||||
let fragment = include_str!("../../shaders/triangle.frag");
|
||||
assert!(vertex.contains("out_fog_factor = native_fog_factor(in_position);"));
|
||||
assert!(fragment.contains("layout(location = 4) in float in_fog_factor;"));
|
||||
assert!(!fragment.contains("distance(in_world_position"));
|
||||
assert!(!fragment.contains("float fog_factor()"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shader_container_rejects_invalid_spirv() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -458,9 +458,24 @@ fn select_surface_format(
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|format| {
|
||||
format.format == vk::Format::B8G8R8A8_SRGB.as_raw()
|
||||
format.format == vk::Format::R8G8B8A8_UNORM.as_raw()
|
||||
&& format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw()
|
||||
})
|
||||
.or_else(|| {
|
||||
formats.iter().copied().find(|format| {
|
||||
format.format == vk::Format::B8G8R8A8_UNORM.as_raw()
|
||||
&& format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw()
|
||||
})
|
||||
})
|
||||
// Keep an sRGB attachment as the final compatibility fallback. The
|
||||
// fragment shader emits legacy byte-equivalent linear values, so an
|
||||
// UNORM attachment is preferred whenever the surface exposes one.
|
||||
.or_else(|| {
|
||||
formats.iter().copied().find(|format| {
|
||||
format.format == vk::Format::B8G8R8A8_SRGB.as_raw()
|
||||
&& format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw()
|
||||
})
|
||||
})
|
||||
.or_else(|| formats.first().copied())
|
||||
.ok_or(VulkanSwapchainError::MissingSurfaceFormat)
|
||||
}
|
||||
@@ -470,7 +485,7 @@ fn undefined_surface_format_override(
|
||||
) -> Option<VulkanSurfaceFormat> {
|
||||
match formats {
|
||||
[format] if format.format == vk::Format::UNDEFINED.as_raw() => Some(VulkanSurfaceFormat {
|
||||
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||
format: vk::Format::R8G8B8A8_UNORM.as_raw(),
|
||||
color_space: format.color_space,
|
||||
}),
|
||||
_ => None,
|
||||
|
||||
Reference in New Issue
Block a user