concinnity-device 0.19.119

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
//! Texture-pool slot updates for DxContext: streamed and hot-reloaded albedo /
//! normal-map slots, the IBL environment map and the color-grading LUT, plus
//! the runtime clone of a static draw object (which reuses the source's
//! descriptors).

use concinnity_core::bake;
use concinnity_core::render::error::{RenderError, RenderResult};
use windows::Win32::Graphics::Direct3D12::*;

use super::super::context::*;
use super::super::texture::*;

impl DxContext {
    // CPU descriptor handle for CBV/SRV/UAV heap `slot`.
    fn srv_slot_cpu(&self, slot: usize) -> D3D12_CPU_DESCRIPTOR_HANDLE {
        // SAFETY: a property query on a live descriptor heap; it only reads.
        let base = unsafe {
            self.descriptors
                .srv_heap
                .GetCPUDescriptorHandleForHeapStart()
        };
        D3D12_CPU_DESCRIPTOR_HANDLE {
            ptr: base.ptr + slot * self.descriptors.srv_descriptor_size,
        }
    }

    // Re-point every per-frame flat-pool copy that samples texture-pool `slot`.
    // The swapped resource has exactly one descriptor per frame copy (index ==
    // its handle), shared by albedo + normal sampling and by the RT hit shader,
    // so one re-point per copy refreshes every consumer at once.
    fn rewrite_bound_texture_srvs(&self, slot: usize) {
        let resource = &self.scene.textures[slot];
        for f in 0..FRAMES {
            write_texture_srv(
                &self.hw.device,
                resource,
                self.srv_slot_cpu(self.flat_pool_slot(f, slot)),
            );
        }
    }

    // Heap slot of pool index `slot` in frame `frame`'s flat-pool copy.
    fn flat_pool_slot(&self, frame: usize, slot: usize) -> usize {
        self.descriptors.layout.flat_pool_base_slot + frame * self.descriptors.flat_pool_len + slot
    }

    // Re-point every SRV that samples texture-pool `slot`. Only legal under a
    // device drain.
    fn rewrite_texture_slot(&self, slot: usize) {
        self.rewrite_bound_texture_srvs(slot);
    }

    // Whether replacing pool `slot` must drain the device first: true when an
    // SRV that samples the slot may be dereferenced by pending command lists
    // AND cannot wait for the per-frame propagation. The flat-pool copies
    // propagate per frame, so only a world with nothing to GPU-drive (which
    // draws nothing) answers yes.
    fn streamed_slot_needs_drain(&self) -> bool {
        !(self.cull.main_bindless_pso.is_some() && self.cull_count() > 0)
    }

    // Replace texture-pool `slot` with a freshly decoded texture.
    //
    // The asset-streaming subsystem calls this to bring a texture resident
    // after init. Like Vulkan -- and unlike Metal, whose bind paths re-read
    // the texture pool every frame -- the D3D12 per-object / per-cluster SRVs
    // are baked into the descriptor heap at init, so a streamed swap must
    // rewrite every heap slot that samples this pool index (as an albedo or a
    // normal map). The streaming fast path never stalls the device: the
    // upload is submitted without waiting (the in-order queue executes it
    // before any later frame's lists), the build-time pairs are re-pointed
    // immediately (undereferenced while the bindless pass drives every draw),
    // the per-frame flat-pool copies re-point one per frame as their fences
    // retire, and the old resource plus upload transients are parked on
    // `stream.retires` until every consumer provably moved off them. When a
    // pending-referenced SRV samples the slot (see `streamed_slot_needs_drain`)
    // the swap instead drains the device and rewrites everything in place,
    // matching the hot-reload paths below.
    pub(crate) fn update_texture_slot(
        &mut self,
        slot: usize,
        image: &bake::texture::TextureImage,
    ) -> RenderResult<()> {
        if slot >= self.scene.textures.len() {
            return Err(RenderError::Other(format!(
                "update_texture_slot: slot {} out of range (pool size {})",
                slot,
                self.scene.textures.len()
            )));
        }
        if self.streamed_slot_needs_drain() {
            self.wait_idle();
            let texture = upload_texture_image(&self.hw.alloc, image)?;
            self.scene.textures[slot] = texture;
            self.rewrite_texture_slot(slot);
            // The full rewrite covered every flat-pool copy, so any propagation
            // queued for this slot is already satisfied.
            self.stream.pool_rewrites.remove(slot);
            return Ok(());
        }
        let (texture, in_flight) = upload_texture_image_deferred(&self.hw.alloc, image)?;
        let old = std::mem::replace(&mut self.scene.textures[slot], texture);
        self.stream.pool_rewrites.queue(slot);
        self.stream.retires.push(
            self.stream.frame,
            super::super::texture::StreamedUploadRetire {
                texture: old,
                upload: in_flight.upload,
                allocator: in_flight.allocator,
                cmd: in_flight.cmd,
            },
        );
        Ok(())
    }

