use ash::vk;
use crate::vulkan::owned::{
OwnedDescriptorPool, OwnedFramebuffer, OwnedPipeline, OwnedPipelineLayout, OwnedRenderPass,
OwnedSampler, OwnedSetLayout, VkDevice,
};
use crate::gfx::backend::FrameParams;
use crate::gfx::render_types::*;
use super::allocator::PooledBuffer;
use super::draw::*;
use super::input::*;
use super::post::*;
use super::texture::*;
pub(super) const HDR_FORMAT: vk::Format = vk::Format::R16G16B16A16_SFLOAT;
pub(super) struct VkShadow {
pub(super) render_pass: OwnedRenderPass,
pub(super) map: GpuImage,
pub(super) map_size: u32,
pub(super) framebuffers: Vec<OwnedFramebuffer>,
pub(super) pipeline: Option<OwnedPipeline>,
pub(super) pipeline_layout: Option<OwnedPipelineLayout>,
pub(super) global_set_layout: Option<OwnedSetLayout>,
pub(super) global_sets: Vec<vk::DescriptorSet>,
pub(super) sampler: OwnedSampler,
pub(super) skinned_pipeline: Option<OwnedPipeline>,
pub(super) skinned_pipeline_layout: Option<OwnedPipelineLayout>,
pub(super) ubos: Vec<PooledBuffer>,
pub(super) uniforms: ShadowUniforms,
pub(super) light_dir: [f32; 3],
pub(super) update: crate::components::ShadowUpdate,
pub(super) distance: u32,
pub(super) cascades: u32,
pub(super) scheduler: crate::gfx::shadow_schedule::ShadowCascadeScheduler,
pub(super) render_mask: u32,
}
impl VkShadow {
pub(super) fn destroy(&mut self, _device: &VkDevice) {
self.map = GpuImage::null();
self.ubos.clear();
}
}
pub(super) struct VkSpotShadow {
pub(super) map: GpuImage,
pub(super) framebuffers: Vec<OwnedFramebuffer>,
pub(super) slice_size: u32,
pub(super) data_buffer: PooledBuffer,
pub(super) ubo: PooledBuffer,
pub(super) sets: Vec<vk::DescriptorSet>,
pub(super) _descriptor_pool: OwnedDescriptorPool,
pub(super) scheduler: crate::gfx::spot_shadow::SpotShadowScheduler,
pub(super) render_mask: u32,
}
impl VkSpotShadow {
pub(super) fn count(&self) -> u32 {
self.framebuffers.len() as u32
}
pub(super) fn advance(&mut self, every_frame: bool) {
let count = self.framebuffers.len();
self.render_mask = self.scheduler.next_mask(every_frame, count);
}
pub(super) fn destroy(&mut self, _device: &VkDevice) {
self.map = GpuImage::null();
self.data_buffer = PooledBuffer::null();
self.ubo = PooledBuffer::null();
}
}
pub(super) struct VkAreaLight {
pub(super) buffer: PooledBuffer,
pub(super) ltc_matrix: GpuImage,
pub(super) ltc_magnitude: GpuImage,
pub(super) sampler: OwnedSampler,
}
impl VkAreaLight {
pub(super) fn destroy(&mut self, _device: &VkDevice) {
self.ltc_matrix = GpuImage::null();
self.ltc_magnitude = GpuImage::null();
self.buffer = PooledBuffer::null();
}
}
pub(super) struct VkSkinned {
pub(super) joint_set_layout: Option<OwnedSetLayout>,
pub(super) descriptor_pool: Option<OwnedDescriptorPool>,
pub(super) vertex_buffer: PooledBuffer,
pub(super) index_buffer: PooledBuffer,
pub(super) vertex_buffer_bytes: u64,
pub(super) index_buffer_bytes: u64,
pub(super) draw_objects: Vec<SkinnedDrawObject>,
pub(super) joint_buffers: Vec<Vec<PooledBuffer>>,
pub(super) joint_sets: Vec<Vec<vk::DescriptorSet>>,
pub(super) joint_matrices: Vec<Vec<[[f32; 4]; 4]>>,
pub(super) skin: Option<super::raytrace::SkinPipeline>,
pub(super) deformed: Vec<super::raytrace::DeviceBuffer>,
pub(super) morph_delta_unique: Vec<PooledBuffer>,
pub(super) morph_delta_buffers: Vec<vk::Buffer>,
pub(super) morph_target_counts: Vec<u32>,
pub(super) morph_weights: Vec<Vec<f32>>,
pub(super) morph_weight_buffers: Vec<Vec<PooledBuffer>>,
pub(super) deformed_primed: std::sync::atomic::AtomicBool,
}
impl VkSkinned {
pub(super) fn destroy(&mut self, device: &VkDevice) {
self.vertex_buffer = PooledBuffer::null();
self.index_buffer = PooledBuffer::null();
self.joint_buffers.clear();
self.morph_delta_unique.clear();
self.morph_weight_buffers.clear();
if let Some(skin) = self.skin.take() {
skin.destroy(device);
}
self.deformed.clear();
}
}
pub(super) struct VkGeometry {
pub(super) vertex_buffer: PooledBuffer,
pub(super) index_buffer: PooledBuffer,
pub(super) mesh_vtx_alloc: crate::suballoc::range_alloc::RangeAllocator,
pub(super) mesh_idx_alloc: crate::suballoc::range_alloc::RangeAllocator,
pub(super) vertex_buffer_bytes: u64,
pub(super) index_buffer_bytes: u64,
}
impl VkGeometry {
pub(super) fn destroy(&mut self) {
self.vertex_buffer = PooledBuffer::null();
self.index_buffer = PooledBuffer::null();
}
}
pub(super) struct VkDescriptors {
pub(super) global_set_layout: OwnedSetLayout,
pub(super) global_update_after_bind: bool,
pub(super) probe_cube_count: u32,
pub(super) _text_set_layout: OwnedSetLayout,
pub(super) _descriptor_pool: OwnedDescriptorPool,
pub(super) global_sets: Vec<vk::DescriptorSet>,
pub(super) text_atlas_sets: Vec<vk::DescriptorSet>,
}
impl VkDescriptors {
pub(super) fn destroy(&self, _device: &VkDevice) {}
}
pub(super) struct VkInstanced {
pub(super) clusters: Vec<InstancedCluster>,
pub(super) any_lod: bool,
pub(super) lod_buckets: Vec<Vec<InstancedLodBucket>>,
}
pub(super) struct VkChunkStream {
pub(super) vtx_alloc: crate::suballoc::range_alloc::RangeAllocator,
pub(super) idx_alloc: crate::suballoc::range_alloc::RangeAllocator,
}
impl VkChunkStream {
pub(super) fn destroy(&self, _device: &VkDevice) {}
}
pub(super) struct VkCull {
pub(super) bindless_pipeline: Option<OwnedPipeline>,
pub(super) bindless_pipeline_layout: Option<OwnedPipelineLayout>,
pub(super) bindless_set_layout: Option<OwnedSetLayout>,
pub(super) bindless_pool_size: usize,
pub(super) bindless_update_after_bind: bool,
pub(super) world_pipelines: Vec<Option<OwnedPipeline>>,
pub(super) bucket_stride: usize,
pub(super) bindless_main_spv: (Vec<u8>, Vec<u8>),
pub(super) bindless_sets: Vec<vk::DescriptorSet>,
pub(super) object_buffers: Vec<PooledBuffer>,
pub(super) cull_pipeline: Option<OwnedPipeline>,
pub(super) cull_pipeline_layout: Option<OwnedPipelineLayout>,
pub(super) cull_set_layout: Option<OwnedSetLayout>,
pub(super) cull_sets: Vec<vk::DescriptorSet>,
pub(super) draw_args_buffers: Vec<PooledBuffer>,
pub(super) indirect_buffers: Vec<PooledBuffer>,
pub(super) cull_status_buffers: Vec<PooledBuffer>,
pub(super) occlusion_two_pass: bool,
pub(super) cull_pipeline_phase2: Option<OwnedPipeline>,
pub(super) cull_sets2: Vec<vk::DescriptorSet>,
pub(super) _two_pass_pool: Option<OwnedDescriptorPool>,
pub(super) indirect_buffers2: Vec<PooledBuffer>,
pub(super) main_render_pass_phase1: Option<OwnedRenderPass>,
pub(super) main_render_pass_phase2: Option<OwnedRenderPass>,
pub(super) hiz: Option<crate::vulkan::hiz::HiZResources>,
pub(super) hiz_valid: bool,
pub(super) hiz_prev_view_proj: [[f32; 4]; 4],
pub(super) shadow_cull_pipeline: Option<OwnedPipeline>,
pub(super) shadow_cull_pipeline_layout: Option<OwnedPipelineLayout>,
pub(super) _shadow_cull_set_layout: Option<OwnedSetLayout>,
pub(super) shadow_cull_sets: Vec<Vec<vk::DescriptorSet>>,
pub(super) shadow_bindless_pipeline: Option<OwnedPipeline>,
pub(super) shadow_bindless_pipeline_layout: Option<OwnedPipelineLayout>,
pub(super) shadow_indirect_buffers: Vec<Vec<PooledBuffer>>,
pub(super) gbuffer_bindless_pipeline: Option<OwnedPipeline>,
pub(super) gbuffer_bindless_pipeline_layout: Option<OwnedPipelineLayout>,
pub(super) _gbuffer_set_layout: Option<OwnedSetLayout>,
pub(super) gbuffer_sets: Vec<vk::DescriptorSet>,
pub(super) prev_model_buffers: Vec<PooledBuffer>,
}
impl VkCull {
pub(super) fn destroy(&mut self, device: &VkDevice) {
if let Some(hiz) = &mut self.hiz {
hiz.destroy(device);
}
self.object_buffers.clear();
self.draw_args_buffers.clear();
self.indirect_buffers.clear();
self.cull_status_buffers.clear();
self.indirect_buffers2.clear();
self.shadow_indirect_buffers.clear();
self.prev_model_buffers.clear();
}
}
pub(super) struct VkFrameSync {
pub(super) image_available: Vec<vk::Semaphore>,
pub(super) render_finished: Vec<vk::Semaphore>,
pub(super) in_flight: Vec<vk::Fence>,
}
impl VkFrameSync {
pub(super) fn destroy(&self, device: &VkDevice) {
unsafe {
for &s in &self.image_available {
device.destroy_semaphore(s, None);
}
for &s in &self.render_finished {
device.destroy_semaphore(s, None);
}
for &f in &self.in_flight {
device.destroy_fence(f, None);
}
}
}
}
pub(super) struct VkCommands {
pub(super) command_pool: vk::CommandPool,
pub(super) command_buffers: Vec<vk::CommandBuffer>,
pub(super) start_command_pools: Vec<vk::CommandPool>,
pub(super) start_command_buffers: Vec<vk::CommandBuffer>,
pub(super) pass_command_pools: Vec<vk::CommandPool>,
pub(super) pass_command_buffers: Vec<vk::CommandBuffer>,
}
impl VkCommands {
pub(super) fn destroy(&self, device: &VkDevice) {
unsafe {
device.destroy_command_pool(self.command_pool, None);
for &pool in self
.start_command_pools
.iter()
.chain(self.pass_command_pools.iter())
{
device.destroy_command_pool(pool, None);
}
}
}
}
pub(super) struct VkUniforms {
pub(super) view_ubo_buffers: Vec<PooledBuffer>,
pub(super) probe_set_ubo_buffers: Vec<PooledBuffer>,
pub(super) light_ubo_buffers: Vec<PooledBuffer>,
pub(super) light_dirty: concinnity_core::render::frame_dirty::FrameDirty,
pub(super) local_light_buffer: PooledBuffer,
pub(super) local_light_size: u64,
pub(super) light_uniforms: crate::gfx::render_types::LightUniforms,
}
impl VkUniforms {
pub(super) fn destroy(&mut self) {
self.view_ubo_buffers.clear();
self.probe_set_ubo_buffers.clear();
self.light_ubo_buffers.clear();
self.local_light_buffer = PooledBuffer::null();
}
}
pub(super) struct DecalState {
pub resources: Option<crate::vulkan::decal::DecalResources>,
pub records: Vec<Option<crate::gfx::decal::DecalRecord>>,
pub free_slots: Vec<usize>,
}
pub(super) struct FogState {
pub resources: Option<crate::vulkan::fog::FogResources>,
pub settings: Option<crate::gfx::volumetric_fog::FogSettings>,
pub sun_dir: [f32; 3],
pub sun_color: [f32; 3],
}
pub(super) struct AutoExposureState {
pub resources: Option<crate::vulkan::auto_exposure::AutoExposureResources>,
pub settings: Option<crate::gfx::auto_exposure::AutoExposureSettings>,
pub state: Option<crate::gfx::auto_exposure::AutoExposureState>,
pub bias_ev: f32,
pub last_elapsed: f32,
}
pub(super) struct HotReloadState {
pub enabled: bool,
pub reload_pending: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
#[expect(
dead_code,
reason = "held so the watcher thread stays alive; dropping it stops the watcher"
)]
pub watcher: Option<crate::vulkan::hot_reload::WatcherHandle>,
}
pub(super) struct ParticleState {
pub resources: Option<crate::vulkan::particle::ParticleResources>,
pub records: Vec<Option<crate::gfx::particles::ParticleEmitterRecord>>,
pub emitter_state: Vec<Option<crate::vulkan::particle::ParticleEmitterGpuState>>,
pub free_slots: Vec<usize>,
pub last_elapsed: std::cell::Cell<f32>,
pub frame_index: std::cell::Cell<u32>,
}
pub(super) struct CompositeState {
pub render_pass: OwnedRenderPass,
pub framebuffers: Vec<OwnedFramebuffer>,
pub pipeline: OwnedPipeline,
pub pipeline_layout: OwnedPipelineLayout,
pub _set_layout: OwnedSetLayout,
pub sets: Vec<vk::DescriptorSet>,
pub sampler: OwnedSampler,
}
pub(super) struct BloomState {
pub write_pass: OwnedRenderPass,
pub blend_pass: OwnedRenderPass,
pub pipeline_prefilter: OwnedPipeline,
pub pipeline_downsample: OwnedPipeline,
pub pipeline_upsample: OwnedPipeline,
pub pipeline_layout: OwnedPipelineLayout,
pub set_layout: OwnedSetLayout,
pub descriptor_pool: OwnedDescriptorPool,
pub mips: Vec<Vec<GpuImage>>,
pub mip_extents: Vec<vk::Extent2D>,
pub write_framebuffers: Vec<Vec<OwnedFramebuffer>>,
pub blend_framebuffers: Vec<Vec<OwnedFramebuffer>>,
pub input_sets: Vec<Vec<vk::DescriptorSet>>,
}
pub(super) struct TextState {
pub atlas_textures: Vec<GpuImage>,
pub pipeline: Option<OwnedPipeline>,
pub pipeline_layout: OwnedPipelineLayout,
pub _sampler: OwnedSampler,
pub upload: super::upload_ring::UploadRing,
}
pub(super) struct DrawState {
pub objects: Vec<DrawObject>,
pub graph_cache: Option<(
crate::gfx::render_graph::FrameGraphInputs,
crate::gfx::render_graph::CompiledGraph,
)>,
pub n_objects: usize,
pub n_instances: usize,
pub n_runtime: usize,
pub n_skinned: usize,
}
pub(super) struct ViewState {
pub clear_color: [f32; 4],
pub scene_fade: f32,
pub mode: concinnity_core::gfx::view_modes::ViewMode,
pub show: concinnity_core::gfx::view_modes::ShowFlags,
pub far: f32,
pub matrix: [[f32; 4]; 4],
pub sky_rot: [[f32; 4]; 3],
}
pub(super) struct ProbeState {
pub placements: Vec<crate::gfx::reflection_probe::ProbePlacement>,
pub set: concinnity_core::render::uniforms::ProbeSet,
pub maps: Vec<GpuImage>,
pub bake_queue: crate::gfx::reflection_probe::ProbeBakeQueue,
pub rendering: Option<super::probe::RenderingBake>,
pub prefiltering: Option<super::probe::PrefilteringBake>,
pub prefilter: Option<super::probe_prefilter::ProbePrefilterPipelines>,
}
pub(super) struct StreamState {
pub pool_rewrites: crate::gfx::slot_rewrites::SlotRewriteQueue,
pub frame: u64,
pub retires: Vec<StreamedUploadRetire>,
}
pub(super) struct SwapchainState {
pub loader: ash::khr::swapchain::Device,
pub handle: vk::SwapchainKHR,
pub images: Vec<vk::Image>,
pub image_views: Vec<vk::ImageView>,
pub format: vk::Format,
pub extent: vk::Extent2D,
pub last_present_index: Option<u32>,
}
pub(crate) struct VkContext {
pub(super) instance: ash::Instance,
pub(super) device: super::owned::VkDevice,
pub(super) physical_device: vk::PhysicalDevice,
pub(super) surface: vk::SurfaceKHR,
pub(super) surface_loader: ash::khr::surface::Instance,
pub(super) graphics_queue: vk::Queue,
pub(super) present_queue: vk::Queue,
pub(super) graphics_family: u32,
pub(super) alloc: super::allocator::DeviceAllocator,
pub(super) swapchain: SwapchainState,
pub(super) render_extent: vk::Extent2D,
pub(super) main_render_pass: OwnedRenderPass,
pub(super) composite: CompositeState,
pub(super) msaa_samples: vk::SampleCountFlags,
pub(super) color_images: Vec<GpuImage>, pub(super) depth_images: Vec<GpuImage>, pub(super) hdr_resolve_images: Vec<GpuImage>,
pub(super) framebuffers: Vec<OwnedFramebuffer>,
pub(super) shadow: VkShadow,
pub(super) spot_shadow: VkSpotShadow,
pub(super) area_light: VkAreaLight,
pub(super) textures: Vec<GpuImage>,
pub(super) fallback_textures: Vec<GpuImage>,
pub(super) linear_sampler: OwnedSampler,
pub(super) cull: VkCull,
pub(super) light_cull: super::light_cull::VkLightCull,
pub(super) text: TextState,
pub(super) color_lut: GpuImage,
pub(super) bloom: BloomState,
pub(super) post_process: crate::gfx::render_types::PostProcessParams,
pub(super) taa: Option<TaaResources>,
pub(super) upscale: Option<Box<dyn VkUpscaleBackend>>,
pub(super) upscale_requested: crate::components::UpscalerBackend,
pub(super) ssao: Option<SsaoResources>,
pub(super) ssao_white: GpuImage,
pub(super) transient_pool: super::transient_pool::TransientImagePool,
pub(super) ssr: Option<SsrResources>,
pub(super) ssr_resolve_active: bool,
pub(super) reflection_composite: Option<ReflectionCompositeResources>,
pub(super) ssgi: Option<SsgiResources>,
pub(super) gbuffer: Option<GbufferResources>,
pub(super) rt_reflections: Option<RtReflectionsResources>,
pub(super) rt_accel: Option<crate::vulkan::raytrace::RtAccelData>,
pub(super) rt_dynamic_mode: crate::vulkan::raytrace::RtDynamicMode,
pub(super) rt_skinned_geometry: bool,
pub(super) rt_topology_dirty: bool,
pub(super) rt_capable: bool,
pub(super) update_after_bind: bool,
pub(super) rt_static_vertex_count: usize,
pub(super) decal: DecalState,
pub(super) lines: crate::vulkan::line::LineState,
pub(super) fog: FogState,
pub(super) raymarch: Option<crate::vulkan::raymarch::RaymarchResources>,
pub(super) transparent: Option<crate::vulkan::transparent::TransparentResources>,
pub(super) planar_reflection: Option<crate::vulkan::planar::PlanarReflectionSet>,
pub(super) hdr_mode: crate::gfx::hdr_output::HdrOutputMode,
pub(super) particle: ParticleState,
pub(super) auto_exposure: AutoExposureState,
pub(super) hot_reload: HotReloadState,
pub(super) world_shader: Option<concinnity_core::components::ShaderPrograms>,
pub(super) frame_stats: std::cell::Cell<crate::gfx::profile::RenderStats>,
pub(super) draw_calls_accum: std::sync::atomic::AtomicU32,
pub(super) timestamp_query_pool: Option<vk::QueryPool>,
pub(super) timestamp_period_ns: f32,
pub(super) device_local_heaps: Vec<u32>,
pub(super) memory_budget_supported: bool,
pub(super) descriptors: VkDescriptors,
pub(super) instanced: VkInstanced,
pub(super) geometry: VkGeometry,
pub(super) chunk_stream: VkChunkStream,
pub(super) skinned: VkSkinned,
pub(super) uniforms: VkUniforms,
pub(super) frame_sync: VkFrameSync,
pub(super) current_frame: usize,
pub(super) frames_in_flight: usize,
pub(super) vsync: bool,
pub(super) commands: VkCommands,
pub(super) draw: DrawState,
pub(super) view: ViewState,
pub(super) wireframe: super::wireframe::VkWireframe,
pub(super) prefilter_mip_count: u32,
pub(super) cube_sampler: OwnedSampler,
pub(super) env_map: EnvironmentMapTextures,
pub(super) probe: ProbeState,
pub(super) stream: StreamState,
pub(super) window: Option<super::PlatformWindow>,
pub(super) _entry: ash::Entry,
pub(super) swapchain_config: crate::gfx::backend_init::SwapchainConfig,
pub(super) reused_by_successor: bool,
pub(super) world_content_destroyed: bool,
}
unsafe impl Send for VkContext {}
static MAIN_THREAD_ID: std::sync::OnceLock<std::thread::ThreadId> = std::sync::OnceLock::new();
pub(super) fn record_main_thread() {
let _ = MAIN_THREAD_ID.set(std::thread::current().id());
}
#[inline]
#[track_caller]
pub(super) fn debug_assert_main_thread(entry: &str) {
debug_assert!(
MAIN_THREAD_ID
.get()
.is_none_or(|main| *main == std::thread::current().id()),
"{entry} must be called from the main thread: VkContext is main-thread-only \
(see `unsafe impl Send for VkContext`); driving GraphicsSystem off the main \
thread races the GLFW window + Vulkan queue submission",
);
}
impl VkContext {
pub(crate) fn draw_frame(
&mut self,
params: FrameParams<'_>,
) -> crate::gfx::error::RenderResult<()> {
let FrameParams {
elapsed,
fov_y_radians,
near,
far,
cam_pos,
text_calls,
lines,
world_hidden,
view_mode,
show,
sky_rot,
} = params;
self.view.mode = view_mode;
self.view.show = show;
self.view.far = far;
self.view.sky_rot = sky_rot;
self.ensure_wireframe_pipelines();
if self.shader_reload_requested() {
self.clear_shader_reload_flag();
self.wait_idle();
match self.reload_shaders() {
Ok(()) => tracing::info!("hot-reload: shader pipelines rebuilt"),
Err(e) => tracing::error!("hot-reload: shader rebuild failed: {}", e),
}
}
if self.frame_is_parked() {
return Ok(());
}
let frame = self.current_frame;
let device = self.device.clone();
let device = &device;
unsafe {
device
.wait_for_fences(
std::slice::from_ref(&self.frame_sync.in_flight[frame]),
true,
u64::MAX,
)
.map_err(|e| super::error::map_vk_result(e, "wait fences"))?;
}
self.apply_streamed_texture_rewrites(frame);
self.alloc.begin_frame();
self.device.begin_frame();
if self.stream.frame.is_multiple_of(1024) && tracing::enabled!(tracing::Level::DEBUG) {
tracing::debug!("device allocator: {}", self.alloc.stats());
}
if let Err(e) = self.bake_pending_probes() {
tracing::warn!("reflection probe bake step failed: {e}");
}
let instanced_total: usize = self
.instanced
.clusters
.iter()
.map(|c| c.instances.len())
.sum();
let objects =
(self.draw.objects.len() + instanced_total + self.skinned.draw_objects.len()) as u32;
let skinned_visible = self
.skinned
.draw_objects
.iter()
.filter(|o| o.visible)
.count() as u32;
let skinned_pool_free = 0u32;
let empty_pass_times = [("", 0u32); crate::gfx::profile::MAX_PASS_TIMINGS];
let (gpu_frame_us, pass_times_us) = if let Some(pool) = self.timestamp_query_pool {
let mut results = vec![[0u64; 2]; super::pass_timing::SLOTS_PER_FRAME];
let res = unsafe {
device.get_query_pool_results(
pool,
super::pass_timing::frame_block_base(frame),
&mut results,
vk::QueryResultFlags::TYPE_64 | vk::QueryResultFlags::WITH_AVAILABILITY,
)
};
if matches!(res, Ok(()) | Err(vk::Result::NOT_READY)) {
let period = self.timestamp_period_ns;
let pair_micros = |start_slot: usize, end_slot: usize| -> u32 {
let [s_val, s_avail] = results[start_slot];
let [e_val, e_avail] = results[end_slot];
if s_avail != 0 && e_avail != 0 && e_val > s_val && period > 0.0 {
let nanos = (e_val - s_val) as f64 * period as f64;
((nanos / 1000.0) as u64).min(u32::MAX as u64) as u32
} else {
0
}
};
let frame_us = pair_micros(0, 1);
let mut times = empty_pass_times;
for (i, name) in crate::gfx::render_graph::PASS_NAMES.iter().enumerate() {
if i >= crate::gfx::profile::MAX_PASS_TIMINGS {
break;
}
times[i] = (*name, pair_micros(2 + 2 * i, 3 + 2 * i));
}
(frame_us, times)
} else {
(0, empty_pass_times)
}
} else {
(0, empty_pass_times)
};
let vram_bytes = self.query_vram_bytes();
let transient_pool_bytes = self.transient_pool.allocated_bytes();
self.draw_calls_accum
.store(0, std::sync::atomic::Ordering::Relaxed);
self.frame_stats.set(crate::gfx::profile::RenderStats {
draw_calls: 0,
objects,
skinned_visible,
skinned_pool_free,
gpu_frame_us,
vram_bytes,
transient_pool_bytes,
pass_times_us,
auto_exposure_ev: self.auto_exposure.state.as_ref().map(|s| s.current_ev),
max_edr: match self.hdr_mode {
crate::gfx::hdr_output::HdrOutputMode::Hdr { max_edr, .. } => Some(max_edr),
crate::gfx::hdr_output::HdrOutputMode::Sdr => None,
},
});
let acquire = unsafe {
self.swapchain.loader.acquire_next_image(
self.swapchain.handle,
u64::MAX,
self.frame_sync.image_available[frame],
vk::Fence::null(),
)
};
let image_index = match acquire {
Ok((idx, suboptimal)) => {
if suboptimal {
self.rebuild_swapchain()?;
return Ok(());
}
idx
}
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
self.rebuild_swapchain()?;
return Ok(());
}
Err(e) => return Err(super::error::map_vk_result(e, "acquire swapchain image")),
};
unsafe { device.reset_fences(std::slice::from_ref(&self.frame_sync.in_flight[frame])) }
.map_err(|e| format!("reset fences: {e}"))?;
let cmd = self.commands.command_buffers[frame];
unsafe {
device
.reset_command_buffer(cmd, vk::CommandBufferResetFlags::empty())
.map_err(|e| format!("reset cmd buf: {e}"))?;
device
.begin_command_buffer(
cmd,
&vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
)
.map_err(|e| format!("begin cmd buf: {e}"))?;
}
let mut submit_bufs = self.record_frame(
RecordFrameTargets {
cmd,
image_index,
frame_idx: frame,
},
RecordFrameView {
elapsed,
fov_y_radians,
near,
far,
cam_pos,
text_calls,
lines,
},
world_hidden,
)?;
unsafe { device.end_command_buffer(cmd) }.map_err(|e| format!("end cmd buf: {e}"))?;
submit_bufs.push(cmd);
let wait_sems = [self.frame_sync.image_available[frame]];
let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
let signal_sems = [self.frame_sync.render_finished[image_index as usize]];
let submit_info = vk::SubmitInfo::default()
.wait_semaphores(&wait_sems)
.wait_dst_stage_mask(&wait_stages)
.command_buffers(&submit_bufs)
.signal_semaphores(&signal_sems);
unsafe {
device
.queue_submit(
self.graphics_queue,
std::slice::from_ref(&submit_info),
self.frame_sync.in_flight[frame],
)
.map_err(|e| super::error::map_vk_result(e, "queue submit"))?;
}
let swapchains = [self.swapchain.handle];
let image_indices = [image_index];
let present_info = vk::PresentInfoKHR::default()
.wait_semaphores(&signal_sems)
.swapchains(&swapchains)
.image_indices(&image_indices);
let present_result = unsafe {
self.swapchain
.loader
.queue_present(self.present_queue, &present_info)
};
if present_result == Err(vk::Result::ERROR_OUT_OF_DATE_KHR) || present_result == Ok(true) {
self.rebuild_swapchain()?;
} else {
present_result.map_err(|e| super::error::map_vk_result(e, "present"))?;
self.swapchain.last_present_index = Some(image_index);
}
self.current_frame = (self.current_frame + 1) % self.frames_in_flight;
Ok(())
}
pub(crate) fn update_view(&mut self, matrix: [[f32; 4]; 4]) {
self.view.matrix = matrix;
}
pub(crate) fn update_models(&mut self, updates: &[(u32, [[f32; 4]; 4])]) {
for &(index, model) in updates {
if let Some(obj) = self.draw.objects.get_mut(index as usize) {
obj.model = model;
}
}
}
pub(crate) fn update_visibility(&mut self, index: usize, visible: bool) {
if let Some(obj) = self.draw.objects.get_mut(index) {
obj.visible = visible;
}
}
pub(crate) fn retire_draw_object(&mut self, index: usize) {
if let Some(obj) = self.draw.objects.get_mut(index) {
obj.visible = false;
obj.resident = false;
}
}
pub(super) fn shade_mode(&self) -> f32 {
if self.view.mode == concinnity_core::gfx::view_modes::ViewMode::Unlit {
1.0
} else {
0.0
}
}
pub(crate) fn set_fade(&mut self, fade: f32) {
self.view.scene_fade = fade.clamp(0.0, 1.0);
}
#[inline]
pub(super) fn window(&self) -> &super::PlatformWindow {
self.window
.as_ref()
.expect("VkContext window taken by reload_world")
}
#[inline]
pub(super) fn window_mut(&mut self) -> &mut super::PlatformWindow {
self.window
.as_mut()
.expect("VkContext window taken by reload_world")
}
#[inline]
pub(super) fn is_minimized(&self) -> bool {
let (w, h) = self.window().framebuffer_size();
extent_minimized(w, h)
}
pub(super) fn frame_is_parked(&self) -> bool {
if self.is_minimized() {
return true;
}
match self.surface_extent() {
Ok(extent) => !super::swapchain::extent_is_presentable(extent),
Err(_) => false,
}
}
pub(crate) fn window_closed(&mut self) -> bool {
self.window_mut().poll()
}
pub(crate) fn wait_idle(&self) {
let _ = unsafe { self.device.device_wait_idle() };
}
pub(crate) fn render_stats(&self) -> crate::gfx::profile::RenderStats {
self.frame_stats.get()
}
pub(super) fn query_vram_bytes(&self) -> u64 {
if !self.memory_budget_supported || self.device_local_heaps.is_empty() {
return 0;
}
let mut budget = vk::PhysicalDeviceMemoryBudgetPropertiesEXT::default();
let mut props2 = vk::PhysicalDeviceMemoryProperties2::default().push_next(&mut budget);
unsafe {
self.instance
.get_physical_device_memory_properties2(self.physical_device, &mut props2);
}
self.device_local_heaps
.iter()
.map(|&i| budget.heap_usage[i as usize])
.sum()
}
pub(super) fn inc_draw_calls(&self, n: u32) {
self.draw_calls_accum
.fetch_add(n, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn capture_cursor(&mut self) {
self.window_mut().capture_cursor();
}
pub(crate) fn set_ui_cursor_hidden(&mut self, hidden: bool) {
self.window_mut().set_ui_cursor_hidden(hidden);
}
pub(crate) fn cursor_outside_window(&self) -> bool {
self.window().cursor_outside_window()
}
pub(crate) fn set_menu_mode(&mut self, on: bool) {
self.window_mut().set_menu_mode(on);
}
pub(crate) fn set_camera_capture(&mut self, capture: bool) {
self.window_mut().set_camera_capture(capture);
}
pub(crate) fn set_vsync(&mut self, on: bool) {
if on == self.vsync {
return;
}
self.vsync = on;
if let Err(e) = self.rebuild_swapchain() {
tracing::warn!("set_vsync: rebuild_swapchain failed: {}", e);
}
}
pub(crate) fn set_window_mode(&mut self, mode: crate::components::WindowMode) {
self.window_mut().set_window_mode(mode);
}
pub(crate) fn set_window_size(&mut self, width: u32, height: u32) {
self.window_mut().set_window_size(width, height);
}
pub(crate) fn display_modes(&self) -> Vec<crate::gfx::display_mode::DisplayMode> {
self.window().display_modes()
}
pub(crate) fn current_display_mode(&self) -> Option<crate::gfx::display_mode::DisplayMode> {
self.window().current_display_mode()
}
pub(crate) fn set_display_mode(&mut self, mode: crate::gfx::display_mode::DisplayMode) {
self.window_mut().set_display_mode(mode);
}
pub(crate) fn update_post_process(
&mut self,
tunables: crate::gfx::render_types::PostProcessTunables,
) {
self.post_process.set_tunables(tunables);
}
pub(crate) fn set_ambient_intensity(&mut self, value: f32) {
if self.uniforms.light_uniforms.ambient_intensity == value {
return;
}
self.uniforms.light_uniforms.ambient_intensity = value;
self.uniforms.light_dirty.mark_all();
}
pub(crate) fn update_directional_lights(
&mut self,
lights: &[crate::components::DirectionalLight],
) {
let (directional, num_directional) = crate::gfx::lights::directional_light_data(lights);
let uniforms = &mut self.uniforms.light_uniforms;
if uniforms.directional == directional && uniforms.num_directional == num_directional {
return;
}
uniforms.directional = directional;
uniforms.num_directional = num_directional;
self.shadow.light_dir = crate::gfx::lights::sun_direction(&self.uniforms.light_uniforms);
self.fog.sun_dir = self.shadow.light_dir;
self.fog.sun_color = crate::gfx::lights::sun_color(&self.uniforms.light_uniforms);
self.uniforms.light_dirty.mark_all();
}
pub(crate) fn set_shadow_update(&mut self, update: crate::components::ShadowUpdate) {
self.shadow.update = update;
}
pub(crate) fn set_shadow_distance(&mut self, distance: u32) {
self.shadow.distance = distance;
}
pub(crate) fn set_shadow_cascades(&mut self, count: u32) {
self.shadow.cascades = count;
}
pub(crate) fn update_quality_params(&mut self, q: crate::gfx::backend::QualitySettings) {
if let (Some(live), Some(cur)) = (q.ssao, self.ssao.as_mut().map(|s| &mut s.settings)) {
*cur = live;
}
if let (Some(live), Some(cur)) = (q.ssr, self.ssr.as_mut().map(|s| &mut s.settings)) {
*cur = live;
}
if let (Some(live), Some(cur)) = (q.ssgi, self.ssgi.as_mut().map(|s| &mut s.settings)) {
cur.intensity = live.intensity;
cur.max_distance = live.max_distance;
}
if let (Some(live), Some(cur)) = (q.auto_exposure, self.auto_exposure.settings.as_mut()) {
*cur = live;
}
}
pub(crate) fn shader_reload_pending(
&self,
) -> Option<std::sync::Arc<std::sync::atomic::AtomicBool>> {
self.hot_reload
.reload_pending
.as_ref()
.map(std::sync::Arc::clone)
}
pub(crate) fn take_input(&mut self) -> InputState {
self.window_mut().take_input()
}
pub(crate) fn set_keymap(&mut self, keymap: &crate::gfx::keymap::KeyMap) {
self.window_mut().set_keymap(keymap);
}
pub(crate) fn logical_size(&self) -> (f32, f32) {
self.window().logical_size()
}
pub(crate) fn top_content_inset(&self) -> f32 {
self.window().top_content_inset()
}
pub(crate) fn capabilities(&self) -> crate::gfx::backend::DeviceCapabilities {
crate::gfx::backend::DeviceCapabilities {
ray_tracing: self.rt_capable,
selectable_upscaler: true,
reuses_build_slots: false,
rewrites_draws: false,
}
}
pub(crate) fn gpu_profile(&self) -> crate::gfx::backend::GpuProfile {
use crate::gfx::backend::{
GpuClassInput, GpuProfile, GpuVendor, apple_family_from_device_name, classify_tier,
};
let props = unsafe {
self.instance
.get_physical_device_properties(self.physical_device)
};
let vendor = match props.vendor_id {
0x10DE => GpuVendor::Nvidia,
0x1002 => GpuVendor::Amd,
0x8086 => GpuVendor::Intel,
0x106B => GpuVendor::Apple, _ => GpuVendor::Other,
};
let discrete = props.device_type == vk::PhysicalDeviceType::DISCRETE_GPU;
let unified = props.device_type == vk::PhysicalDeviceType::INTEGRATED_GPU;
let mem = unsafe {
self.instance
.get_physical_device_memory_properties(self.physical_device)
};
let budget: u64 = (0..mem.memory_heap_count as usize)
.filter(|&i| {
mem.memory_heaps[i]
.flags
.contains(vk::MemoryHeapFlags::DEVICE_LOCAL)
})
.map(|i| mem.memory_heaps[i].size)
.sum();
let tier = classify_tier(&GpuClassInput {
vendor,
memory_budget_bytes: budget,
discrete,
apple_family: apple_family_from_device_name(&super::gpu_profile::device_name(&props)),
});
GpuProfile {
vendor,
tier,
memory_budget_bytes: budget,
unified_memory: unified,
discrete,
}
}
}
impl crate::gfx::scene_flow::SceneControl for VkContext {
fn update_visibility(&mut self, draw_idx: usize, visible: bool) {
self.update_visibility(draw_idx, visible);
}
fn set_fade(&mut self, fade: f32) {
self.set_fade(fade);
}
}
impl VkContext {
pub(super) fn destroy_world_content(&mut self) {
if self.world_content_destroyed {
return;
}
self.world_content_destroyed = true;
let device = self.device.clone();
let device = &device;
if let Some(rendering) = self.probe.rendering.take() {
rendering.destroy(device, self.commands.command_pool);
}
if let Some(prefiltering) = self.probe.prefiltering.take() {
prefiltering.destroy(device, self.commands.command_pool);
}
self.drain_stream_retires();
self.frame_sync.destroy(device);
self.commands.destroy(device);
self.destroy_swapchain_resources();
self.shadow.destroy(device);
self.spot_shadow.destroy(device);
self.area_light.destroy(device);
self.env_map.irradiance = GpuImage::null();
self.env_map.prefilter = GpuImage::null();
self.wireframe.destroy();
self.text.pipeline = None;
self.cull.destroy(device);
self.light_cull.destroy(device);
self.color_lut = GpuImage::null();
if let Some(mut taa) = self.taa.take() {
taa.destroy(device);
}
if let Some(mut ssao) = self.ssao.take() {
ssao.destroy(device);
}
self.ssao_white = GpuImage::null();
self.transient_pool.destroy(device);
if let Some(mut ssr) = self.ssr.take() {
ssr.destroy(device);
}
if let Some(mut rc) = self.reflection_composite.take() {
rc.destroy(device);
}
if let Some(mut ssgi) = self.ssgi.take() {
ssgi.destroy(device);
}
if let Some(mut gb) = self.gbuffer.take() {
gb.destroy(device);
}
if let Some(mut rt) = self.rt_reflections.take() {
rt.destroy(device);
}
if let Some(mut accel) = self.rt_accel.take() {
accel.destroy(device);
}
if let Some(mut up) = self.upscale.take() {
up.destroy(device);
}
if let Some(mut decals) = self.decal.resources.take() {
decals.destroy(device);
}
if let Some(mut lines) = self.lines.resources.take() {
lines.destroy(device);
}
self.text.upload.destroy();
if let Some(mut fog) = self.fog.resources.take() {
fog.destroy(device);
}
if let Some(mut rm) = self.raymarch.take() {
rm.destroy(device);
}
if let Some(mut planar) = self.planar_reflection.take() {
planar.destroy(device);
}
if let Some(mut transparent) = self.transparent.take() {
transparent.destroy(device);
}
if let Some(mut ae) = self.auto_exposure.resources.take() {
ae.destroy(device);
}
self.destroy_particle_emitter_states(device);
if let Some(mut p) = self.particle.resources.take() {
p.destroy(device);
}
if let Some(pool) = self.timestamp_query_pool.take() {
unsafe { device.destroy_query_pool(pool, None) };
}
self.chunk_stream.destroy(device);
self.skinned.destroy(device);
self.descriptors.destroy(device);
self.geometry.destroy();
self.uniforms.destroy();
self.textures.clear();
self.fallback_textures.clear();
self.text.atlas_textures.clear();
self.probe.maps.clear();
}
}
impl Drop for VkContext {
fn drop(&mut self) {
self.wait_idle();
self.destroy_world_content();
if !self.reused_by_successor {
self.alloc.destroy();
}
if !self.reused_by_successor {
unsafe { self.surface_loader.destroy_surface(self.surface, None) };
}
}
}
fn extent_minimized(width: i32, height: i32) -> bool {
width <= 0 || height <= 0
}
#[cfg(test)]
mod tests {
use super::extent_minimized;
#[test]
fn extent_minimized_gates_on_zero_or_negative_dimensions() {
assert!(!extent_minimized(1280, 720));
assert!(!extent_minimized(1, 1));
assert!(extent_minimized(0, 0));
assert!(extent_minimized(1280, 0));
assert!(extent_minimized(0, 720));
assert!(extent_minimized(-1, 720));
assert!(extent_minimized(1280, -1));
}
}