concinnity-device 0.19.119

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
// Runtime residency of the material-referenced world shader pipelines.
//
// Init builds a pipeline for every world Shader whose payload it decoded and
// leaves a `None` in `world_pipelines` for each one it deferred (a Shader owned
// by a scene other than the start scene). The streaming pump calls in here as
// those scenes pin and unpin, handing over a pipeline its worker already built.
//
// The bucket regions of the GPU-culled command buffer are issued here too: the
// cull kernel wrote every record's command into exactly one region, so each
// region is one `cmd_draw_indexed_indirect` under that bucket's pipeline.
// Mirrors `metal/world_shaders.rs` and `directx/world_shaders.rs`.

use ash::vk;
use concinnity_core::render::backend::{PipelineBuilder, PipelineSwap, PreparedPipelines};
use concinnity_core::render::backend_init::WorldShader;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::world_pipelines::{check_rebuild, replace_bucket};

use super::context::VkContext;
use super::pipeline::{
    BindlessSpv, BucketPipelineTargets, BucketPipelines, PrepassTargets, build_bucket_pipeline,
    build_bucket_prepass, build_world_prepass, build_world_shader_pipeline,
};
use crate::vulkan::pipeline_builder::{VkPipelineBuilder, world_shader_for};
use std::sync::Arc;

impl VkContext {
    // Install one shader bucket's main-pass and pre-pass pipelines: `prepared`
    // when they were built for this context's targets, else ones built here.
    // Replaces whatever the bucket currently holds, so a re-pin after an
    // eviction installs cleanly.
    pub(in crate::vulkan) fn install_world_shader(
        &mut self,
        bucket: u32,
        programs: &concinnity_core::components::ShaderPrograms,
        prepared: Option<PreparedPipelines>,
    ) -> RenderResult<()> {
        let pipelines = self.bucket_pipelines(bucket, programs, prepared, false)?;
        // The displaced pipelines drop into the device's retire queue, which
        // holds them until every frame in flight that recorded against them
        // retires.
        self.cull.world_pipelines.install(bucket, pipelines)?;
        Ok(())
    }

    // One shader bucket's pipelines to replace a bucket that has a pre-pass
    // when `live_prepass`: `prepared` when it keeps what the live bucket draws
    // with, else ones built here. Refused when the build would drop a live
    // pre-pass, so the caller keeps the live pair.
    fn bucket_pipelines(
        &self,
        bucket: u32,
        programs: &concinnity_core::components::ShaderPrograms,
        prepared: Option<PreparedPipelines>,
        live_prepass: bool,
    ) -> RenderResult<BucketPipelines> {
        self.cull.world_pipelines.slot(bucket)?;
        let targets = self.bucket_pipeline_targets().ok_or_else(|| {
            RenderError::Other("shader buckets need the bindless main pass".to_string())
        })?;
        replace_bucket(
            bucket as usize,
            live_prepass,
            world_shader_for(prepared, &self.pipeline_gate, targets),
            |p: &BucketPipelines| p.prepass.is_some(),
            || build_world_shader_pipeline(&self.hw.device, targets, bucket as usize, programs),
        )
    }

    // Rebuild one world Shader's pipelines from hot-reloaded programs, or swap
    // in `prepared` when they were built for this context's targets. Bucket 0
    // is the main pass's and the pre-pass's own pair; another bucket is rebuilt
    // only while installed. Both halves are built before the old pipelines
    // retire, so a failed build of either leaves the live ones bound.
    pub(in crate::vulkan) fn update_world_shader(
        &mut self,
        bucket: u32,
        programs: &concinnity_core::components::ShaderPrograms,
        prepared: Option<PreparedPipelines>,
    ) -> RenderResult<PipelineSwap> {
        if bucket == 0 {
            let prepared = self
                .bucket_pipeline_targets()
                .and_then(|targets| world_shader_for(prepared, &self.pipeline_gate, targets));
            self.update_default_world_shader(programs, prepared)?;
            return Ok(PipelineSwap::Swapped);
        }
        self.cull.world_pipelines.slot(bucket)?;
        if !self.cull.world_pipelines.resident(bucket as usize) {
            return Ok(PipelineSwap::NotResident);
        }
        let live_prepass = (self.cull.world_pipelines.get(bucket as usize))
            .is_some_and(|live| live.prepass.is_some());
        let pipelines = self.bucket_pipelines(bucket, programs, prepared, live_prepass)?;
        self.cull.world_pipelines.install(bucket, pipelines)?;
        Ok(PipelineSwap::Swapped)
    }

