2026-06-25 06:42:28 +04:00
#![allow(unsafe_code)]
use ash ::vk ;
2026-10-11 21:41:37 +04:00
use fparkan_render ::{ LegacyDepthMode , LegacyPipelineState };
2026-06-25 06:42:28 +04:00
2026-10-18 07:28:16 +04:00
use super ::smoke_types ::{ resolve_draw_texture_indices , VulkanStaticMaterial };
2026-10-11 13:12:50 +04:00
use super ::update_material_texture_descriptor ;
2026-06-25 06:42:28 +04:00
use super ::{
2026-10-11 13:12:50 +04:00
create_command_pool , create_frame_sync , create_index_buffer , create_static_mesh_index_buffer ,
2026-07-18 07:29:31 +04:00
create_static_mesh_vertex_buffer , create_static_texture_image , create_swapchain_resources ,
2026-10-11 13:12:50 +04:00
create_validation_messenger , create_vertex_buffer , create_vulkan_instance_probe ,
2026-07-18 07:29:31 +04:00
create_vulkan_logical_device_probe_for_request , create_vulkan_surface_probe ,
create_vulkan_swapchain_probe_for_extent , destroy_allocated_buffer , destroy_allocated_image ,
2026-10-18 11:23:35 +04:00
destroy_swapchain_resources , ensure_point_light_capacity , plan_vulkan_surface ,
readback_buffer_bytes , update_static_texture_image , write_frame_uniform ,
write_material_specular_buffer , write_point_light_buffer , write_static_mesh_index_buffer ,
write_static_mesh_vertex_buffer , VulkanAllocatedBuffer , VulkanInstanceConfig ,
VulkanInstanceProbe , VulkanLogicalDeviceProbe , VulkanReadbackArtifact , VulkanSmokeFrameOutcome ,
VulkanSmokeRenderer , VulkanSmokeRendererCreateInfo , VulkanSmokeRendererError ,
VulkanSmokeRendererReport , VulkanSmokeShutdownReport , VulkanSurfaceProbe , VulkanSwapchainProbe ,
VulkanSwapchainResources , VulkanValidationMessenger , VulkanValidationReport ,
2026-06-25 06:42:28 +04:00
};
2026-06-25 07:35:48 +04:00
use crate ::policy ::KHR_PORTABILITY_SUBSET_EXTENSION ;
2026-09-06 05:22:12 +04:00
use crate ::shader ::validate_triangle_shaders ;
2026-06-25 06:42:28 +04:00
2026-07-03 17:11:49 +04:00
#[cfg(test)]
2026-06-25 07:18:31 +04:00
fn take_runtime_owners_in_dependency_order < Instance , Validation , Surface , Device , Swapchain > (
instance : & mut Option < Instance > ,
validation : & mut Option < Validation > ,
surface : & mut Option < Surface > ,
device : & mut Option < Device > ,
swapchain : & mut Option < Swapchain > ,
) {
swapchain . take ();
device . take ();
surface . take ();
validation . take ();
instance . take ();
}
2026-07-18 05:28:07 +04:00
fn take_runtime_children_with_validation_snapshot <
Surface ,
Device ,
Swapchain ,
Validation ,
Snapshot ,
Capture ,
> (
2026-06-25 22:35:08 +04:00
surface : & mut Option < Surface > ,
device : & mut Option < Device > ,
swapchain : & mut Option < Swapchain > ,
2026-07-18 05:31:39 +04:00
validation : Option <& Validation > ,
2026-06-25 22:35:08 +04:00
capture : Capture ,
) -> Option < Snapshot >
where
Capture : FnOnce ( & Validation ) -> Snapshot ,
{
2026-07-03 16:55:19 +04:00
swapchain . take ();
device . take ();
surface . take ();
2026-07-18 05:31:39 +04:00
validation . map ( capture )
2026-06-25 22:35:08 +04:00
}
2026-06-25 07:32:26 +04:00
struct RollbackOnDrop < T , F >
where
F : FnOnce ( T ),
{
value : Option < T > ,
rollback : Option < F > ,
}
impl < T , F > RollbackOnDrop < T , F >
where
F : FnOnce ( T ),
{
fn new ( value : T , rollback : F ) -> Self {
Self {
value : Some ( value ),
rollback : Some ( rollback ),
}
}
fn commit ( mut self ) -> T {
self . rollback . take ();
2026-06-25 08:04:47 +04:00
match self . value . take () {
Some ( value ) => value ,
None => unreachable! ( "rollback guard must hold a value until commit" ),
}
2026-06-25 07:32:26 +04:00
}
}
impl < T , F > Drop for RollbackOnDrop < T , F >
where
F : FnOnce ( T ),
{
fn drop ( & mut self ) {
if let ( Some ( value ), Some ( rollback )) = ( self . value . take (), self . rollback . take ()) {
rollback ( value );
}
}
}
2026-06-25 06:42:28 +04:00
impl VulkanSmokeRenderer {
/// Creates a live Vulkan smoke renderer bound to a live native window.
///
/// # Errors
///
/// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation,
/// memory allocation, or synchronization resource creation fails.
2026-09-06 05:22:12 +04:00
2026-06-25 06:42:28 +04:00
pub fn new (
create_info : & VulkanSmokeRendererCreateInfo ,
) -> Result < Self , VulkanSmokeRendererError > {
2026-07-18 07:14:47 +04:00
create_info
. mesh
. validate ()
. map_err ( | context | VulkanSmokeRendererError ::InvalidStaticMesh { context }) ? ;
2026-07-18 14:53:11 +04:00
if ! create_info . camera . is_finite () {
return Err ( VulkanSmokeRendererError ::InvalidStaticCamera {
context : "non-finite clip_from_world matrix" ,
});
}
2026-07-18 08:07:45 +04:00
let draw_texture_indices =
resolve_draw_texture_indices ( & create_info . mesh . draw_ranges , & create_info . materials )
. map_err ( | context | VulkanSmokeRendererError ::InvalidStaticMesh { context }) ? ;
2026-06-25 08:10:31 +04:00
let bootstrap_progress = create_info . bootstrap_progress . as_ref ();
2026-09-06 05:22:12 +04:00
validate_triangle_shaders (). map_err ( VulkanSmokeRendererError ::Shader ) ? ;
2026-06-25 06:42:28 +04:00
let surface_plan = plan_vulkan_surface ( Some ( create_info . native_handles ))
. map_err ( VulkanSmokeRendererError ::Surface ) ? ;
let mut instance_config = VulkanInstanceConfig ::smoke ( & create_info . application_name );
instance_config
. required_extensions
. clone_from ( & surface_plan . required_instance_extensions );
instance_config . enable_validation = create_info . enable_validation ;
let instance = create_vulkan_instance_probe ( & instance_config )
. map_err ( VulkanSmokeRendererError ::Instance ) ? ;
2026-06-25 08:10:31 +04:00
if let Some ( progress ) = bootstrap_progress {
progress . mark_loader_available ();
progress . mark_instance_created ();
}
2026-06-25 06:42:28 +04:00
let validation = if create_info . enable_validation {
Some ( create_validation_messenger ( & instance ) ? )
} else {
None
};
let surface = create_vulkan_surface_probe ( & instance , Some ( create_info . native_handles ))
. map_err ( VulkanSmokeRendererError ::Surface ) ? ;
2026-06-25 08:10:31 +04:00
if let Some ( progress ) = bootstrap_progress {
progress . mark_surface_created ();
}
2026-06-25 08:20:47 +04:00
let device = create_vulkan_logical_device_probe_for_request (
& instance ,
& surface ,
create_info . drawable_extent ,
2026-06-25 08:26:14 +04:00
create_info . render_request ,
2026-06-25 08:20:47 +04:00
)
. map_err ( VulkanSmokeRendererError ::LogicalDevice ) ? ;
2026-06-25 08:10:31 +04:00
if let Some ( progress ) = bootstrap_progress {
progress . mark_logical_device_created ();
}
2026-06-25 06:42:28 +04:00
let swapchain = create_vulkan_swapchain_probe_for_extent (
& instance ,
& surface ,
& device ,
create_info . drawable_extent ,
vk ::SwapchainKHR ::null (),
)
. map_err ( VulkanSmokeRendererError ::Swapchain ) ? ;
2026-06-25 08:10:31 +04:00
if let Some ( progress ) = bootstrap_progress {
progress . mark_swapchain_created ();
}
2026-06-25 06:42:28 +04:00
let command_pool = create_command_pool ( & device ) ? ;
2026-07-18 07:14:47 +04:00
let vertex_buffer =
match create_static_mesh_vertex_buffer ( & instance , & device , & create_info . mesh ) {
Ok ( buffer ) => buffer ,
Err ( error ) => {
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
unsafe { device . device (). destroy_command_pool ( command_pool , None ) };
return Err ( error );
}
};
let index_buffer =
match create_static_mesh_index_buffer ( & instance , & device , & create_info . mesh ) {
Ok ( buffer ) => buffer ,
Err ( error ) => {
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
unsafe { device . device (). destroy_command_pool ( command_pool , None ) };
destroy_allocated_buffer ( & device , & vertex_buffer );
return Err ( error );
}
};
2026-10-11 13:12:50 +04:00
// Keep the compatibility triangle path valid: a white base texel,
// neutral detail texels and a transparent white overlay make the
// additional terrain stages mathematically transparent.
2026-07-18 07:39:38 +04:00
let fallback_texture = super ::VulkanStaticTexture {
width : 1 ,
height : 1 ,
rgba8 : vec ! [ 255 , 255 , 255 , 255 ],
2026-10-11 13:12:50 +04:00
mip_levels : Vec ::new (),
2026-07-18 07:29:31 +04:00
};
2026-10-11 13:12:50 +04:00
let fallback_detail_texture = super ::VulkanStaticTexture {
width : 1 ,
height : 1 ,
rgba8 : vec ! [ 128 , 128 , 128 , 255 ],
mip_levels : Vec ::new (),
};
let fallback_overlay_texture = super ::VulkanStaticTexture {
width : 1 ,
height : 1 ,
rgba8 : vec ! [ 255 , 255 , 255 , 0 ],
mip_levels : Vec ::new (),
};
// Fallback stages are immutable compatibility images. Keep one
// allocation for each fallback kind and map every logical material
// slot to it. Authored images remain one-per-slot because dynamic
// phase uploads must never couple two materials.
2026-07-18 08:07:45 +04:00
let texture_sources = if create_info . materials . is_empty () {
2026-10-11 13:12:50 +04:00
vec! [
( & fallback_texture , Some ( 0_ usize )),
( & fallback_detail_texture , Some ( 1_ usize )),
( & fallback_overlay_texture , Some ( 2_ usize )),
( & fallback_detail_texture , Some ( 1_ usize )),
]
} else {
let mut sources = Vec ::with_capacity ( create_info . materials . len () * 4 );
for material in & create_info . materials {
sources . push (( & material . texture , None ));
sources . push ((
material
. detail_texture
. as_ref ()
. map_or ( & fallback_detail_texture , | texture | texture ),
material . detail_texture . is_none (). then_some ( 1_ usize ),
));
sources . push ((
material
. overlay_texture
. as_ref ()
. map_or ( & fallback_overlay_texture , | texture | texture ),
material . overlay_texture . is_none (). then_some ( 2_ usize ),
));
sources . push ((
material
. overlay_detail_texture
. as_ref ()
. map_or ( & fallback_detail_texture , | texture | texture ),
material . overlay_detail_texture . is_none (). then_some ( 1_ usize ),
));
}
sources
};
let mut textures = Vec ::with_capacity ( texture_sources . len ());
let mut texture_slot_indices = Vec ::with_capacity ( texture_sources . len ());
let mut fallback_image_indices = [ None ; 3 ];
for ( texture_source , fallback_kind ) in texture_sources {
let texture_index = if let Some ( fallback_kind ) = fallback_kind {
if let Some ( texture_index ) = fallback_image_indices [ fallback_kind ] {
texture_index
} else {
let image = match create_static_texture_image (
& instance ,
& device ,
command_pool ,
texture_source ,
) {
Ok ( image ) => image ,
Err ( error ) => {
for texture in & textures {
destroy_allocated_image ( & device , texture );
}
// SAFETY: These resources belong to this live device and are rolled back before it drops.
unsafe { device . device (). destroy_command_pool ( command_pool , None ) };
destroy_allocated_buffer ( & device , & index_buffer );
destroy_allocated_buffer ( & device , & vertex_buffer );
return Err ( error );
}
};
let texture_index = textures . len ();
textures . push ( image );
fallback_image_indices [ fallback_kind ] = Some ( texture_index );
texture_index
}
} else {
let image = match create_static_texture_image (
& instance ,
& device ,
command_pool ,
texture_source ,
) {
Ok ( image ) => image ,
Err ( error ) => {
for texture in & textures {
destroy_allocated_image ( & device , texture );
}
// SAFETY: These resources belong to this live device and are rolled back before it drops.
unsafe { device . device (). destroy_command_pool ( command_pool , None ) };
destroy_allocated_buffer ( & device , & index_buffer );
destroy_allocated_buffer ( & device , & vertex_buffer );
return Err ( error );
}
};
let texture_index = textures . len ();
textures . push ( image );
texture_index
};
texture_slot_indices . push ( texture_index );
}
let material_alphas = if create_info . materials . is_empty () {
vec! [[ 1.0 , 0.0 ]]
2026-07-18 08:07:45 +04:00
} else {
create_info
. materials
. iter ()
2026-10-11 13:12:50 +04:00
. map ( | material | {
[
material . diffuse_alpha ,
material . overlay_diffuse_alpha . unwrap_or ( 0.0 ),
]
})
. collect ()
};
let material_lighting = if create_info . materials . is_empty () {
vec! [[[ 1.0 ; 3 ], [ 0.0 ; 3 ], [ 1.0 ; 3 ], [ 0.0 ; 3 ]]]
} else {
create_info
. materials
. iter ()
. map ( | material | {
[
material . directional_rgb ,
material . additive_rgb ,
material . overlay_directional_rgb . unwrap_or ([ 1.0 ; 3 ]),
material . overlay_additive_rgb . unwrap_or ([ 0.0 ; 3 ]),
]
})
. collect ()
};
let material_uv_transforms = if create_info . materials . is_empty () {
vec! [[[ 0.0 , 0.0 , 1.0 , 1.0 ]; 4 ]]
} else {
create_info
. materials
. iter ()
. map ( | material | {
[
material . uv_transform ,
material . detail_uv_transform ,
material
. overlay_uv_transform
. unwrap_or ([ 0.0 , 0.0 , 1.0 , 1.0 ]),
material
. overlay_detail_uv_transform
. unwrap_or ([ 0.0 , 0.0 , 1.0 , 1.0 ]),
]
})
. collect ()
};
let material_unlit = if create_info . materials . is_empty () {
vec! [ 0.0 ]
} else {
create_info
. materials
. iter ()
2026-10-18 07:28:16 +04:00
. map ( VulkanStaticMaterial ::encoded_material_mode )
2026-10-11 13:12:50 +04:00
. collect ()
};
let material_specular = if create_info . materials . is_empty () {
vec! [[[ 0.0 ; 4 ]; 2 ]]
} else {
create_info
. materials
. iter ()
. map ( | material | {
[
[
material . specular_rgb [ 0 ],
material . specular_rgb [ 1 ],
material . specular_rgb [ 2 ],
f32 ::from ( material . specular_power ),
],
[
material . overlay_specular_rgb . unwrap_or ([ 0.0 ; 3 ])[ 0 ],
material . overlay_specular_rgb . unwrap_or ([ 0.0 ; 3 ])[ 1 ],
material . overlay_specular_rgb . unwrap_or ([ 0.0 ; 3 ])[ 2 ],
f32 ::from ( material . overlay_specular_power . unwrap_or ( 0 )),
],
]
})
. collect ()
};
let material_selectors = if create_info . materials . is_empty () {
vec! [ 0 ]
} else {
create_info
. materials
. iter ()
. map ( | material | material . material_index )
2026-07-18 08:07:45 +04:00
. collect ()
};
2026-06-25 06:42:28 +04:00
let mut renderer = Self {
instance : Some ( instance ),
validation ,
surface : Some ( surface ),
device : Some ( device ),
swapchain : Some ( swapchain ),
command_pool ,
swapchain_resources : None ,
vertex_buffer : Some ( vertex_buffer ),
index_buffer : Some ( index_buffer ),
2026-10-11 13:12:50 +04:00
texture_slot_indices ,
2026-07-18 08:07:45 +04:00
textures ,
2026-10-11 13:12:50 +04:00
vertex_count : create_info . mesh . vertices . len (),
index_capacity : create_info . mesh . indices . len (),
draw_range_capacities : create_info
. mesh
. draw_ranges
. iter ()
. map ( | range | range . index_count )
. collect (),
2026-07-18 07:56:49 +04:00
draw_ranges : create_info . mesh . draw_ranges . clone (),
2026-07-18 08:07:45 +04:00
draw_texture_indices ,
2026-10-11 13:12:50 +04:00
draw_order : ( 0 .. create_info . mesh . draw_ranges . len ()). collect (),
draw_range_sort_keys : vec ! [ None ; create_info . mesh . draw_ranges . len ()],
draw_order_dirty : false ,
draw_range_world_queue : vec ! [ None ; create_info . mesh . draw_ranges . len ()],
2026-10-30 23:16:31 +04:00
draw_range_diffuse_alpha_overrides : vec ! [ None ; create_info . mesh . draw_ranges . len ()],
2026-10-11 21:41:37 +04:00
world_transparent_background_ranges : Vec ::new (),
2026-10-11 13:12:50 +04:00
draw_range_base_pipeline_states : create_info
. mesh
. draw_ranges
. iter ()
. map ( | range | range . pipeline_state )
. collect (),
material_alphas ,
material_lighting ,
material_uv_transforms ,
material_unlit ,
material_specular ,
material_selectors ,
material_specular_dirty : true ,
readback_enabled : false ,
2026-07-18 14:53:11 +04:00
camera : create_info . camera ,
2026-10-11 13:12:50 +04:00
frame_uniforms : super ::VulkanFrameUniforms ::from_camera ( create_info . camera ),
2026-10-18 11:23:35 +04:00
point_lights : Vec ::new (),
point_lights_explicit : false ,
2026-06-25 06:42:28 +04:00
frame_sync : Vec ::new (),
images_in_flight : Vec ::new (),
current_frame : 0 ,
2026-07-18 19:00:43 +04:00
last_readback_image_index : None ,
2026-07-18 08:31:19 +04:00
depth_request : create_info . render_request . depth ,
2026-06-25 06:42:28 +04:00
pending_extent : None ,
swapchain_recreate_count : 0 ,
report : VulkanSmokeRendererReport {
portability_enumeration : instance_config . enable_portability_enumeration ,
2026-06-25 07:35:48 +04:00
portability_subset_enabled : false ,
2026-06-25 06:42:28 +04:00
device_name : String ::new (),
graphics_queue_family : 0 ,
present_queue_family : 0 ,
enabled_extension_count : 0 ,
swapchain_extent : ( 0 , 0 ),
swapchain_image_count : 0 ,
2026-07-18 09:11:43 +04:00
swapchain_image_format : 0 ,
2026-07-18 08:59:28 +04:00
swapchain_image_usage : 0 ,
2026-07-18 09:05:20 +04:00
readback_copy_count : 0 ,
readback_byte_count : 0 ,
readback_fnv1a64 : 0 ,
2026-06-25 06:42:28 +04:00
},
};
renderer . rebuild_swapchain_resources ( false ) ? ;
let device_ref = renderer . device_ref () ? ;
let swapchain_ref = renderer . swapchain_ref () ? ;
renderer . report = VulkanSmokeRendererReport {
portability_enumeration : renderer
. instance
. as_ref ()
. is_some_and ( | instance | instance . report . create_flags != 0 ),
2026-06-25 07:35:48 +04:00
portability_subset_enabled : device_ref
. report
. enabled_extensions
. iter ()
. any ( | extension | extension == KHR_PORTABILITY_SUBSET_EXTENSION ),
2026-06-25 06:42:28 +04:00
device_name : device_ref . report . device_name . clone (),
graphics_queue_family : device_ref . report . graphics_queue_family ,
present_queue_family : device_ref . report . present_queue_family ,
enabled_extension_count : device_ref
. report
. enabled_extensions
. len ()
. try_into ()
. unwrap_or ( u32 ::MAX ),
swapchain_extent : swapchain_ref . report . plan . extent ,
swapchain_image_count : swapchain_ref . report . image_count ,
2026-07-18 09:11:43 +04:00
swapchain_image_format : swapchain_ref . report . plan . format . format ,
2026-07-18 08:59:28 +04:00
swapchain_image_usage : swapchain_ref . report . plan . image_usage ,
2026-07-18 09:05:20 +04:00
readback_copy_count : 0 ,
readback_byte_count : 0 ,
readback_fnv1a64 : 0 ,
2026-06-25 06:42:28 +04:00
};
Ok ( renderer )
}
/// Returns the current bootstrap report.
#[must_use]
pub const fn report ( & self ) -> & VulkanSmokeRendererReport {
& self . report
}
/// Returns measured validation counters and VUIDs.
#[must_use]
pub fn validation_report ( & self ) -> VulkanValidationReport {
self . validation . as_ref (). map_or (
VulkanValidationReport {
warning_count : 0 ,
error_count : 0 ,
vuids : Vec ::new (),
},
VulkanValidationMessenger ::report ,
)
}
/// Returns the measured swapchain recreation count.
#[must_use]
pub const fn swapchain_recreate_count ( & self ) -> u32 {
self . swapchain_recreate_count
}
2026-07-18 14:58:51 +04:00
/// Returns the camera that will be uploaded for the next recorded frame.
#[must_use]
pub const fn camera ( & self ) -> super ::VulkanStaticCamera {
self . camera
}
2026-10-11 13:12:50 +04:00
/// Returns the frame state uploaded before the next recorded frame.
#[must_use]
pub const fn frame_uniforms ( & self ) -> super ::VulkanFrameUniforms {
self . frame_uniforms
}
2026-07-18 14:58:51 +04:00
/// Replaces the camera for subsequent frames without recreating GPU resources.
///
/// # Errors
///
/// Returns [`VulkanSmokeRendererError::InvalidStaticCamera`] when a matrix
/// element is non-finite. The caller must update only between
/// [`Self::draw_frame`] calls on the renderer-owning thread.
pub fn set_camera (
& mut self ,
camera : super ::VulkanStaticCamera ,
) -> Result < (), VulkanSmokeRendererError > {
if ! camera . is_finite () {
return Err ( VulkanSmokeRendererError ::InvalidStaticCamera {
context : "non-finite clip_from_world matrix" ,
});
}
self . camera = camera ;
2026-10-11 13:12:50 +04:00
self . frame_uniforms . clip_from_world = camera . clip_from_world ;
Ok (())
}
/// Replaces lighting, fog, camera-position and matrix state for
/// subsequent frames without recreating GPU resources.
///
/// # Errors
///
/// Returns [`VulkanSmokeRendererError::InvalidStaticCamera`] when any
/// submitted value is non-finite. The caller must update only between
/// [`Self::draw_frame`] calls on the renderer-owning thread.
pub fn set_frame_uniforms (
& mut self ,
frame_uniforms : super ::VulkanFrameUniforms ,
) -> Result < (), VulkanSmokeRendererError > {
if ! frame_uniforms . is_finite () {
return Err ( VulkanSmokeRendererError ::InvalidStaticCamera {
context : "non-finite frame uniform" ,
});
}
self . camera . clip_from_world = frame_uniforms . clip_from_world ;
self . frame_uniforms = frame_uniforms ;
Ok (())
}
2026-10-18 11:23:35 +04:00
/// Replaces the complete dynamic point-light set for subsequent frames.
///
/// The storage buffer grows on demand and rejects counts beyond the
/// device's `maxStorageBufferRange`; it never truncates scene lights.
/// Values are uploaded to the current fence-safe frame slot by
/// [`Self::draw_frame`].
pub fn set_point_lights (
& mut self ,
point_lights : & [ super ::VulkanPointLight ],
) -> Result < (), VulkanSmokeRendererError > {
let max_count = self . resources_ref () ? . max_point_light_count ;
if point_lights . len () > max_count {
return Err ( VulkanSmokeRendererError ::InvalidPointLights {
context : "point-light count exceeds maxStorageBufferRange" ,
});
}
if point_lights . iter (). any ( | light | {
! light
. position
. into_iter ()
. chain ( light . rgb )
. chain ([ light . range ])
. chain ( light . coefficients )
. all ( f32 ::is_finite )
}) {
return Err ( VulkanSmokeRendererError ::InvalidPointLights {
context : "non-finite point-light field" ,
});
}
self . point_lights . clear ();
self . point_lights . extend_from_slice ( point_lights );
self . point_lights_explicit = true ;
Ok (())
}
2026-10-11 13:12:50 +04:00
/// Enables the optional swapchain image copy used by screenshot/readback
/// callers. Runtime frames leave it disabled to avoid a transfer every
/// frame.
pub fn set_readback_enabled ( & mut self , enabled : bool ) {
self . readback_enabled = enabled ;
if ! enabled {
self . last_readback_image_index = None ;
}
}
/// Replaces the shared vertex allocation between frames.
///
/// The initial mesh owns enough vertices for the scene's dynamic slots;
/// callers update those slots in place as camera-relative environment
/// geometry moves. The device is idled before the host-visible write so
/// no submitted command can observe a partial update.
pub fn update_vertices (
& mut self ,
vertices : & [ super ::VulkanStaticVertex ],
) -> Result < (), VulkanSmokeRendererError > {
if vertices . len () != self . vertex_count {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "updated static mesh vertex count differs from allocation" ,
});
}
// SAFETY: The renderer owns the queue and all submitted work; waiting
// for idle makes the shared vertex allocation quiescent.
unsafe { self . device_ref () ? . device (). device_wait_idle () }. map_err ( | result | {
VulkanSmokeRendererError ::VulkanOperation {
context : "vkDeviceWaitIdle(static mesh vertex update)" ,
result ,
}
}) ? ;
write_static_mesh_vertex_buffer ( self . device_ref () ? , self . vertex_buffer_ref () ? , vertices )
}
/// Uploads the current shared mesh while retaining dynamic draw state.
///
/// Environment geometry and projected shadow geometry share the original
/// vertex/index allocations. `geometry_ranges` carries only the ranges
/// whose index segment changed; material selection, blend/depth state,
/// transparency classification, and active counts owned by other setters
/// remain untouched. The allocations grow to the largest submitted frame
/// and never grow for a frame that fits the retained capacity.
///
/// A range with `index_count == 0` is valid and disables that range for the
/// frame. The mesh itself still needs at least one vertex and one index so
/// Vulkan never receives a zero-sized allocation.
pub fn update_mesh_geometry (
& mut self ,
vertices : & [ super ::VulkanStaticVertex ],
indices : & [ u32 ],
geometry_ranges : & [ super ::VulkanDynamicDrawRange ],
) -> Result < (), VulkanSmokeRendererError > {
validate_geometry_upload (
vertices ,
indices ,
& self . draw_ranges ,
geometry_ranges ,
self . vertex_count ,
self . index_capacity ,
) ? ;
let vertex_grew = vertices . len () > self . vertex_count ;
let index_grew = indices . len () > self . index_capacity ;
// SAFETY: The renderer owns the queue and all submitted work. Waiting
// before either writing or replacing a shared allocation keeps old
// command buffers from observing a partial upload.
unsafe { self . device_ref () ? . device (). device_wait_idle () }. map_err ( | result | {
VulkanSmokeRendererError ::VulkanOperation {
context : "vkDeviceWaitIdle(dynamic mesh update)" ,
result ,
}
}) ? ;
let ( new_vertex_buffer , new_index_buffer ) = if vertex_grew || index_grew {
let instance = self . instance_ref () ? ;
let device = self . device_ref () ? ;
let new_vertex_buffer = if vertex_grew {
Some ( create_vertex_buffer ( instance , device , vertices ) ? )
} else {
None
};
let new_index_buffer = if index_grew {
match create_index_buffer ( instance , device , indices ) {
Ok ( buffer ) => Some ( buffer ),
Err ( error ) => {
if let Some ( buffer ) = new_vertex_buffer . as_ref () {
destroy_allocated_buffer ( device , buffer );
}
return Err ( error );
}
}
} else {
None
};
if ! vertex_grew {
if let Err ( error ) =
write_static_mesh_vertex_buffer ( device , self . vertex_buffer_ref () ? , vertices )
{
if let Some ( buffer ) = new_vertex_buffer . as_ref () {
destroy_allocated_buffer ( device , buffer );
}
if let Some ( buffer ) = new_index_buffer . as_ref () {
destroy_allocated_buffer ( device , buffer );
}
return Err ( error );
}
}
if ! index_grew {
if let Err ( error ) =
write_static_mesh_index_buffer ( device , self . index_buffer_ref () ? , indices )
{
if let Some ( buffer ) = new_vertex_buffer . as_ref () {
destroy_allocated_buffer ( device , buffer );
}
if let Some ( buffer ) = new_index_buffer . as_ref () {
destroy_allocated_buffer ( device , buffer );
}
return Err ( error );
}
}
( new_vertex_buffer , new_index_buffer )
} else {
let device = self . device_ref () ? ;
write_static_mesh_vertex_buffer ( device , self . vertex_buffer_ref () ? , vertices ) ? ;
write_static_mesh_index_buffer ( device , self . index_buffer_ref () ? , indices ) ? ;
( None , None )
};
if let Some ( buffer ) = new_vertex_buffer {
let old = self . vertex_buffer . replace ( buffer );
if let Some ( old ) = old {
destroy_allocated_buffer ( self . device_ref () ? , & old );
}
self . vertex_count = vertices . len ();
}
if let Some ( buffer ) = new_index_buffer {
let old = self . index_buffer . replace ( buffer );
if let Some ( old ) = old {
destroy_allocated_buffer ( self . device_ref () ? , & old );
}
self . index_capacity = indices . len ();
}
apply_geometry_range_updates (
& mut self . draw_ranges ,
& mut self . draw_range_capacities ,
2026-10-19 23:05:50 +04:00
& self . draw_range_world_queue ,
2026-10-11 13:12:50 +04:00
geometry_ranges ,
2026-10-19 23:05:50 +04:00
& mut self . draw_order_dirty ,
2026-10-11 13:12:50 +04:00
);
Ok (())
}
/// Replaces one material texture in place while retaining its descriptor
/// set and sampler. This is used by dynamic fixed-size atlases such as
/// the native 256 by 256 projected-shadow texture.
///
/// The updated image must have the same dimensions and generated/authored
/// mip count as the texture allocated during renderer creation. The
/// renderer idles its device before the transfer, so callers may invoke
/// this between frame updates without taking ownership of Vulkan handles.
pub fn update_material_texture (
& mut self ,
material_index : usize ,
texture_slot : usize ,
texture : & super ::VulkanStaticTexture ,
) -> Result < (), VulkanSmokeRendererError > {
if texture_slot >= 4 {
return Err ( VulkanSmokeRendererError ::InvalidStaticTexture {
context : "dynamic material texture slot is out of bounds" ,
});
}
let logical_texture_index = material_index
. checked_mul ( 4 )
. and_then ( | index | index . checked_add ( texture_slot ))
. ok_or ( VulkanSmokeRendererError ::InvalidStaticTexture {
context : "dynamic material texture index overflows address space" ,
}) ? ;
let texture_index = self
. texture_slot_indices
. get ( logical_texture_index )
. copied ()
. ok_or ( VulkanSmokeRendererError ::InvalidStaticTexture {
context : "dynamic material texture index is out of bounds" ,
}) ? ;
if self . textures . get ( texture_index ). is_none () {
return Err ( VulkanSmokeRendererError ::InvalidStaticTexture {
context : "dynamic material texture index is out of bounds" ,
});
}
{
let device = self . device_ref () ? ;
// SAFETY: every submission made by this renderer uses this device; the
// idle wait makes all descriptor image reads quiescent before upload.
unsafe { device . device (). device_wait_idle () }. map_err ( | result | {
VulkanSmokeRendererError ::VulkanOperation {
context : "vkDeviceWaitIdle(dynamic texture update)" ,
result ,
}
}) ? ;
}
let shared_texture = self
. texture_slot_indices
. iter ()
. filter ( | mapped_index | ** mapped_index == texture_index )
. count ()
> 1 ;
if shared_texture {
// A fallback image can serve many logical slots. Detach the
// requested slot before uploading so this public update method
// keeps its one-material/one-slot semantics.
let image = {
let instance = self . instance_ref () ? ;
let device = self . device_ref () ? ;
create_static_texture_image ( instance , device , self . command_pool , texture ) ?
};
let new_texture_index = self . textures . len ();
self . textures . push ( image );
self . texture_slot_indices [ logical_texture_index ] = new_texture_index ;
let device = self . device_ref () ? ;
let resources = self . resources_ref () ? ;
update_material_texture_descriptor (
device ,
resources ,
material_index ,
texture_slot ,
& self . textures [ new_texture_index ],
)
} else {
let instance = self . instance_ref () ? ;
let device = self . device_ref () ? ;
let image = self . textures . get ( texture_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticTexture {
context : "dynamic material texture index is out of bounds" ,
},
) ? ;
update_static_texture_image ( instance , device , self . command_pool , image , texture )
}
}
/// Changes one draw range's active index count while retaining its
/// pipeline and material selector. This lets a fixed-capacity weather
/// allocation draw only the particles emitted in the current frame.
pub fn set_draw_range_index_count (
& mut self ,
range_index : usize ,
index_count : u32 ,
) -> Result < (), VulkanSmokeRendererError > {
if index_count % 3 != 0 {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index count is not a triangle count" ,
});
}
let capacity = self . draw_range_capacities . get ( range_index ). copied (). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
},
) ? ;
if index_count > capacity {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range exceeds its index allocation" ,
});
}
let range = self . draw_ranges . get_mut ( range_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
},
) ? ;
2026-10-19 23:05:50 +04:00
let world_queue = self
. draw_range_world_queue
. get ( range_index )
. copied ()
. ok_or ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
}) ? ;
mark_draw_order_dirty_on_activity_change (
world_queue ,
range . index_count ,
index_count ,
& mut self . draw_order_dirty ,
);
2026-10-11 13:12:50 +04:00
range . index_count = index_count ;
Ok (())
}
2026-10-30 23:16:31 +04:00
/// Overrides one draw's diffuse alpha without changing its shared material.
pub fn set_draw_range_diffuse_alpha_override (
& mut self ,
range_index : usize ,
diffuse_alpha : Option < f32 > ,
) -> Result < (), VulkanSmokeRendererError > {
if diffuse_alpha . is_some_and ( | alpha | ! alpha . is_finite () || ! ( 0. 0 ..= 1.0 ). contains ( & alpha )) {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw-range diffuse alpha must be finite and 0..1" ,
});
}
let alpha_override = self
. draw_range_diffuse_alpha_overrides
. get_mut ( range_index )
. ok_or ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw-range alpha index is out of bounds" ,
}) ? ;
* alpha_override = diffuse_alpha ;
Ok (())
}
2026-10-11 13:12:50 +04:00
/// Sets the native per-range alpha reference in legacy 0..=255 units.
///
2026-10-18 00:25:11 +04:00
/// Zero remains an explicit value; use the native-default constant when
/// matching the renderer's initial state. The update is dynamic and does
/// not recreate a graphics pipeline.
2026-10-11 13:12:50 +04:00
pub fn set_draw_range_alpha_test_reference (
& mut self ,
range_index : usize ,
alpha_test_reference : u8 ,
) -> Result < (), VulkanSmokeRendererError > {
let range = self . draw_ranges . get_mut ( range_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
},
) ? ;
range . alpha_test_reference = alpha_test_reference ;
Ok (())
}
2026-10-11 21:41:37 +04:00
/// Applies the active MAT0 phase's blend and alpha-test category to one
/// range while retaining its authored depth and cull state.
pub fn set_draw_range_material_category (
& mut self ,
range_index : usize ,
category : u8 ,
) -> Result < (), VulkanSmokeRendererError > {
let base_state = * self
. draw_range_base_pipeline_states
. get ( range_index )
. ok_or ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
}) ? ;
let world_queue = self
. draw_range_world_queue
. get ( range_index )
. copied ()
. ok_or ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
}) ? ;
// Fixed sky, sprite, weather, and shadow ranges own explicit states.
// MAT0 categories apply only after a range has entered the world queue.
let ( next_base , next_state ) =
world_material_pipeline_states ( base_state , category , world_queue ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "MAT0 pipeline category is outside the native 0..=4 table" ,
},
) ? ;
let range = self . draw_ranges . get_mut ( range_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
},
) ? ;
self . draw_range_base_pipeline_states [ range_index ] = next_base ;
let pipeline_changed = range . pipeline_state != next_state ;
range . pipeline_state = next_state ;
if pipeline_changed {
self . rebuild_pipeline_resources () ? ;
}
Ok (())
}
/// Places nebula, dome, and cloud ranges between opaque and transparent
/// world draws. The list is explicit so unrelated fixed passes keep their
/// established order.
pub fn set_world_transparent_background_ranges (
& mut self ,
range_indices : & [ usize ],
) -> Result < (), VulkanSmokeRendererError > {
let mut seen = vec! [ false ; self . draw_ranges . len ()];
for & range_index in range_indices {
let Some ( slot ) = seen . get_mut ( range_index ) else {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "world background range index is out of bounds" ,
});
};
if * slot || self . draw_range_world_queue [ range_index ]. is_some () {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "world background range is duplicated or already queued" ,
});
}
* slot = true ;
}
self . world_transparent_background_ranges = range_indices . to_vec ();
self . draw_order_dirty = true ;
Ok (())
}
2026-10-11 13:12:50 +04:00
/// Marks a draw range as belonging to the native transparent queue.
///
/// The flag is deliberately independent of the blend category: native
/// materials can blend while retaining depth writes, and some alpha
/// materials are depth-tested opaque. When enabled, a range that uses a
/// depth attachment switches to depth-test/read-only. The original depth
/// state is retained so disabling the flag restores the caller's state.
///
2026-10-11 21:41:37 +04:00
/// The transparent flag only participates in the world draw order. Without
/// explicit backgrounds, opaque and transparent ranges replace the
/// existing world slots. With explicit backgrounds, opaque ranges compact
/// into those slots and the sorted transparent ranges are inserted after
/// the last background; unrelated fixed ranges keep relative order but
/// may shift slots.
2026-10-11 13:12:50 +04:00
pub fn set_draw_range_transparency (
& mut self ,
range_index : usize ,
transparent : bool ,
) -> Result < (), VulkanSmokeRendererError > {
2026-10-11 21:41:37 +04:00
if self
. world_transparent_background_ranges
. contains ( & range_index )
{
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "world background range cannot join the world queue" ,
});
}
2026-10-11 13:12:50 +04:00
let base_state = * self
. draw_range_base_pipeline_states
. get ( range_index )
. ok_or ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
}) ? ;
let range = self . draw_ranges . get_mut ( range_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
},
) ? ;
2026-10-11 21:41:37 +04:00
let next_state = apply_world_transparency ( base_state , transparent );
2026-10-11 13:12:50 +04:00
let pipeline_changed = range . pipeline_state != next_state ;
range . pipeline_state = next_state ;
// Calling this setter opts the range into the world queue. Fixed
// screen/sky/weather ranges remain `None` because their pass order is
// intentionally owned by the caller.
2026-10-19 23:05:50 +04:00
set_and_mark_dirty_if_changed (
& mut self . draw_range_world_queue [ range_index ],
Some ( transparent ),
& mut self . draw_order_dirty ,
);
2026-10-11 13:12:50 +04:00
if pipeline_changed {
self . rebuild_pipeline_resources () ? ;
}
Ok (())
}
/// Supplies the native camera-sort key returned by the draw-item camera
/// callback. The native callback computes Euclidean distance from the
/// camera to the first world vertex of the whole indexed batch.
///
/// The recovered comparator inserts in descending floating-point camera
/// key order. Keeping the key separate from the range handle mirrors the
/// native layer's callback result and its `[item pointer, key]` entries.
pub fn set_draw_range_sort_key (
& mut self ,
range_index : usize ,
sort_key : f32 ,
) -> Result < (), VulkanSmokeRendererError > {
if ! sort_key . is_finite () {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "native draw sort key must be finite" ,
});
}
let key = self . draw_range_sort_keys . get_mut ( range_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
},
) ? ;
2026-10-19 23:05:50 +04:00
set_and_mark_dirty_if_changed ( key , Some ( sort_key ), & mut self . draw_order_dirty );
2026-10-11 13:12:50 +04:00
Ok (())
}
/// Returns the current stable range-handle order used by command
/// recording. Handles are indices into the original `draw_ranges` vector.
#[must_use]
pub fn draw_order ( & self ) -> & [ usize ] {
& self . draw_order
}
/// Changes the diffuse and overlay alpha values carried by one material's
/// push constants. The descriptor images remain unchanged.
pub fn set_material_alphas (
& mut self ,
material_index : usize ,
diffuse_alpha : f32 ,
overlay_alpha : f32 ,
) -> Result < (), VulkanSmokeRendererError > {
if ! diffuse_alpha . is_finite ()
|| ! overlay_alpha . is_finite ()
|| ! ( 0. 0 ..= 1.0 ). contains ( & diffuse_alpha )
|| ! ( 0. 0 ..= 1.0 ). contains ( & overlay_alpha )
{
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic material alpha must be finite and 0..1" ,
});
}
let alphas = self . material_alphas . get_mut ( material_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic material index is out of bounds" ,
},
) ? ;
* alphas = [ diffuse_alpha , overlay_alpha ];
Ok (())
}
/// Changes the base directional and additive lighting coefficients of one
/// material while retaining its descriptor images and pipeline state.
pub fn set_material_lighting (
& mut self ,
material_index : usize ,
directional_rgb : [ f32 ; 3 ],
additive_rgb : [ f32 ; 3 ],
) -> Result < (), VulkanSmokeRendererError > {
self . set_material_lighting_with_overlay (
material_index ,
directional_rgb ,
additive_rgb ,
None ,
None ,
)
}
/// Changes base and optional overlay directional/additive coefficients
/// while retaining descriptor images and pipeline state.
pub fn set_material_lighting_with_overlay (
& mut self ,
material_index : usize ,
directional_rgb : [ f32 ; 3 ],
additive_rgb : [ f32 ; 3 ],
overlay_directional_rgb : Option < [ f32 ; 3 ] > ,
overlay_additive_rgb : Option < [ f32 ; 3 ] > ,
) -> Result < (), VulkanSmokeRendererError > {
if directional_rgb
. into_iter ()
. chain ( additive_rgb )
. chain ( overlay_directional_rgb . into_iter (). flatten ())
. chain ( overlay_additive_rgb . into_iter (). flatten ())
2026-10-18 00:33:21 +04:00
. any ( | value | ! value . is_finite ())
2026-10-11 13:12:50 +04:00
{
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
2026-10-18 00:33:21 +04:00
context : "dynamic material lighting must be finite" ,
2026-10-11 13:12:50 +04:00
});
}
let lighting = self . material_lighting . get_mut ( material_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic material index is out of bounds" ,
},
) ? ;
lighting [ 0 ] = directional_rgb ;
lighting [ 1 ] = additive_rgb ;
lighting [ 2 ] = overlay_directional_rgb . unwrap_or ([ 1.0 ; 3 ]);
lighting [ 3 ] = overlay_additive_rgb . unwrap_or ([ 0.0 ; 3 ]);
Ok (())
}
/// Replaces the four Page transforms carried by one material's push
/// constants. The transform order is base, detail, overlay, and overlay
/// detail, matching the material shader's texture bindings.
///
/// The update is kept CPU-side and is consumed by the next recorded draw;
/// no descriptor or image resource is recreated. This is intentionally a
/// complete replacement so an animated MAT0 phase cannot leave one stale
/// Page rectangle behind when its texture set is unchanged.
pub fn set_material_uv_transforms (
& mut self ,
material_index : usize ,
uv_transforms : [[ f32 ; 4 ]; 4 ],
) -> Result < (), VulkanSmokeRendererError > {
if uv_transforms
. into_iter ()
. flatten ()
. any ( | value | ! value . is_finite ())
{
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic material UV transform must be finite" ,
});
}
let transforms = self . material_uv_transforms . get_mut ( material_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic material index is out of bounds" ,
},
) ? ;
* transforms = uv_transforms ;
Ok (())
}
/// Replaces the base and overlay specular records used by the vertex
/// Gouraud lighting path. RGB values remain normalized; powers retain
/// the native MAT0 byte and are encoded as the fourth float in each GPU
/// record.
pub fn set_material_specular (
& mut self ,
material_index : usize ,
specular_rgb : [ f32 ; 3 ],
power : u8 ,
overlay_specular_rgb : Option < [ f32 ; 3 ] > ,
overlay_power : Option < u8 > ,
) -> Result < (), VulkanSmokeRendererError > {
if specular_rgb
. into_iter ()
. chain ( overlay_specular_rgb . into_iter (). flatten ())
2026-10-18 00:33:21 +04:00
. any ( | value | ! value . is_finite ())
2026-10-11 13:12:50 +04:00
{
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
2026-10-18 00:33:21 +04:00
context : "dynamic material specular RGB must be finite" ,
2026-10-11 13:12:50 +04:00
});
}
let specular = self . material_specular . get_mut ( material_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic material index is out of bounds" ,
},
) ? ;
let overlay_rgb = overlay_specular_rgb . unwrap_or ([ 0.0 ; 3 ]);
let next = [
[
specular_rgb [ 0 ],
specular_rgb [ 1 ],
specular_rgb [ 2 ],
f32 ::from ( power ),
],
[
overlay_rgb [ 0 ],
overlay_rgb [ 1 ],
overlay_rgb [ 2 ],
f32 ::from ( overlay_power . unwrap_or ( 0 )),
],
];
if * specular == next {
return Ok (());
}
* specular = next ;
self . material_specular_dirty = true ;
Ok (())
}
/// Rebinds one draw range to a positional material slot without
/// reallocating any vertex or descriptor resources.
pub fn set_draw_range_material (
& mut self ,
range_index : usize ,
material_index : usize ,
) -> Result < (), VulkanSmokeRendererError > {
let selector = * self . material_selectors . get ( material_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic material index is out of bounds" ,
},
) ? ;
let range = self . draw_ranges . get_mut ( range_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
},
) ? ;
range . material_index = selector ;
let texture_index = self . draw_texture_indices . get_mut ( range_index ). ok_or (
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic draw range index is out of bounds" ,
},
) ? ;
* texture_index = material_index ;
2026-07-18 14:58:51 +04:00
Ok (())
}
2026-06-25 22:35:08 +04:00
/// Explicitly idles and tears down the renderer while the native window is still alive.
///
/// # Errors
///
/// Returns [`VulkanSmokeRendererError`] when the renderer cannot reach a
/// stable idle point before teardown begins.
pub fn shutdown ( mut self ) -> Result < VulkanSmokeShutdownReport , VulkanSmokeRendererError > {
self . shutdown_inner ()
}
2026-10-18 12:04:09 +04:00
/// Returns the latest submitted frame using the existing optional readback copy.
///
/// This synchronizes the device before mapping the host-coherent buffer, so
/// callers should use it only for bounded diagnostic captures.
pub fn readback_last_submitted_frame (
& self ,
) -> Result < Option < VulkanReadbackArtifact > , VulkanSmokeRendererError > {
let device = self . device_ref () ? ;
// SAFETY: The renderer owns this device and all submitted work; idling
// prevents the last readback transfer from racing the host buffer map.
unsafe { device . device (). device_wait_idle () }. map_err ( | error | {
VulkanSmokeRendererError ::VulkanOperation {
context : "vkDeviceWaitIdle(readback frame)" ,
result : error ,
}
}) ? ;
let Some ( bytes ) = self . completed_readback ( device ) ? else {
return Ok ( None );
};
let format = self . swapchain_ref () ? . report . plan . format . format ;
Ok ( Some ( VulkanReadbackArtifact {
format ,
width : self . report . swapchain_extent . 0 ,
height : self . report . swapchain_extent . 1 ,
bytes ,
}))
}
2026-06-25 06:42:28 +04:00
/// Requests swapchain recreation for a new drawable extent.
pub fn request_resize ( & mut self , extent : ( u32 , u32 )) {
self . pending_extent = Some ( extent );
}
fn device_ref ( & self ) -> Result <& VulkanLogicalDeviceProbe , VulkanSmokeRendererError > {
self . device
. as_ref ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "logical device" ,
})
}
fn swapchain_ref ( & self ) -> Result <& VulkanSwapchainProbe , VulkanSmokeRendererError > {
self . swapchain
. as_ref ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "swapchain" ,
})
}
fn instance_ref ( & self ) -> Result <& VulkanInstanceProbe , VulkanSmokeRendererError > {
self . instance
. as_ref ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "instance" ,
})
}
2026-07-18 08:07:45 +04:00
fn textures_ref ( & self ) -> Result <& [ super ::VulkanAllocatedImage ], VulkanSmokeRendererError > {
if self . textures . is_empty () {
Err ( VulkanSmokeRendererError ::InvariantViolation {
context : "static material textures" ,
2026-07-18 07:39:38 +04:00
})
2026-07-18 08:07:45 +04:00
} else {
Ok ( & self . textures )
}
2026-07-18 07:39:38 +04:00
}
2026-06-25 06:42:28 +04:00
fn surface_ref ( & self ) -> Result <& VulkanSurfaceProbe , VulkanSmokeRendererError > {
self . surface
. as_ref ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation { context : "surface" })
}
fn resources_ref ( & self ) -> Result <& VulkanSwapchainResources , VulkanSmokeRendererError > {
self . swapchain_resources
. as_ref ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "swapchain resources" ,
})
}
fn vertex_buffer_ref ( & self ) -> Result <& VulkanAllocatedBuffer , VulkanSmokeRendererError > {
self . vertex_buffer
. as_ref ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "vertex buffer" ,
})
}
fn index_buffer_ref ( & self ) -> Result <& VulkanAllocatedBuffer , VulkanSmokeRendererError > {
self . index_buffer
. as_ref ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "index buffer" ,
})
}
/// Draws and presents one indexed-triangle frame.
///
/// # Errors
///
/// Returns [`VulkanSmokeRendererError`] when synchronization, command recording,
/// submission, or presentation fails.
2026-09-06 05:22:12 +04:00
2026-06-25 06:42:28 +04:00
pub fn draw_frame ( & mut self ) -> Result < VulkanSmokeFrameOutcome , VulkanSmokeRendererError > {
if let Some ( extent ) = self . pending_extent . take () {
if extent . 0 == 0 || extent . 1 == 0 {
self . pending_extent = Some ( extent );
return Ok ( VulkanSmokeFrameOutcome ::ZeroExtent );
}
self . recreate_swapchain ( extent ) ? ;
return Ok ( VulkanSmokeFrameOutcome ::Recreated );
}
2026-10-11 13:12:50 +04:00
let sync = self . frame_sync . get ( self . current_frame ). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "frame synchronization state" ,
},
) ? ;
2026-06-25 06:42:28 +04:00
let image_available = sync . image_available ;
let in_flight_fence = sync . fence ;
// SAFETY: The fence belongs to this live logical device and is waited from one thread.
unsafe {
self . device_ref () ?
. device ()
. wait_for_fences ( & [ in_flight_fence ], true , 1_000_000_000 )
}
. map_err ( | error | VulkanSmokeRendererError ::VulkanOperation {
context : "vkWaitForFences" ,
result : error ,
}) ? ;
2026-10-18 11:23:35 +04:00
let legacy_point_light = self . frame_uniforms . point_light ;
let point_light_count = if self . point_lights_explicit {
self . point_lights . len ()
} else {
usize ::from ( legacy_point_light . active )
};
let needs_point_light_growth =
point_light_count > self . resources_ref () ? . point_light_capacity ;
if needs_point_light_growth {
// Updating every material's binding 6 and replacing the shared
// SSBO requires all prior command buffers to finish.
// SAFETY: The renderer owns the logical device and no submission
// can observe the replacement after this idle point.
unsafe { self . device_ref () ? . device (). device_wait_idle () }. map_err ( | result | {
VulkanSmokeRendererError ::VulkanOperation {
context : "vkDeviceWaitIdle(point-light buffer growth)" ,
result ,
}
}) ? ;
let instance =
self . instance
. as_ref ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "Vulkan instance during point-light growth" ,
}) ? ;
let device =
self . device
. as_ref ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "logical device during point-light growth" ,
}) ? ;
let resources = self . swapchain_resources . as_mut (). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "swapchain resources during point-light growth" ,
},
) ? ;
ensure_point_light_capacity ( instance , device , resources , point_light_count ) ? ;
}
2026-10-11 13:12:50 +04:00
if self . material_specular_dirty {
// The storage buffer is shared by all in-flight frames. Wait for
// every prior use before replacing the records so a setter cannot
// race a vertex shader from the previous frame.
// SAFETY: The renderer owns the logical device and no submission
// can observe the host write after this idle point.
unsafe { self . device_ref () ? . device (). device_wait_idle () }. map_err ( | result | {
VulkanSmokeRendererError ::VulkanOperation {
context : "vkDeviceWaitIdle(material specular update)" ,
result ,
}
}) ? ;
let resources = self . resources_ref () ? ;
write_material_specular_buffer (
self . device_ref () ? ,
& resources . material_specular_buffer ,
resources . material_specular_stride ,
& self . material_specular ,
) ? ;
self . material_specular_dirty = false ;
}
2026-06-25 06:42:28 +04:00
// SAFETY: The swapchain, semaphore and fence inputs are live for the duration of the acquire call.
let acquire = unsafe {
self . swapchain_ref () ? . loader (). acquire_next_image (
self . swapchain_ref () ? . swapchain (),
1_000_000_000 ,
image_available ,
vk ::Fence ::null (),
)
};
let ( image_index , acquire_suboptimal ) = match acquire {
Ok ( result ) => result ,
Err ( vk ::Result ::ERROR_OUT_OF_DATE_KHR ) => {
self . recreate_swapchain ( self . report . swapchain_extent ) ? ;
return Ok ( VulkanSmokeFrameOutcome ::Recreated );
}
Err ( error ) => {
return Err ( VulkanSmokeRendererError ::VulkanOperation {
context : "vkAcquireNextImageKHR" ,
result : error ,
});
}
};
let image_index_usize = usize ::try_from ( image_index ). unwrap_or ( 0 );
let image_fence = self . images_in_flight [ image_index_usize ];
if image_fence != vk ::Fence ::null () {
// SAFETY: The fence belongs to this renderer and can be waited independently.
unsafe {
self . device_ref () ?
. device ()
. wait_for_fences ( & [ image_fence ], true , 1_000_000_000 )
}
. map_err ( | error | VulkanSmokeRendererError ::VulkanOperation {
context : "vkWaitForFences(image)" ,
result : error ,
}) ? ;
}
self . images_in_flight [ image_index_usize ] = in_flight_fence ;
2026-10-11 13:12:50 +04:00
let frame_bytes = self . frame_uniforms . to_ne_bytes ();
2026-10-18 11:23:35 +04:00
{
let explicit_point_lights = self . point_lights_explicit ;
let legacy_point_light = self . frame_uniforms . point_light ;
let point_lights = if explicit_point_lights {
self . point_lights . as_slice ()
} else if legacy_point_light . active {
std ::slice ::from_ref ( & legacy_point_light )
} else {
& []
};
let device =
self . device
. as_ref ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "logical device during frame upload" ,
}) ? ;
let resources = self . swapchain_resources . as_ref (). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "swapchain resources during frame upload" ,
},
) ? ;
write_frame_uniform (
device ,
& resources . frame_uniform_buffer ,
resources . frame_uniform_stride ,
self . current_frame ,
& frame_bytes ,
) ? ;
write_point_light_buffer (
device ,
& resources . point_light_buffer ,
resources . point_light_stride ,
self . current_frame ,
point_lights ,
) ? ;
}
2026-10-11 13:12:50 +04:00
let render_finished = self
. resources_ref () ?
. render_finished
. get ( image_index_usize )
. copied ()
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "render-finished semaphore for swapchain image" ,
}) ? ;
self . record_command_buffer ( image_index_usize ) ? ;
// Reset only after recording succeeds. A recording error leaves the
// signaled fence available for the next recovery attempt.
2026-06-25 06:42:28 +04:00
// SAFETY: The fence belongs to this frame context and is not in use after the wait above.
unsafe { self . device_ref () ? . device (). reset_fences ( & [ in_flight_fence ]) }. map_err (
| error | VulkanSmokeRendererError ::VulkanOperation {
context : "vkResetFences" ,
result : error ,
},
) ? ;
let wait_semaphores = [ image_available ];
let wait_stages = [ vk ::PipelineStageFlags ::COLOR_ATTACHMENT_OUTPUT ];
let command_buffers = [ self . resources_ref () ? . command_buffers [ image_index_usize ]];
let signal_semaphores = [ render_finished ];
let submit_info = [ vk ::SubmitInfo ::default ()
. wait_semaphores ( & wait_semaphores )
. wait_dst_stage_mask ( & wait_stages )
. command_buffers ( & command_buffers )
. signal_semaphores ( & signal_semaphores )];
// SAFETY: Submission references live queue, sync objects and recorded command buffer.
unsafe {
self . device_ref () ? . device (). queue_submit (
self . device_ref () ? . graphics_queue (),
& submit_info ,
in_flight_fence ,
)
}
. map_err ( | error | VulkanSmokeRendererError ::VulkanOperation {
context : "vkQueueSubmit" ,
result : error ,
}) ? ;
2026-10-11 13:12:50 +04:00
if self . readback_enabled && ! self . resources_ref () ? . readback_buffers . is_empty () {
2026-07-18 09:05:20 +04:00
self . report . readback_copy_count = self . report . readback_copy_count . saturating_add ( 1 );
2026-07-18 19:00:43 +04:00
self . last_readback_image_index = Some ( image_index_usize );
2026-07-18 09:05:20 +04:00
}
2026-06-25 06:42:28 +04:00
let present_wait = [ render_finished ];
let swapchains = [ self . swapchain_ref () ? . swapchain ()];
let image_indices = [ image_index ];
let present_info = vk ::PresentInfoKHR ::default ()
. wait_semaphores ( & present_wait )
. swapchains ( & swapchains )
. image_indices ( & image_indices );
// SAFETY: Presentation uses the rendered image index and a semaphore signaled by queue submission.
let present_suboptimal = match unsafe {
self . swapchain_ref () ?
. loader ()
. queue_present ( self . device_ref () ? . present_queue (), & present_info )
} {
Ok ( suboptimal ) => suboptimal ,
Err ( vk ::Result ::ERROR_OUT_OF_DATE_KHR ) => {
self . recreate_swapchain ( self . report . swapchain_extent ) ? ;
return Ok ( VulkanSmokeFrameOutcome ::Recreated );
}
Err ( error ) => {
return Err ( VulkanSmokeRendererError ::VulkanOperation {
context : "vkQueuePresentKHR" ,
result : error ,
});
}
};
self . current_frame = ( self . current_frame + 1 ) % self . frame_sync . len (). max ( 1 );
if acquire_suboptimal || present_suboptimal {
self . recreate_swapchain ( self . report . swapchain_extent ) ? ;
Ok ( VulkanSmokeFrameOutcome ::Recreated )
} else {
Ok ( VulkanSmokeFrameOutcome ::Presented )
}
}
fn recreate_swapchain ( & mut self , extent : ( u32 , u32 )) -> Result < (), VulkanSmokeRendererError > {
let device = self . device_ref () ? ;
// SAFETY: The logical device remains live and idling at swapchain recreation boundaries.
unsafe { device . device (). device_wait_idle () }. map_err ( | error | {
VulkanSmokeRendererError ::VulkanOperation {
context : "vkDeviceWaitIdle" ,
result : error ,
}
}) ? ;
self . pending_extent = None ;
self . rebuild_swapchain ( extent ) ? ;
self . swapchain_recreate_count = self . swapchain_recreate_count . saturating_add ( 1 );
Ok (())
}
fn rebuild_swapchain ( & mut self , extent : ( u32 , u32 )) -> Result < (), VulkanSmokeRendererError > {
self . destroy_swapchain_resources ();
let instance = self . instance_ref () ? ;
let surface = self . surface_ref () ? ;
let device = self . device_ref () ? ;
let old_swapchain = self
. swapchain
. as_ref ()
. map_or ( vk ::SwapchainKHR ::null (), VulkanSwapchainProbe ::swapchain );
let new_swapchain = create_vulkan_swapchain_probe_for_extent (
instance ,
surface ,
device ,
extent ,
old_swapchain ,
)
. map_err ( VulkanSmokeRendererError ::Swapchain ) ? ;
self . swapchain = Some ( new_swapchain );
self . rebuild_swapchain_resources ( true ) ? ;
Ok (())
}
fn rebuild_swapchain_resources (
& mut self ,
reuse_command_pool : bool ,
) -> Result < (), VulkanSmokeRendererError > {
2026-06-25 07:32:26 +04:00
let device = self . device_ref () ? ;
let swapchain = self . swapchain_ref () ? ;
let resources = RollbackOnDrop ::new (
2026-06-25 06:42:28 +04:00
create_swapchain_resources (
2026-07-18 08:31:19 +04:00
self . instance_ref () ? ,
2026-06-25 06:42:28 +04:00
device ,
swapchain ,
self . command_pool ,
self . vertex_buffer_ref () ? ,
self . index_buffer_ref () ? ,
2026-07-18 08:07:45 +04:00
self . textures_ref () ? ,
2026-10-11 13:12:50 +04:00
& self . texture_slot_indices ,
2026-07-18 08:22:29 +04:00
& self . draw_ranges ,
2026-07-18 08:31:19 +04:00
self . depth_request ,
2026-06-25 06:42:28 +04:00
reuse_command_pool ,
2026-06-25 07:32:26 +04:00
) ? ,
| resources | destroy_swapchain_resources ( device , self . command_pool , resources ),
);
let frame_sync = create_frame_sync ( device ) ? ;
2026-06-25 06:42:28 +04:00
let swapchain_extent = self . swapchain_ref () ? . report . plan . extent ;
let swapchain_image_count = self . swapchain_ref () ? . report . image_count ;
2026-07-18 08:59:28 +04:00
let swapchain_image_usage = self . swapchain_ref () ? . report . plan . image_usage ;
2026-06-25 07:32:26 +04:00
let resources = resources . commit ();
2026-06-25 06:42:28 +04:00
self . images_in_flight = vec! [ vk ::Fence ::null (); resources . image_views . len ()];
self . frame_sync = frame_sync ;
2026-07-18 19:00:43 +04:00
self . last_readback_image_index = None ;
2026-10-11 13:12:50 +04:00
self . material_specular_dirty = true ;
2026-06-25 06:42:28 +04:00
self . report . swapchain_extent = swapchain_extent ;
self . report . swapchain_image_count = swapchain_image_count ;
2026-07-18 09:11:43 +04:00
self . report . swapchain_image_format = self . swapchain_ref () ? . report . plan . format . format ;
2026-07-18 08:59:28 +04:00
self . report . swapchain_image_usage = swapchain_image_usage ;
2026-06-25 06:42:28 +04:00
self . swapchain_resources = Some ( resources );
Ok (())
}
2026-10-11 13:12:50 +04:00
/// Rebuilds only the swapchain-owned pipeline bundle after a structural
/// range-state change. Draw-range setters can be called while a renderer
/// is live, so all prior submissions are completed before handles are
/// destroyed and recreated.
fn rebuild_pipeline_resources ( & mut self ) -> Result < (), VulkanSmokeRendererError > {
if self . swapchain_resources . is_none () {
return Ok (());
}
let device = self . device_ref () ? ;
// SAFETY: The renderer owns every submission on this device. Waiting
// here makes the old pipelines, framebuffers, and command buffers
// quiescent before their destruction.
unsafe { device . device (). device_wait_idle () }. map_err ( | result | {
VulkanSmokeRendererError ::VulkanOperation {
context : "vkDeviceWaitIdle(draw range pipeline update)" ,
result ,
}
}) ? ;
self . destroy_swapchain_resources ();
self . rebuild_swapchain_resources ( true )
}
2026-10-11 21:41:37 +04:00
/// Recomputes the stable command-recording order. Without explicit
/// backgrounds, opaque world ranges followed by the sorted transparent
/// world ranges replace the existing world slots. With explicit
/// backgrounds, opaque ranges compact into the world slots and sorted
/// transparent ranges are inserted after the last background; unrelated
/// fixed ranges keep relative order but may shift slots. The transparent
/// queue matches the native descending key comparator.
2026-10-11 13:12:50 +04:00
fn rebuild_draw_order ( & mut self ) {
let range_count = self . draw_ranges . len ();
if self . draw_order . len () != range_count {
self . draw_order = ( 0 .. range_count ). collect ();
}
self . draw_order = sorted_world_draw_order (
range_count ,
& self . draw_ranges ,
& self . draw_range_world_queue ,
& self . draw_range_sort_keys ,
2026-10-11 21:41:37 +04:00
& self . world_transparent_background_ranges ,
2026-10-11 13:12:50 +04:00
);
}
2026-06-25 06:42:28 +04:00
fn record_command_buffer (
& mut self ,
image_index : usize ,
) -> Result < (), VulkanSmokeRendererError > {
2026-10-11 13:12:50 +04:00
if self . draw_order_dirty {
self . rebuild_draw_order ();
self . draw_order_dirty = false ;
}
2026-06-25 06:42:28 +04:00
let device = self . device_ref () ? ;
let swapchain = self . swapchain_ref () ? ;
let resources = self . resources_ref () ? ;
let command_buffer = resources . command_buffers [ image_index ];
// SAFETY: The command buffer belongs to the resettable pool owned by this renderer.
unsafe {
device
. device ()
. reset_command_buffer ( command_buffer , vk ::CommandBufferResetFlags ::empty ())
}
. map_err ( | error | VulkanSmokeRendererError ::VulkanOperation {
context : "vkResetCommandBuffer" ,
result : error ,
}) ? ;
let begin_info = vk ::CommandBufferBeginInfo ::default ();
// SAFETY: The command buffer is in the initial state after reset and recorded on one thread.
unsafe {
device
. device ()
. begin_command_buffer ( command_buffer , & begin_info )
}
. map_err ( | error | VulkanSmokeRendererError ::VulkanOperation {
context : "vkBeginCommandBuffer" ,
result : error ,
}) ? ;
2026-07-18 08:31:19 +04:00
let clear_values = [
vk ::ClearValue {
color : vk ::ClearColorValue {
float32 : [ 0.05 , 0.08 , 0.11 , 1.0 ],
},
2026-06-25 06:42:28 +04:00
},
2026-07-18 08:31:19 +04:00
vk ::ClearValue {
depth_stencil : vk ::ClearDepthStencilValue {
depth : 1.0 ,
stencil : 0 ,
},
},
];
2026-06-25 06:42:28 +04:00
let render_area = vk ::Rect2D {
offset : vk ::Offset2D { x : 0 , y : 0 },
extent : vk ::Extent2D {
width : swapchain . report . plan . extent . 0 ,
height : swapchain . report . plan . extent . 1 ,
},
};
let render_pass_info = vk ::RenderPassBeginInfo ::default ()
. render_pass ( resources . render_pass )
. framebuffer ( resources . framebuffers [ image_index ])
. render_area ( render_area )
. clear_values ( & clear_values );
// SAFETY: All commands target live frame resources owned by this renderer.
unsafe {
device . device (). cmd_begin_render_pass (
command_buffer ,
& render_pass_info ,
vk ::SubpassContents ::INLINE ,
);
let vertex_buffers = [ self . vertex_buffer_ref () ? . buffer ];
let offsets = [ 0_ u64 ];
device
. device ()
. cmd_bind_vertex_buffers ( command_buffer , 0 , & vertex_buffers , & offsets );
device . device (). cmd_bind_index_buffer (
command_buffer ,
self . index_buffer_ref () ? . buffer ,
0 ,
2026-07-18 15:19:07 +04:00
vk ::IndexType ::UINT32 ,
2026-06-25 06:42:28 +04:00
);
2026-10-30 18:28:45 +04:00
let mut viewport_depth = None ;
2026-10-11 13:12:50 +04:00
for & range_index in & self . draw_order {
let range = self . draw_ranges . get ( range_index ). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "stable draw order range handle" ,
},
) ? ;
let texture_index = * self . draw_texture_indices . get ( range_index ). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "stable draw texture range handle" ,
},
) ? ;
2026-07-18 08:22:29 +04:00
let pipeline = resources . pipelines . get ( & range . pipeline_key ()). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "static draw pipeline variant" ,
},
) ? ;
device . device (). cmd_bind_pipeline (
command_buffer ,
vk ::PipelineBindPoint ::GRAPHICS ,
* pipeline ,
);
2026-10-11 13:12:50 +04:00
let frame_offset = u32 ::try_from (
resources
. frame_uniform_stride
. saturating_mul ( u64 ::try_from ( self . current_frame ). unwrap_or ( u64 ::MAX )),
)
. unwrap_or ( u32 ::MAX );
let material_offset = u32 ::try_from (
resources
. material_specular_stride
. saturating_mul ( u64 ::try_from ( texture_index ). unwrap_or ( u64 ::MAX )),
)
. unwrap_or ( u32 ::MAX );
2026-10-18 11:23:35 +04:00
let point_light_offset = u32 ::try_from (
resources
. point_light_stride
. saturating_mul ( u64 ::try_from ( self . current_frame ). unwrap_or ( u64 ::MAX )),
)
. unwrap_or ( u32 ::MAX );
2026-07-18 08:07:45 +04:00
let descriptor_set = resources . descriptor_sets . get ( texture_index ). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "static material descriptor set" ,
},
) ? ;
device . device (). cmd_bind_descriptor_sets (
command_buffer ,
vk ::PipelineBindPoint ::GRAPHICS ,
resources . pipeline_layout ,
0 ,
std ::slice ::from_ref ( descriptor_set ),
2026-10-18 11:23:35 +04:00
& [ frame_offset , material_offset , point_light_offset ],
2026-07-18 08:07:45 +04:00
);
2026-07-18 08:40:40 +04:00
let alpha_cutoff = range . alpha_test_cutoff (). to_ne_bytes ();
device . device (). cmd_push_constants (
command_buffer ,
resources . pipeline_layout ,
2026-10-11 13:12:50 +04:00
vk ::ShaderStageFlags ::VERTEX | vk ::ShaderStageFlags ::FRAGMENT ,
0 ,
2026-07-18 08:40:40 +04:00
& alpha_cutoff ,
);
2026-10-11 13:12:50 +04:00
let material_alpha = self . material_alphas . get ( texture_index ). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "static material alpha" ,
},
) ? ;
2026-10-30 23:16:31 +04:00
let diffuse_alpha_override = self
. draw_range_diffuse_alpha_overrides
. get ( range_index )
. ok_or ( VulkanSmokeRendererError ::InvariantViolation {
context : "draw-range diffuse-alpha override" ,
}) ? ;
let material_alphas =
draw_material_alphas ( * material_alpha , * diffuse_alpha_override )
. map ( f32 ::to_ne_bytes )
. concat ();
2026-10-11 13:12:50 +04:00
device . device (). cmd_push_constants (
command_buffer ,
resources . pipeline_layout ,
vk ::ShaderStageFlags ::VERTEX | vk ::ShaderStageFlags ::FRAGMENT ,
4 ,
& material_alphas ,
);
let material_lighting = self . material_lighting . get ( texture_index ). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "static material lighting" ,
},
) ? ;
let material_lighting = material_lighting
. iter ()
. flatten ()
. flat_map ( | value | value . to_ne_bytes ())
. collect ::< Vec < _ >> ();
device . device (). cmd_push_constants (
command_buffer ,
resources . pipeline_layout ,
vk ::ShaderStageFlags ::VERTEX | vk ::ShaderStageFlags ::FRAGMENT ,
12 ,
& material_lighting ,
);
let material_uv_transforms = self . material_uv_transforms . get ( texture_index ). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "static material UV transforms" ,
},
) ? ;
let material_uv_transforms = material_uv_transforms
. iter ()
. flatten ()
. flat_map ( | value | value . to_ne_bytes ())
. collect ::< Vec < _ >> ();
device . device (). cmd_push_constants (
command_buffer ,
resources . pipeline_layout ,
vk ::ShaderStageFlags ::VERTEX | vk ::ShaderStageFlags ::FRAGMENT ,
60 ,
& material_uv_transforms ,
);
2026-10-18 12:18:30 +04:00
let material_mode = self . material_unlit . get ( texture_index ). ok_or (
2026-10-11 13:12:50 +04:00
VulkanSmokeRendererError ::InvariantViolation {
context : "static material unlit flag" ,
},
) ? ;
2026-10-18 12:18:30 +04:00
let material_mode = if range . terrain_base_only {
(( * material_mode ). round () as i32 | 16 ) as f32
} else {
* material_mode
};
2026-10-30 18:28:45 +04:00
let viewport = super ::swapchain_resources ::draw_viewport (
swapchain . report . plan . extent ,
material_mode ,
);
let next_viewport_depth = [ viewport . min_depth , viewport . max_depth ];
if viewport_depth != Some ( next_viewport_depth ) {
device
. device ()
. cmd_set_viewport ( command_buffer , 0 , & [ viewport ]);
viewport_depth = Some ( next_viewport_depth );
}
2026-10-18 12:18:30 +04:00
let material_mode = [ material_mode . to_ne_bytes ()];
2026-10-11 13:12:50 +04:00
device . device (). cmd_push_constants (
command_buffer ,
resources . pipeline_layout ,
vk ::ShaderStageFlags ::VERTEX | vk ::ShaderStageFlags ::FRAGMENT ,
124 ,
2026-10-18 12:18:30 +04:00
& material_mode . concat (),
2026-10-11 13:12:50 +04:00
);
2026-07-18 07:56:49 +04:00
device . device (). cmd_draw_indexed (
command_buffer ,
range . index_count ,
1 ,
range . first_index ,
0 ,
0 ,
);
}
2026-06-25 06:42:28 +04:00
device . device (). cmd_end_render_pass ( command_buffer );
2026-10-11 13:12:50 +04:00
if self . readback_enabled {
if let ( Some ( image ), Some ( readback )) = (
resources . images . get ( image_index ),
resources . readback_buffers . get ( image_index ),
) {
let range = super ::color_subresource_range ( 1 );
let to_transfer = vk ::ImageMemoryBarrier ::default ()
. old_layout ( vk ::ImageLayout ::PRESENT_SRC_KHR )
. new_layout ( vk ::ImageLayout ::TRANSFER_SRC_OPTIMAL )
. src_access_mask ( vk ::AccessFlags ::COLOR_ATTACHMENT_WRITE )
. dst_access_mask ( vk ::AccessFlags ::TRANSFER_READ )
. image ( * image )
. subresource_range ( range );
let back_to_present = vk ::ImageMemoryBarrier ::default ()
. old_layout ( vk ::ImageLayout ::TRANSFER_SRC_OPTIMAL )
. new_layout ( vk ::ImageLayout ::PRESENT_SRC_KHR )
. src_access_mask ( vk ::AccessFlags ::TRANSFER_READ )
. dst_access_mask ( vk ::AccessFlags ::empty ())
. 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 : swapchain . report . plan . extent . 0 ,
height : swapchain . report . plan . extent . 1 ,
depth : 1 ,
});
device . device (). cmd_pipeline_barrier (
command_buffer ,
vk ::PipelineStageFlags ::COLOR_ATTACHMENT_OUTPUT ,
vk ::PipelineStageFlags ::TRANSFER ,
vk ::DependencyFlags ::empty (),
& [],
& [],
& [ to_transfer ],
);
device . device (). cmd_copy_image_to_buffer (
command_buffer ,
* image ,
vk ::ImageLayout ::TRANSFER_SRC_OPTIMAL ,
readback . buffer ,
& [ region ],
);
device . device (). cmd_pipeline_barrier (
command_buffer ,
vk ::PipelineStageFlags ::TRANSFER ,
vk ::PipelineStageFlags ::BOTTOM_OF_PIPE ,
vk ::DependencyFlags ::empty (),
& [],
& [],
& [ back_to_present ],
);
}
2026-07-18 09:05:20 +04:00
}
2026-06-25 06:42:28 +04:00
}
2026-06-25 07:14:16 +04:00
// SAFETY: The render pass owns the attachment layout transitions for this clear-and-present path.
unsafe { device . device (). end_command_buffer ( command_buffer ) }. map_err ( | error | {
VulkanSmokeRendererError ::VulkanOperation {
context : "vkEndCommandBuffer" ,
result : error ,
}
2026-06-25 06:42:28 +04:00
}) ? ;
Ok (())
}
fn destroy_swapchain_resources ( & mut self ) {
let Some ( device ) = self . device . as_ref () else {
return ;
};
for sync in self . frame_sync . drain ( .. ) {
// SAFETY: These sync objects belong to this device and are destroyed once.
unsafe {
device
. device ()
. destroy_semaphore ( sync . image_available , None );
device . device (). destroy_fence ( sync . fence , None );
}
}
if let Some ( resources ) = self . swapchain_resources . take () {
destroy_swapchain_resources ( device , self . command_pool , resources );
}
self . images_in_flight . clear ();
self . current_frame = 0 ;
}
2026-06-25 22:35:08 +04:00
fn destroy_device_owned_resources ( & mut self ) {
2026-06-25 06:42:28 +04:00
self . destroy_swapchain_resources ();
if let Some ( device ) = self . device . as_ref () {
2026-07-18 08:07:45 +04:00
for texture in self . textures . drain ( .. ) {
2026-07-18 07:29:31 +04:00
destroy_allocated_image ( device , & texture );
}
2026-06-25 06:42:28 +04:00
if let Some ( buffer ) = self . index_buffer . take () {
// SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
unsafe {
device . device (). destroy_buffer ( buffer . buffer , None );
device . device (). free_memory ( buffer . memory , None );
}
}
if let Some ( buffer ) = self . vertex_buffer . take () {
// SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
unsafe {
device . device (). destroy_buffer ( buffer . buffer , None );
device . device (). free_memory ( buffer . memory , None );
}
}
// SAFETY: The command pool belongs to this device and is destroyed once after the device is idle and all command buffers allocated from it were freed above.
unsafe {
device
. device ()
. destroy_command_pool ( self . command_pool , None );
};
}
2026-06-25 22:35:08 +04:00
self . pending_extent = None ;
}
fn shutdown_inner ( & mut self ) -> Result < VulkanSmokeShutdownReport , VulkanSmokeRendererError > {
2026-07-18 09:05:20 +04:00
let completed_readback = if let Some ( device ) = self . device . as_ref () {
2026-06-25 22:35:08 +04:00
// SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
unsafe { device . device (). device_wait_idle () }. map_err ( | error | {
VulkanSmokeRendererError ::VulkanOperation {
context : "vkDeviceWaitIdle" ,
result : error ,
}
}) ? ;
2026-07-18 09:05:20 +04:00
self . completed_readback ( device ) ?
} else {
None
};
2026-07-18 09:08:07 +04:00
let readback_artifact = completed_readback . map ( | bytes | {
self . report . readback_byte_count = u64 ::try_from ( bytes . len ()). unwrap_or ( u64 ::MAX );
self . report . readback_fnv1a64 = fnv1a64 ( & bytes );
VulkanReadbackArtifact {
format : self
. swapchain_ref ()
. map_or ( 0 , | swapchain | swapchain . report . plan . format . format ),
width : self . report . swapchain_extent . 0 ,
height : self . report . swapchain_extent . 1 ,
bytes ,
}
});
2026-06-25 22:35:08 +04:00
self . destroy_device_owned_resources ();
2026-07-03 16:55:19 +04:00
let validation = take_runtime_children_with_validation_snapshot (
2026-06-25 22:35:08 +04:00
& mut self . surface ,
& mut self . device ,
& mut self . swapchain ,
2026-07-18 05:31:39 +04:00
self . validation . as_ref (),
2026-06-25 22:35:08 +04:00
VulkanValidationMessenger ::report ,
)
. unwrap_or_default ();
2026-07-03 16:55:19 +04:00
self . validation . take ();
self . instance . take ();
2026-06-25 22:35:08 +04:00
Ok ( VulkanSmokeShutdownReport {
renderer_report : self . report . clone (),
2026-07-18 09:08:07 +04:00
readback_artifact ,
2026-06-25 22:35:08 +04:00
swapchain_recreate_count : self . swapchain_recreate_count ,
validation ,
})
}
2026-07-18 09:05:20 +04:00
fn completed_readback (
& self ,
device : & VulkanLogicalDeviceProbe ,
2026-07-18 09:08:07 +04:00
) -> Result < Option < Vec < u8 >> , VulkanSmokeRendererError > {
2026-07-18 09:05:20 +04:00
let Some ( resources ) = self . swapchain_resources . as_ref () else {
return Ok ( None );
};
let byte_len = usize ::try_from ( self . report . swapchain_extent . 0 )
. ok ()
. and_then ( | width | {
usize ::try_from ( self . report . swapchain_extent . 1 )
. ok ()
. and_then ( | height | width . checked_mul ( height ))
})
. and_then ( | pixels | pixels . checked_mul ( 4 ))
. ok_or ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "completed readback byte length" ,
}) ? ;
2026-10-11 13:12:50 +04:00
if ! self . readback_enabled || resources . readback_buffers . is_empty () {
2026-07-18 09:05:20 +04:00
return Ok ( None );
}
2026-07-18 19:00:43 +04:00
let Some ( image_index ) = completed_readback_buffer_index (
self . last_readback_image_index ,
resources . readback_buffers . len (),
) ?
else {
return Ok ( None );
};
let buffer = resources . readback_buffers . get ( image_index ). ok_or (
VulkanSmokeRendererError ::InvariantViolation {
context : "last readback image index" ,
},
) ? ;
Ok ( Some ( readback_buffer_bytes ( device , buffer , byte_len ) ? ))
2026-07-18 09:05:20 +04:00
}
2026-06-25 22:35:08 +04:00
fn teardown ( & mut self ) {
if let Some ( device ) = self . device . as_ref () {
// SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
let _ = unsafe { device . device (). device_wait_idle () };
}
self . destroy_device_owned_resources ();
2026-07-03 16:55:19 +04:00
let _ = take_runtime_children_with_validation_snapshot (
2026-06-25 07:18:31 +04:00
& mut self . surface ,
& mut self . device ,
& mut self . swapchain ,
2026-07-18 05:31:39 +04:00
self . validation . as_ref (),
2026-06-25 22:35:08 +04:00
VulkanValidationMessenger ::report ,
2026-06-25 07:18:31 +04:00
);
2026-07-03 16:55:19 +04:00
self . validation . take ();
self . instance . take ();
2026-06-25 06:42:28 +04:00
}
}
2026-07-18 19:00:43 +04:00
fn completed_readback_buffer_index (
last_image_index : Option < usize > ,
buffer_count : usize ,
) -> Result < Option < usize > , VulkanSmokeRendererError > {
match last_image_index {
None => Ok ( None ),
Some ( index ) if index < buffer_count => Ok ( Some ( index )),
Some ( _ ) => Err ( VulkanSmokeRendererError ::InvariantViolation {
context : "last readback image index" ,
}),
}
}
2026-07-18 09:08:07 +04:00
fn fnv1a64 ( bytes : & [ u8 ]) -> u64 {
bytes . iter (). fold ( 0xcbf2_9ce4_8422_2325_ u64 , | hash , byte | {
( hash ^ u64 ::from ( * byte )). wrapping_mul ( 0x0000_0100_0000_01b3 )
})
}
2026-06-25 06:42:28 +04:00
impl Drop for VulkanSmokeRenderer {
fn drop ( & mut self ) {
self . teardown ();
}
}
2026-06-25 07:18:31 +04:00
2026-10-11 13:12:50 +04:00
fn validate_geometry_upload (
vertices : & [ super ::VulkanStaticVertex ],
indices : & [ u32 ],
current_ranges : & [ super ::VulkanStaticDrawRange ],
geometry_ranges : & [ super ::VulkanDynamicDrawRange ],
retained_vertex_capacity : usize ,
retained_index_capacity : usize ,
) -> Result < (), VulkanSmokeRendererError > {
if vertices . is_empty () || indices . is_empty () {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh upload cannot have zero-sized geometry" ,
});
}
if ! indices . len (). is_multiple_of ( 3 ) {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh index upload is not a triangle list" ,
});
}
if 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 ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh vertex attributes must be finite and overlay alpha must be 0..1" ,
});
}
if indices . iter (). any ( |& index | {
usize ::try_from ( index )
. ok ()
. is_none_or ( | index | index >= vertices . len ())
}) {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh index exceeds vertex count" ,
});
}
if retained_vertex_capacity == 0 || retained_index_capacity == 0 {
return Err ( VulkanSmokeRendererError ::InvariantViolation {
context : "dynamic mesh retained allocation is empty" ,
});
}
let mut next_ranges = current_ranges . to_vec ();
let mut changed = vec! [ false ; current_ranges . len ()];
for geometry in geometry_ranges {
if geometry . range_index >= current_ranges . len () {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh range index is out of bounds" ,
});
}
if changed [ geometry . range_index ] {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh range is updated more than once" ,
});
}
changed [ geometry . range_index ] = true ;
if geometry . first_index % 3 != 0 || geometry . index_count % 3 != 0 {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh range is not triangle aligned" ,
});
}
let first = usize ::try_from ( geometry . first_index ). map_err ( | _ | {
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh range first index exceeds address space" ,
}
}) ? ;
let count = usize ::try_from ( geometry . index_count ). map_err ( | _ | {
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh range index count exceeds address space" ,
}
}) ? ;
let end = first
. checked_add ( count )
. ok_or ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh range exceeds address space" ,
}) ? ;
if end > indices . len () {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "dynamic mesh range exceeds uploaded indices" ,
});
}
next_ranges [ geometry . range_index ]. first_index = geometry . first_index ;
next_ranges [ geometry . range_index ]. index_count = geometry . index_count ;
}
for range in next_ranges {
if range . first_index % 3 != 0 || range . index_count % 3 != 0 {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "retained dynamic mesh range is not triangle aligned" ,
});
}
let first = usize ::try_from ( range . first_index ). map_err ( | _ | {
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "retained dynamic mesh range first index exceeds address space" ,
}
}) ? ;
let count = usize ::try_from ( range . index_count ). map_err ( | _ | {
VulkanSmokeRendererError ::InvalidStaticMesh {
context : "retained dynamic mesh range index count exceeds address space" ,
}
}) ? ;
let end = first
. checked_add ( count )
. ok_or ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "retained dynamic mesh range exceeds address space" ,
}) ? ;
if end > indices . len () {
return Err ( VulkanSmokeRendererError ::InvalidStaticMesh {
context : "retained dynamic mesh range exceeds uploaded indices" ,
});
}
}
Ok (())
}
fn apply_geometry_range_updates (
ranges : & mut [ super ::VulkanStaticDrawRange ],
capacities : & mut [ u32 ],
2026-10-19 23:05:50 +04:00
world_queues : & [ Option < bool > ],
2026-10-11 13:12:50 +04:00
geometry_ranges : & [ super ::VulkanDynamicDrawRange ],
2026-10-19 23:05:50 +04:00
draw_order_dirty : & mut bool ,
2026-10-11 13:12:50 +04:00
) {
for geometry in geometry_ranges {
// `validate_geometry_upload` has already checked every handle. Keep
// this helper infallible so state is changed only after the Vulkan
// upload succeeds.
let range = & mut ranges [ geometry . range_index ];
2026-10-19 23:05:50 +04:00
mark_draw_order_dirty_on_activity_change (
world_queues [ geometry . range_index ],
range . index_count ,
geometry . index_count ,
draw_order_dirty ,
);
2026-10-11 13:12:50 +04:00
range . first_index = geometry . first_index ;
range . index_count = geometry . index_count ;
// A moved segment no longer owns the old tail. Keeping the previous
// range capacity would let a later count setter read indices from a
// different segment. The shared GPU allocation itself remains
// retained; this is the logical capacity of this range's segment.
capacities [ geometry . range_index ] = geometry . index_count ;
}
}
2026-10-11 21:41:37 +04:00
fn apply_world_transparency (
base_state : LegacyPipelineState ,
transparent : bool ,
) -> LegacyPipelineState {
if transparent && base_state . depth != LegacyDepthMode ::Disabled {
LegacyPipelineState {
depth : LegacyDepthMode ::TestReadOnly ,
.. base_state
}
} else {
base_state
}
}
2026-10-30 23:16:31 +04:00
fn draw_material_alphas (
material_alphas : [ f32 ; 2 ],
diffuse_alpha_override : Option < f32 > ,
) -> [ f32 ; 2 ] {
[
diffuse_alpha_override . unwrap_or ( material_alphas [ 0 ]),
material_alphas [ 1 ],
]
}
2026-10-11 21:41:37 +04:00
fn world_material_pipeline_states (
original : LegacyPipelineState ,
category : u8 ,
world_queue : Option < bool > ,
) -> Option < ( LegacyPipelineState , LegacyPipelineState ) > {
let categorized = original . with_material_category ( category ) ? ;
let Some ( transparent ) = world_queue else {
return Some (( original , original ));
};
let base = categorized ;
Some (( base , apply_world_transparency ( base , transparent )))
}
2026-10-11 13:12:50 +04:00
/// Replays the native queue construction from the original stable range
2026-10-11 21:41:37 +04:00
/// order. Without explicit background ranges, world opaque ranges replace the
/// existing world slots and world transparent ranges follow in descending
/// camera distance order. With explicit backgrounds, opaque ranges are
/// compacted into world positions and sorted transparent ranges are inserted
/// after the last active background; fixed passes keep their relative order but
/// can shift slots. Zero-index-count ranges are inactive and are omitted.
2026-10-11 13:12:50 +04:00
fn sorted_world_draw_order (
range_count : usize ,
ranges : & [ super ::VulkanStaticDrawRange ],
world_queue : & [ Option < bool > ],
keys : & [ Option < f32 > ],
2026-10-11 21:41:37 +04:00
transparent_background_ranges : & [ usize ],
2026-10-11 13:12:50 +04:00
) -> Vec < usize > {
let mut next = ( 0 .. range_count ). collect ::< Vec < _ >> ();
let mut world_opaque = Vec ::new ();
let mut world_transparent = Vec ::new ();
for range_index in 0 .. range_count {
if ranges
. get ( range_index )
. is_none_or ( | range | range . index_count == 0 )
{
continue ;
}
match world_queue . get ( range_index ). copied (). flatten () {
Some ( false ) => world_opaque . push ( range_index ),
Some ( true ) => world_transparent . push ( range_index ),
None => {}
}
}
// Native equal-key insertion is a midpoint insertion rather than a
// documented stable sort. Replay that insertion shape from original range
// order instead of repeatedly sorting the previous frame's order.
let mut ordered_transparent = Vec ::with_capacity ( world_transparent . len ());
for range_index in world_transparent {
let new_key = keys . get ( range_index ). and_then ( | key | * key );
let mut low = 0 usize ;
let mut high = ordered_transparent . len (). saturating_sub ( 1 );
while high > low {
let middle = low + ( high - low ) / 2 ;
let existing_key = keys . get ( ordered_transparent [ middle ]). and_then ( | key | * key );
match ( new_key , existing_key ) {
( Some ( new_key ), Some ( existing_key )) if new_key > existing_key => high = middle ,
( Some ( new_key ), Some ( existing_key )) if new_key < existing_key => low = middle + 1 ,
( Some ( _ ), Some ( _ )) => {
low = middle ;
break ;
}
( Some ( _ ), None ) => high = middle ,
( None , Some ( _ )) => low = middle + 1 ,
( None , None ) => {
low = middle ;
break ;
}
}
}
if ! ordered_transparent . is_empty () && high == low {
let existing_key = keys . get ( ordered_transparent [ low ]). and_then ( | key | * key );
let insert_after = match ( new_key , existing_key ) {
( Some ( new_key ), Some ( existing_key )) => new_key < existing_key ,
( None , Some ( _ )) => true ,
_ => false ,
};
if insert_after {
low += 1 ;
}
}
ordered_transparent . insert ( low , range_index );
}
2026-10-11 21:41:37 +04:00
if transparent_background_ranges . is_empty () {
let mut world_order = world_opaque ;
world_order . extend ( ordered_transparent );
let mut world_cursor = 0 usize ;
for slot in & mut next {
if world_queue . get ( * slot ). copied (). flatten (). is_some ()
&& ranges
. get ( * slot )
. is_some_and ( | range | range . index_count != 0 )
{
if let Some ( & replacement ) = world_order . get ( world_cursor ) {
* slot = replacement ;
world_cursor += 1 ;
}
2026-10-11 13:12:50 +04:00
}
}
2026-10-11 21:41:37 +04:00
return next ;
}
// Fixed sky backgrounds are not world-queue members. Remove the world
// transparent slots, compact opaque ranges into the world slots, then
// insert the sorted transparent queue after the explicit background group.
let mut opaque_cursor = 0 usize ;
let mut next_without_transparency = Vec ::with_capacity ( range_count );
for slot in 0 .. range_count {
let active_world = ranges . get ( slot ). is_some_and ( | range | range . index_count != 0 );
match world_queue . get ( slot ). copied (). flatten () {
Some ( true ) if active_world => {}
Some ( false ) if active_world => {
if let Some ( & replacement ) = world_opaque . get ( opaque_cursor ) {
next_without_transparency . push ( replacement );
opaque_cursor += 1 ;
} else {
next_without_transparency . push ( slot );
}
}
_ => next_without_transparency . push ( slot ),
}
2026-10-11 13:12:50 +04:00
}
2026-10-11 21:41:37 +04:00
let insertion = transparent_background_ranges
. iter ()
. filter_map ( | background | {
next_without_transparency
. iter ()
. position ( | range | range == background )
})
. max ()
. map ( | index | index + 1 );
if let Some ( insertion ) = insertion {
next_without_transparency . splice ( insertion .. insertion , ordered_transparent );
}
next_without_transparency
2026-10-11 13:12:50 +04:00
}
2026-10-19 23:05:50 +04:00
fn set_and_mark_dirty_if_changed < T : PartialEq > (
current : & mut T ,
next : T ,
draw_order_dirty : & mut bool ,
) {
if * current == next {
return ;
}
* current = next ;
* draw_order_dirty = true ;
}
fn mark_draw_order_dirty_on_activity_change (
world_queue : Option < bool > ,
current_index_count : u32 ,
next_index_count : u32 ,
draw_order_dirty : & mut bool ,
) {
let active_changed = ( current_index_count == 0 ) != ( next_index_count == 0 );
if world_queue . is_some () && active_changed {
* draw_order_dirty = true ;
}
}
2026-06-25 07:18:31 +04:00
#[cfg(test)]
mod tests {
2026-10-11 13:12:50 +04:00
use super ::super ::{ VulkanDynamicDrawRange , VulkanStaticDrawRange , VulkanStaticMesh };
2026-06-25 22:35:08 +04:00
use super ::{
2026-10-11 21:41:37 +04:00
apply_geometry_range_updates , apply_world_transparency , completed_readback_buffer_index ,
2026-10-30 23:16:31 +04:00
draw_material_alphas , mark_draw_order_dirty_on_activity_change ,
set_and_mark_dirty_if_changed , sorted_world_draw_order ,
take_runtime_children_with_validation_snapshot , take_runtime_owners_in_dependency_order ,
validate_geometry_upload , world_material_pipeline_states , RollbackOnDrop ,
2026-06-25 22:35:08 +04:00
};
2026-10-31 17:16:52 +04:00
use fparkan_render ::{
LegacyBlendMode , LegacyCullMode , LegacyDepthBias , LegacyDepthMode , LegacyPipelineState ,
};
2026-06-25 07:18:31 +04:00
use std ::cell ::RefCell ;
use std ::rc ::Rc ;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TeardownStep {
2026-06-25 22:35:08 +04:00
Snapshot ,
2026-06-25 07:18:31 +04:00
Instance ,
Validation ,
Surface ,
Device ,
Swapchain ,
}
struct DropTracker {
step : TeardownStep ,
log : Rc < RefCell < Vec < TeardownStep >>> ,
}
impl Drop for DropTracker {
fn drop ( & mut self ) {
self . log . borrow_mut (). push ( self . step );
}
}
fn tracker ( step : TeardownStep , log : & Rc < RefCell < Vec < TeardownStep >>> ) -> DropTracker {
DropTracker {
step ,
log : Rc ::clone ( log ),
}
}
fn record_teardown_steps ( present_steps : & [ TeardownStep ]) -> Vec < TeardownStep > {
let log = Rc ::new ( RefCell ::new ( Vec ::new ()));
let mut instance = present_steps
. contains ( & TeardownStep ::Instance )
. then ( || tracker ( TeardownStep ::Instance , & log ));
let mut validation = present_steps
. contains ( & TeardownStep ::Validation )
. then ( || tracker ( TeardownStep ::Validation , & log ));
let mut surface = present_steps
. contains ( & TeardownStep ::Surface )
. then ( || tracker ( TeardownStep ::Surface , & log ));
let mut device = present_steps
. contains ( & TeardownStep ::Device )
. then ( || tracker ( TeardownStep ::Device , & log ));
let mut swapchain = present_steps
. contains ( & TeardownStep ::Swapchain )
. then ( || tracker ( TeardownStep ::Swapchain , & log ));
take_runtime_owners_in_dependency_order (
& mut instance ,
& mut validation ,
& mut surface ,
& mut device ,
& mut swapchain ,
);
Rc ::into_inner ( log )
. expect ( "all drop trackers released" )
. into_inner ()
}
#[test]
fn runtime_owners_drop_in_explicit_dependency_order () {
assert_eq! (
record_teardown_steps ( & [
TeardownStep ::Instance ,
TeardownStep ::Validation ,
TeardownStep ::Surface ,
TeardownStep ::Device ,
TeardownStep ::Swapchain ,
]),
vec! [
TeardownStep ::Swapchain ,
TeardownStep ::Device ,
TeardownStep ::Surface ,
TeardownStep ::Validation ,
TeardownStep ::Instance ,
]
);
}
2026-07-18 19:00:43 +04:00
#[test]
fn completed_readback_selects_only_the_last_submitted_image () {
assert_eq! ( completed_readback_buffer_index ( None , 2 ), Ok ( None ));
assert_eq! ( completed_readback_buffer_index ( Some ( 1 ), 2 ), Ok ( Some ( 1 )));
assert! ( completed_readback_buffer_index ( Some ( 2 ), 2 ). is_err ());
}
2026-10-11 13:12:50 +04:00
#[test]
fn geometry_updates_preserve_other_range_runtime_state () {
let base_state = LegacyPipelineState {
blend : LegacyBlendMode ::SourceAlpha ,
depth : LegacyDepthMode ::TestReadOnly ,
.. LegacyPipelineState ::default ()
};
let mut ranges = vec! [
VulkanStaticDrawRange {
first_index : 0 ,
index_count : 3 ,
material_index : 7 ,
lightmap_index : 4 ,
batch_flags : 0x108 ,
pipeline_state : base_state ,
2026-10-18 12:18:30 +04:00
terrain_base_only : false ,
2026-10-11 13:12:50 +04:00
alpha_test_reference : 17 ,
},
VulkanStaticDrawRange {
first_index : 3 ,
index_count : 3 ,
material_index : 9 ,
lightmap_index : u8 ::MAX ,
batch_flags : 0 ,
pipeline_state : LegacyPipelineState ::default (),
2026-10-18 12:18:30 +04:00
terrain_base_only : false ,
2026-10-11 13:12:50 +04:00
alpha_test_reference : 0 ,
},
];
let original = ranges [ 0 ];
let mut capacities = vec! [ 3 , 3 ];
2026-10-19 23:05:50 +04:00
let mut draw_order_dirty = false ;
2026-10-11 13:12:50 +04:00
apply_geometry_range_updates (
& mut ranges ,
& mut capacities ,
2026-10-19 23:05:50 +04:00
& [ None , None ],
2026-10-11 13:12:50 +04:00
& [ VulkanDynamicDrawRange {
range_index : 1 ,
first_index : 3 ,
index_count : 9 ,
}],
2026-10-19 23:05:50 +04:00
& mut draw_order_dirty ,
2026-10-11 13:12:50 +04:00
);
assert_eq! ( ranges [ 0 ], original );
assert_eq! ( capacities , vec! [ 3 , 9 ]);
assert_eq! ( ranges [ 1 ]. first_index , 3 );
assert_eq! ( ranges [ 1 ]. index_count , 9 );
apply_geometry_range_updates (
& mut ranges ,
& mut capacities ,
2026-10-19 23:05:50 +04:00
& [ None , None ],
2026-10-11 13:12:50 +04:00
& [ VulkanDynamicDrawRange {
range_index : 1 ,
first_index : 3 ,
index_count : 3 ,
}],
2026-10-19 23:05:50 +04:00
& mut draw_order_dirty ,
2026-10-11 13:12:50 +04:00
);
assert_eq! ( ranges [ 0 ], original );
assert_eq! ( capacities , vec! [ 3 , 3 ]);
assert_eq! ( ranges [ 1 ]. index_count , 3 );
}
#[test]
fn geometry_update_can_disable_empty_shadow_range_without_zero_sized_upload () {
let mesh = VulkanStaticMesh ::smoke_triangle ();
let geometry = [ VulkanDynamicDrawRange {
range_index : 0 ,
first_index : 3 ,
index_count : 0 ,
}];
assert_eq! (
validate_geometry_upload (
& mesh . vertices ,
& mesh . indices ,
& mesh . draw_ranges ,
& geometry ,
3 ,
3 ,
),
Ok (())
);
}
2026-10-19 23:05:50 +04:00
#[test]
fn geometry_updates_dirty_world_order_on_activation_and_deactivation () {
let mut ranges = [ VulkanStaticDrawRange {
first_index : 0 ,
index_count : 0 ,
material_index : 0 ,
lightmap_index : u8 ::MAX ,
batch_flags : 0 ,
pipeline_state : LegacyPipelineState ::default (),
terrain_base_only : false ,
alpha_test_reference : 0 ,
}];
let mut capacities = [ 6 ];
let world_queues = [ Some ( true )];
let mut draw_order_dirty = false ;
apply_geometry_range_updates (
& mut ranges ,
& mut capacities ,
& world_queues ,
& [ VulkanDynamicDrawRange {
range_index : 0 ,
first_index : 0 ,
index_count : 3 ,
}],
& mut draw_order_dirty ,
);
assert! (
draw_order_dirty ,
"activating the queued range changes order"
);
draw_order_dirty = false ;
apply_geometry_range_updates (
& mut ranges ,
& mut capacities ,
& world_queues ,
& [ VulkanDynamicDrawRange {
range_index : 0 ,
first_index : 0 ,
index_count : 6 ,
}],
& mut draw_order_dirty ,
);
assert! (
! draw_order_dirty ,
"changing an active count does not change order"
);
apply_geometry_range_updates (
& mut ranges ,
& mut capacities ,
& world_queues ,
& [ VulkanDynamicDrawRange {
range_index : 0 ,
first_index : 0 ,
index_count : 0 ,
}],
& mut draw_order_dirty ,
);
assert! (
draw_order_dirty ,
"deactivating the queued range changes order"
);
}
2026-06-25 07:18:31 +04:00
#[test]
fn runtime_owners_drop_remaining_children_after_partial_init_failures () {
let cases = [
( vec! [ TeardownStep ::Instance ], vec! [ TeardownStep ::Instance ]),
(
vec! [ TeardownStep ::Instance , TeardownStep ::Validation ],
vec! [ TeardownStep ::Validation , TeardownStep ::Instance ],
),
(
vec! [
TeardownStep ::Instance ,
TeardownStep ::Validation ,
TeardownStep ::Surface ,
],
vec! [
TeardownStep ::Surface ,
TeardownStep ::Validation ,
TeardownStep ::Instance ,
],
),
(
vec! [
TeardownStep ::Instance ,
TeardownStep ::Validation ,
TeardownStep ::Surface ,
TeardownStep ::Device ,
],
vec! [
TeardownStep ::Device ,
TeardownStep ::Surface ,
TeardownStep ::Validation ,
TeardownStep ::Instance ,
],
),
(
vec! [
TeardownStep ::Instance ,
TeardownStep ::Surface ,
TeardownStep ::Device ,
TeardownStep ::Swapchain ,
],
vec! [
TeardownStep ::Swapchain ,
TeardownStep ::Device ,
TeardownStep ::Surface ,
TeardownStep ::Instance ,
],
),
];
for ( present_steps , expected ) in cases {
assert_eq! ( record_teardown_steps ( & present_steps ), expected );
}
}
2026-06-25 07:32:26 +04:00
2026-06-25 22:35:08 +04:00
#[test]
2026-07-03 16:55:19 +04:00
fn final_validation_snapshot_is_captured_after_surface_device_swapchain_drop () {
2026-06-25 22:35:08 +04:00
let log = Rc ::new ( RefCell ::new ( Vec ::new ()));
let mut instance = Some ( tracker ( TeardownStep ::Instance , & log ));
let mut validation = Some ( tracker ( TeardownStep ::Validation , & log ));
let mut surface = Some ( tracker ( TeardownStep ::Surface , & log ));
let mut device = Some ( tracker ( TeardownStep ::Device , & log ));
let mut swapchain = Some ( tracker ( TeardownStep ::Swapchain , & log ));
2026-07-03 16:55:19 +04:00
let snapshot = take_runtime_children_with_validation_snapshot (
2026-06-25 22:35:08 +04:00
& mut surface ,
& mut device ,
& mut swapchain ,
2026-07-18 05:31:39 +04:00
validation . as_ref (),
2026-06-25 22:35:08 +04:00
| _ | {
log . borrow_mut (). push ( TeardownStep ::Snapshot );
TeardownStep ::Validation
},
);
2026-07-03 16:55:19 +04:00
validation . take ();
instance . take ();
2026-06-25 22:35:08 +04:00
assert_eq! ( snapshot , Some ( TeardownStep ::Validation ));
assert_eq! (
Rc ::into_inner ( log )
. expect ( "all drop trackers released" )
. into_inner (),
vec! [
TeardownStep ::Swapchain ,
TeardownStep ::Device ,
TeardownStep ::Surface ,
2026-07-03 16:55:19 +04:00
TeardownStep ::Snapshot ,
2026-06-25 22:35:08 +04:00
TeardownStep ::Validation ,
TeardownStep ::Instance ,
]
);
}
2026-06-25 07:32:26 +04:00
#[test]
fn rollback_guard_runs_cleanup_when_later_step_fails () {
let log = Rc ::new ( RefCell ::new ( Vec ::new ()));
{
let _guard = RollbackOnDrop ::new ( tracker ( TeardownStep ::Swapchain , & log ), | tracker | {
drop ( tracker )
});
}
assert_eq! ( log . borrow (). as_slice (), & [ TeardownStep ::Swapchain ]);
}
#[test]
fn rollback_guard_skips_cleanup_after_commit () {
let log = Rc ::new ( RefCell ::new ( Vec ::new ()));
let tracker = RollbackOnDrop ::new ( tracker ( TeardownStep ::Swapchain , & log ), | tracker | {
drop ( tracker )
})
. commit ();
assert! ( log . borrow (). is_empty ());
drop ( tracker );
assert_eq! ( log . borrow (). as_slice (), & [ TeardownStep ::Swapchain ]);
}
2026-10-11 13:12:50 +04:00
#[test]
fn native_transparent_order_is_descending_and_keeps_fixed_pass_slots () {
let ranges = ( 0 .. 5 )
. map ( | _ | super ::super ::VulkanStaticDrawRange {
first_index : 0 ,
index_count : 3 ,
material_index : 0 ,
lightmap_index : u8 ::MAX ,
batch_flags : 0 ,
pipeline_state : fparkan_render ::LegacyPipelineState ::default (),
2026-10-18 12:18:30 +04:00
terrain_base_only : false ,
2026-10-11 13:12:50 +04:00
alpha_test_reference : 0 ,
})
. collect ::< Vec < _ >> ();
let order = sorted_world_draw_order (
5 ,
& ranges ,
& [ None , Some ( true ), None , Some ( true ), Some ( false )],
& [ None , Some ( 2.0 ), None , Some ( 9.0 ), Some ( - 3.0 )],
2026-10-11 21:41:37 +04:00
& [],
2026-10-11 13:12:50 +04:00
);
assert_eq! ( order , vec! [ 0 , 4 , 2 , 3 , 1 ]);
}
#[test]
fn inactive_world_range_is_not_reused_as_a_sort_slot () {
let ranges = vec! [
super ::super ::VulkanStaticDrawRange {
first_index : 0 ,
index_count : 3 ,
material_index : 0 ,
lightmap_index : u8 ::MAX ,
batch_flags : 0 ,
pipeline_state : fparkan_render ::LegacyPipelineState ::default (),
2026-10-18 12:18:30 +04:00
terrain_base_only : false ,
2026-10-11 13:12:50 +04:00
alpha_test_reference : 0 ,
},
super ::super ::VulkanStaticDrawRange {
first_index : 3 ,
index_count : 0 ,
material_index : 0 ,
lightmap_index : u8 ::MAX ,
batch_flags : 0 ,
pipeline_state : fparkan_render ::LegacyPipelineState ::default (),
2026-10-18 12:18:30 +04:00
terrain_base_only : false ,
2026-10-11 13:12:50 +04:00
alpha_test_reference : 0 ,
},
super ::super ::VulkanStaticDrawRange {
first_index : 3 ,
index_count : 3 ,
material_index : 0 ,
lightmap_index : u8 ::MAX ,
batch_flags : 0 ,
pipeline_state : fparkan_render ::LegacyPipelineState ::default (),
2026-10-18 12:18:30 +04:00
terrain_base_only : false ,
2026-10-11 13:12:50 +04:00
alpha_test_reference : 0 ,
},
];
let order = sorted_world_draw_order (
3 ,
& ranges ,
& [ None , Some ( true ), Some ( true )],
& [ None , Some ( 100.0 ), Some ( 1.0 )],
2026-10-11 21:41:37 +04:00
& [],
2026-10-11 13:12:50 +04:00
);
assert_eq! ( order , vec! [ 0 , 1 , 2 ]);
}
#[test]
fn native_equal_key_insertion_uses_inclusive_midpoint () {
let ranges = ( 0 .. 4 )
. map ( | index | super ::super ::VulkanStaticDrawRange {
first_index : index * 3 ,
index_count : 3 ,
material_index : 0 ,
lightmap_index : u8 ::MAX ,
batch_flags : 0 ,
pipeline_state : fparkan_render ::LegacyPipelineState ::default (),
2026-10-18 12:18:30 +04:00
terrain_base_only : false ,
2026-10-11 13:12:50 +04:00
alpha_test_reference : 0 ,
})
. collect ::< Vec < _ >> ();
2026-10-11 21:41:37 +04:00
let order = sorted_world_draw_order ( 4 , & ranges , & [ Some ( true ); 4 ], & [ Some ( 1.0 ); 4 ], & []);
2026-10-11 13:12:50 +04:00
assert_eq! ( order , vec! [ 2 , 3 , 1 , 0 ]);
}
2026-10-11 21:41:37 +04:00
2026-10-19 23:05:50 +04:00
#[test]
fn unchanged_sort_keys_and_transparency_do_not_dirty_draw_order () {
let mut sort_key = None ;
let mut draw_order_dirty = false ;
set_and_mark_dirty_if_changed ( & mut sort_key , Some ( 3.0 ), & mut draw_order_dirty );
assert_eq! ( sort_key , Some ( 3.0 ));
assert! ( draw_order_dirty );
draw_order_dirty = false ;
set_and_mark_dirty_if_changed ( & mut sort_key , Some ( 3.0 ), & mut draw_order_dirty );
assert_eq! ( sort_key , Some ( 3.0 ));
assert! ( ! draw_order_dirty );
set_and_mark_dirty_if_changed ( & mut sort_key , Some ( 4.0 ), & mut draw_order_dirty );
assert_eq! ( sort_key , Some ( 4.0 ));
assert! ( draw_order_dirty );
let mut transparent = None ;
draw_order_dirty = false ;
set_and_mark_dirty_if_changed ( & mut transparent , Some ( true ), & mut draw_order_dirty );
assert_eq! ( transparent , Some ( true ));
assert! ( draw_order_dirty );
draw_order_dirty = false ;
set_and_mark_dirty_if_changed ( & mut transparent , Some ( true ), & mut draw_order_dirty );
assert_eq! ( transparent , Some ( true ));
assert! ( ! draw_order_dirty );
set_and_mark_dirty_if_changed ( & mut transparent , Some ( false ), & mut draw_order_dirty );
assert_eq! ( transparent , Some ( false ));
assert! ( draw_order_dirty );
}
#[test]
fn world_draw_order_dirties_on_activation_and_deactivation () {
let mut draw_order_dirty = false ;
mark_draw_order_dirty_on_activity_change ( None , 0 , 6 , & mut draw_order_dirty );
assert! (
! draw_order_dirty ,
"fixed pass slots keep their original order"
);
mark_draw_order_dirty_on_activity_change ( Some ( true ), 0 , 6 , & mut draw_order_dirty );
assert! (
draw_order_dirty ,
"activating a queued range changes its order"
);
draw_order_dirty = false ;
mark_draw_order_dirty_on_activity_change ( Some ( false ), 6 , 12 , & mut draw_order_dirty );
assert! (
! draw_order_dirty ,
"changing an active count does not change order"
);
mark_draw_order_dirty_on_activity_change ( Some ( true ), 6 , 0 , & mut draw_order_dirty );
assert! (
draw_order_dirty ,
"deactivating a queued range changes its order"
);
}
2026-10-11 21:41:37 +04:00
#[test]
fn tree_canopy_queue_follows_sky_and_precedes_foreground_effects () {
const SCREEN_GRADIENT : usize = 0 ;
const TERRAIN : usize = 1 ;
const CANOPY_NEAR : usize = 2 ;
const BUILDING : usize = 3 ;
const CANOPY_FAR : usize = 4 ;
const SHADOW : usize = 5 ;
const NEBULA : usize = 6 ;
const DOME : usize = 7 ;
const CLOUDS : usize = 8 ;
const SUN : usize = 9 ;
const WEATHER : usize = 10 ;
let ranges = ( 0 .. 11 )
. map ( | index | VulkanStaticDrawRange {
first_index : index * 3 ,
index_count : 3 ,
material_index : 0 ,
lightmap_index : u8 ::MAX ,
batch_flags : 0 ,
pipeline_state : LegacyPipelineState ::default (),
2026-10-18 12:18:30 +04:00
terrain_base_only : false ,
2026-10-11 21:41:37 +04:00
alpha_test_reference : 0 ,
})
. collect ::< Vec < _ >> ();
let mut world_queue = vec! [ None ; ranges . len ()];
world_queue [ TERRAIN ] = Some ( false );
world_queue [ CANOPY_NEAR ] = Some ( true );
world_queue [ BUILDING ] = Some ( false );
world_queue [ CANOPY_FAR ] = Some ( true );
let mut sort_keys = vec! [ None ; ranges . len ()];
sort_keys [ CANOPY_NEAR ] = Some ( 2.0 );
sort_keys [ CANOPY_FAR ] = Some ( 9.0 );
let order = sorted_world_draw_order (
ranges . len (),
& ranges ,
& world_queue ,
& sort_keys ,
& [ NEBULA , DOME , CLOUDS ],
);
assert_eq! (
order ,
vec! [
SCREEN_GRADIENT ,
TERRAIN ,
BUILDING ,
SHADOW ,
NEBULA ,
DOME ,
CLOUDS ,
CANOPY_FAR ,
CANOPY_NEAR ,
SUN ,
WEATHER ,
]
);
}
2026-10-31 17:16:52 +04:00
#[test]
fn control_fx_queue_follows_opaque_and_sky_without_reactivating_empty_ranges () {
const SCREEN_GRADIENT : usize = 0 ;
const CONTROL_FX : usize = 1 ;
const TERRAIN : usize = 2 ;
const BUILDING : usize = 3 ;
const SHADOW : usize = 4 ;
const NEBULA : usize = 5 ;
const DOME : usize = 6 ;
const CLOUDS : usize = 7 ;
const EMPTY_CONTROL_FX : usize = 8 ;
const SUN : usize = 9 ;
const WEATHER : usize = 10 ;
let mut ranges = ( 0 .. 11 )
. map ( | index | VulkanStaticDrawRange {
first_index : index * 3 ,
index_count : 3 ,
material_index : 0 ,
lightmap_index : u8 ::MAX ,
batch_flags : 0 ,
pipeline_state : LegacyPipelineState ::default (),
terrain_base_only : false ,
alpha_test_reference : 0 ,
})
. collect ::< Vec < _ >> ();
ranges [ EMPTY_CONTROL_FX ]. index_count = 0 ;
let mut world_queue = vec! [ None ; ranges . len ()];
world_queue [ TERRAIN ] = Some ( false );
world_queue [ BUILDING ] = Some ( false );
world_queue [ CONTROL_FX ] = Some ( true );
world_queue [ EMPTY_CONTROL_FX ] = Some ( true );
let mut sort_keys = vec! [ None ; ranges . len ()];
sort_keys [ CONTROL_FX ] = Some ( 20.0 );
sort_keys [ EMPTY_CONTROL_FX ] = Some ( 30.0 );
let order = sorted_world_draw_order (
ranges . len (),
& ranges ,
& world_queue ,
& sort_keys ,
& [ NEBULA , DOME , CLOUDS ],
);
assert_eq! (
order ,
vec! [
SCREEN_GRADIENT ,
TERRAIN ,
BUILDING ,
SHADOW ,
NEBULA ,
DOME ,
CLOUDS ,
CONTROL_FX ,
EMPTY_CONTROL_FX ,
SUN ,
WEATHER ,
]
);
assert_eq! ( ranges [ EMPTY_CONTROL_FX ]. index_count , 0 );
}
2026-10-11 21:41:37 +04:00
#[test]
fn material_category_and_transparency_preserve_world_state_in_either_order () {
let original = LegacyPipelineState {
blend : LegacyBlendMode ::Opaque ,
depth : LegacyDepthMode ::TestWrite ,
cull : LegacyCullMode ::FrontFace ,
alpha_test : false ,
2026-10-31 17:16:52 +04:00
depth_bias : LegacyDepthBias ::ProjectedDecal ,
2026-10-11 21:41:37 +04:00
};
let ( base , category_first ) =
world_material_pipeline_states ( original , 1 , Some ( true )). expect ( "category 1" );
let transparency_first = apply_world_transparency ( original , true )
. with_material_category ( 1 )
. expect ( "category 1 after transparency" );
assert_eq! ( category_first , transparency_first );
assert_eq! ( base . blend , LegacyBlendMode ::SourceAlpha );
assert! ( base . alpha_test );
assert_eq! ( base . depth , LegacyDepthMode ::TestWrite );
assert_eq! ( base . cull , LegacyCullMode ::FrontFace );
2026-10-31 17:16:52 +04:00
assert_eq! ( base . depth_bias , LegacyDepthBias ::ProjectedDecal );
2026-10-11 21:41:37 +04:00
assert_eq! ( category_first . depth , LegacyDepthMode ::TestReadOnly );
assert_eq! ( category_first . cull , LegacyCullMode ::FrontFace );
2026-10-31 17:16:52 +04:00
assert_eq! ( category_first . depth_bias , LegacyDepthBias ::ProjectedDecal );
2026-10-11 21:41:37 +04:00
let fixed_state = LegacyPipelineState {
blend : LegacyBlendMode ::Additive ,
depth : LegacyDepthMode ::Disabled ,
cull : LegacyCullMode ::Disabled ,
alpha_test : false ,
2026-10-31 17:16:52 +04:00
depth_bias : LegacyDepthBias ::Disabled ,
2026-10-11 21:41:37 +04:00
};
assert_eq! (
world_material_pipeline_states ( fixed_state , 1 , None ),
Some (( fixed_state , fixed_state ))
);
assert! ( world_material_pipeline_states ( original , 5 , Some ( false )). is_none ());
assert! ( world_material_pipeline_states ( fixed_state , 5 , None ). is_none ());
}
2026-10-30 23:16:31 +04:00
#[test]
fn draw_alpha_override_is_per_range_and_none_restores_material_alpha () {
let shared_material = [ 0.25 , 0.5 ];
assert_eq! ( draw_material_alphas ( shared_material , Some ( 0.0 )), [ 0.0 , 0.5 ]);
assert_eq! ( draw_material_alphas ( shared_material , None ), shared_material );
let updated_material = [ 0.75 , 0.125 ];
assert_eq! (
draw_material_alphas ( updated_material , None ),
updated_material
);
assert_eq! (
draw_material_alphas ( updated_material , Some ( 0.0 )),
[ 0.0 , 0.125 ]
);
}
2026-06-25 07:18:31 +04:00
}