    // Per-frame streamed-texture upkeep, called at the top of `draw_frame`
    // right after frame slot `frame`'s fence wait: re-point this frame's
    // flat-pool copy at any swapped slots (legal now -- the wait retired every
    // list that dereferences this copy), then release retires whose covering
    // fence has signaled (dropping the entry releases the COM references).
    pub(in crate::directx) fn apply_streamed_texture_rewrites(&mut self, frame: usize) {
        self.stream.frame += 1;
        if !self.stream.pool_rewrites.is_empty() {
            let last = self.scene.textures.len().saturating_sub(1);
            for slot in self.stream.pool_rewrites.begin_frame() {
                let resource = &self.scene.textures[slot.min(last)];
                write_texture_srv(
                    &self.hw.device,
                    resource,
                    self.srv_slot_cpu(self.flat_pool_slot(frame, slot)),
                );
            }
        }
        self.stream
            .retires
            .collect(self.stream.frame, StreamState::RETIRE_DEPTH);
    }

    // Reset texture-pool `slot` to a 1x1 mid-gray placeholder.
    //
    // Used by the asset-streaming subsystem to mark a slot whose texture is
    // not yet resident; a later `update_texture_slot` brings the real texture
    // back. The gray is distinct from the white no-texture fallback so a
    // not-yet-streamed slot reads differently under inspection.
    pub(crate) fn evict_texture_slot(&mut self, slot: usize) -> RenderResult<()> {
        let gray = bake::texture::TextureImage::rgba8(1, 1, vec![128, 128, 128, 255]);
        self.update_texture_slot(slot, &gray)
    }

    // Replace the live color-grading LUT with a fresh `size³` RGBA8 payload.
    // Driven by asset hot-reload (`cn debug` only) when the file-backed
    // `ColorLut` source is saved. Reuses the SRV heap slot the composite pass
    // already binds, so the new texture is picked up on the next `draw_frame`
    // with no pipeline or descriptor-table change. `wait_idle` first
    // guarantees no in-flight command list still references the old texture
    // (or the now-stale SRV) before it is overwritten and dropped. Mirrors
    // `MtlContext::update_color_lut`.
    pub(crate) fn update_color_lut(&mut self, size: u32, data: &[u8]) -> RenderResult<()> {
        self.wait_idle();
        let srv_cpu = self.scene.color_lut.srv_cpu;
        let srv_gpu = self.scene.color_lut.srv_gpu;
        let new_lut = upload_color_lut(&self.hw.alloc, size, data, srv_cpu, srv_gpu)?;
        self.scene.color_lut = new_lut;
        Ok(())
    }

    // Swap the live IBL cubemap pair for a freshly precomputed envmap payload.
    // Driven by asset hot-reload (`cn debug` only). Re-uploads into the same
    // SRV heap slots [1] (irradiance) + [2] (prefilter) the init path wrote,
    // so every pipeline that references those slots keeps working without a
    // descriptor-table rebind. The new payload may declare different mip /
    // face sizes than the original; `EnvironmentMapTextures` is replaced
    // wholesale and the next frame's `ViewUniforms` picks up the new
    // `prefilter_mip_count` from `self.scene.env_map`. `wait_idle` first guarantees
    // no in-flight command list still references the old cubes (or the
    // now-stale SRVs) before they are overwritten and dropped. Mirrors
    // `MtlContext::update_environment_map`.
    pub(crate) fn update_environment_map(&mut self, payload: &[u8]) -> RenderResult<()> {
        let view = bake::environment_map::deserialize(payload)
            .map_err(|e| RenderError::Other(format!("envmap hot-reload payload malformed: {e}")))?;
        self.wait_idle();
        let irr_srv_cpu = self.scene.env_map.irradiance.srv_cpu;
        let irr_srv_gpu = self.scene.env_map.irradiance.srv_gpu;
        let pre_srv_cpu = self.scene.env_map.prefilter.srv_cpu;
        let pre_srv_gpu = self.scene.env_map.prefilter.srv_gpu;
        let new_env = upload_environment_map(
            &self.hw.alloc,
            EnvironmentMapPayload {
                irradiance_face: view.irradiance_face,
                irradiance_bytes: view.irradiance_bytes,
                prefilter_face: view.prefilter_face,
                mip_bytes: &view.prefilter_mip_bytes,
            },
            EnvironmentMapDescriptors {
                irr_srv_cpu,
                irr_srv_gpu,
                pre_srv_cpu,
                pre_srv_gpu,
            },
        )?;
        self.scene.env_map = new_env;
        Ok(())
    }
}