    // What every bucket's pipelines are built against here, or `None` when the
    // GPU-driven main pass is not live. The pre-pass's half is there only once
    // the world has a G-buffer.
    pub(in crate::vulkan) fn bucket_pipeline_targets(&self) -> Option<BucketPipelineTargets> {
        let layout = self.cull.bindless_pipeline_layout.as_ref()?;
        let prepass = match (self.cull.prepass_layout.as_ref(), self.gbuffer.as_ref()) {
            (Some(prepass), Some(gb)) => Some(PrepassTargets {
                render_pass: gb.prepass_render_pass.handle(),
                layout: prepass.pipeline_layout.handle(),
            }),
            _ => None,
        };
        Some(BucketPipelineTargets {
            render_pass: self.targets.main_render_pass.handle(),
            layout: layout.handle(),
            prepass,
            msaa_samples: self.targets.msaa_samples,
            swapchain_format: self.swapchain.format,
            hot_reload: self.hot_reload.enabled,
            template_generation: self.hot_reload.generation,
        })
    }

    // Build the pre-pass pipeline each bucket is missing while a G-buffer
    // exists. A pipeline that fails to build leaves its bucket without
    // G-buffer draws, never without shading. A G-buffer stays for the
    // context's life once built, and a failed rebuild empties only its
    // targets, never the render pass these pipelines build against.
    pub(in crate::vulkan) fn sync_prepass_pipelines(&mut self) {
        let Some((targets, prepass)) = self
            .bucket_pipeline_targets()
            .and_then(|targets| Some((targets, targets.prepass?)))
        else {
            return;
        };
        let device = self.hw.device.clone();
        let engine = &self.cull.bindless_main_spv;
        let build =
            |bucket: usize, programs: Option<&concinnity_core::components::ShaderPrograms>| {
                match programs {
                    Some(programs) => {
                        build_world_prepass(&device, prepass, bucket, programs, targets.hot_reload)
                    }
                    None => build_bucket_prepass(
                        &device,
                        prepass,
                        bucket,
                        (&engine.prepass_vert, &engine.prepass_frag),
                    ),
                }
            };
        if self.cull.prepass_pipeline.is_none() {
            let fresh = build(0, self.world_shader.as_ref());
            self.cull.prepass_pipeline = fresh;
        }
        let buckets: Vec<usize> = self.cull.world_pipelines.resident_buckets().collect();
        for bucket in buckets {
            let Some(pipelines) = self.cull.world_pipelines.get(bucket) else {
                continue;
            };
            if pipelines.prepass.is_some() {
                continue;
            }
            let fresh = build(bucket, pipelines.programs.as_ref());
            if let Some(pipelines) = self.cull.world_pipelines.get_mut(bucket) {
                pipelines.prepass = fresh;
            }
        }
    }

    // Every resident material bucket's pipelines rebuilt from the current
    // templates, `engine` standing in for a bucket with no Shader of its own.
    // Fails as a whole when any bucket's main pipeline fails or its rebuild
    // would drop a live pre-pass, so the caller keeps every live pair.
    pub(in crate::vulkan) fn rebuild_world_buckets(
        &self,
        engine: &BindlessSpv,
    ) -> RenderResult<Vec<(usize, BucketPipelines)>> {
        let Some(targets) = self.bucket_pipeline_targets() else {
            return Ok(Vec::new());
        };
        let mut rebuilt = Vec::new();
        for bucket in self.cull.world_pipelines.resident_buckets() {
            let Some(live) = self.cull.world_pipelines.get(bucket) else {
                continue;
            };
            let shader = WorldShader {
                programs: live.programs.as_ref(),
                deferred: false,
            };
            let pipelines =
                build_bucket_pipeline(&self.hw.device, targets, bucket, shader, engine)?;
            check_rebuild(bucket, live.prepass.is_some(), pipelines.prepass.is_some())?;
            rebuilt.push((bucket, pipelines));
        }
        Ok(rebuilt)
    }

    // A builder for this context's world Shader and volume pipelines, for a
    // streaming or hot-reload worker.
    pub(in crate::vulkan) fn pipeline_builder(&self) -> Arc<dyn PipelineBuilder> {
        Arc::new(VkPipelineBuilder {
            device: self.hw.device.clone(),
            gate: self.pipeline_gate.clone(),
            world: self.bucket_pipeline_targets(),
            volumes: self.volume_pipeline_targets(),
        })
    }

    // Release one bucket's pipelines. They drop into the device's retire queue,
    // which destroys them only once every frame in flight that recorded against
    // them has retired.
    pub(in crate::vulkan) fn evict_world_shader(&mut self, bucket: u32) {
        self.cull.world_pipelines.evict(bucket);
    }

    // Issue the bucket 1.. regions of `indirect`, each under its own material
    // shader's pipeline. Bucket 0's region is issued by the caller (it runs under
    // the pipeline the pass already bound), so this covers only the
    // material-referenced shaders. `draw_count` is the record prefix each region
    // draws, matching bucket 0's. Returns the number of indirect draws issued, and
    // leaves the last bucket's pipeline bound.
    pub(in crate::vulkan) fn draw_bucket_regions(
        &self,
        cmd: vk::CommandBuffer,
        indirect: vk::Buffer,
        draw_count: u32,
    ) -> u32 {
        self.for_each_resident_bucket(|bucket| {
            let Some(pipelines) = self.cull.world_pipelines.get(bucket) else {
                return;
            };
            // Every bucket shares the bindless layout, so the Wireframe twin
            // stands in for each one while that view mode is on.
            let pipeline = self.wireframe_or(&pipelines.main, self.wireframe.bindless.as_ref());
            // SAFETY: `cmd` is a command buffer in the recording state, and every handle and slice
            // these commands name is live for the call.
            unsafe {
                self.hw.device.cmd_bind_pipeline(
                    cmd,
                    vk::PipelineBindPoint::GRAPHICS,
                    pipeline.handle(),
                );
            }
            self.draw_bucket_region(cmd, indirect, draw_count, bucket);
        })
    }

    // Issue the bucket 1.. regions of `indirect`, each under its shader's
    // G-buffer pre-pass pipeline, so a world Shader's vertex hook places its
    // depth and motion as it places its shading. A non-resident bucket is
    // skipped here as in the main pass, so the pre-pass never lays down depth
    // and motion for geometry the color pass omits, and so is one whose
    // pre-pass pipeline failed to build. Returns the regions issued, and leaves
    // the last bucket's pipeline bound.
    pub(in crate::vulkan) fn draw_prepass_bucket_regions(
        &self,
        cmd: vk::CommandBuffer,
        indirect: vk::Buffer,
        draw_count: u32,
    ) -> u32 {
        let mut issued = 0;
        for bucket in self.cull.world_pipelines.resident_buckets() {
            let Some(prepass) = self
                .cull
                .world_pipelines
                .get(bucket)
                .and_then(|pipelines| pipelines.prepass.as_ref())
            else {
                continue;
            };
            // SAFETY: `cmd` is a command buffer in the recording state, and every handle and slice
            // these commands name is live for the call.
            unsafe {
                self.hw.device.cmd_bind_pipeline(
                    cmd,
                    vk::PipelineBindPoint::GRAPHICS,
                    prepass.handle(),
                );
            }
            self.draw_bucket_region(cmd, indirect, draw_count, bucket);
            issued += 1;
        }
        issued
    }

    // Run `f` for every bucket past the default whose Shader is resident,
    // returning how many ran. A bucket whose scene has not pinned yet has no
    // pipeline: skip it until warmup builds one rather than drawing it with the
    // wrong program.
    fn for_each_resident_bucket(&self, mut f: impl FnMut(usize)) -> u32 {
        let mut issued = 0;
        for bucket in self.cull.world_pipelines.resident_buckets() {
            f(bucket);
            issued += 1;
        }
        issued
    }

    fn draw_bucket_region(
        &self,
        cmd: vk::CommandBuffer,
        indirect: vk::Buffer,
        draw_count: u32,
        bucket: usize,
    ) {
        // SAFETY: `cmd` is a command buffer in the recording state, and every handle and slice
        // these commands name is live for the call.
        unsafe {
            self.hw.device.cmd_draw_indexed_indirect(
                cmd,
                indirect,
                self.bucket_region_offset(bucket),
                draw_count,
                super::cull::INDIRECT_COMMAND_STRIDE,
            );
        }
    }
}