awsm-renderer 0.4.2

awsm-renderer
Documentation
//! Anti-aliasing configuration.

use crate::{bind_groups::BindGroupCreate, error::Result, AwsmRenderer};

/// Anti-aliasing configuration for the renderer.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AntiAliasing {
    // if None, no MSAA
    // Some(4) is the only supported option for now
    pub msaa_sample_count: Option<u32>,
    pub smaa: bool,
    pub mipmap: bool,
}

impl AntiAliasing {
    /// Returns whether MSAA is enabled and supported.
    pub fn has_msaa_checked(&self) -> crate::error::Result<bool> {
        match self.msaa_sample_count {
            Some(4) => Ok(true),
            None => Ok(false),
            Some(sample_count) => Err(crate::error::AwsmError::UnsupportedMsaaCount(sample_count)),
        }
    }
}

impl Default for AntiAliasing {
    fn default() -> Self {
        Self {
            // Some(4) is the only supported option for now
            msaa_sample_count: Some(4),
            //msaa_sample_count: None,
            smaa: false,
            mipmap: true,
        }
    }
}

impl AwsmRenderer {
    /// Updates the anti-aliasing settings and recompiles the
    /// AA-dependent pipelines for the new config.
    ///
    /// **Two-tier model:**
    /// - **Material pipelines** (classify / opaque / per-shader edge
    ///   resolve): this method only FLAGS the reconcile
    ///   (`mark_variants_dirty`) + resets the ensure fingerprint — it does
    ///   NOT recompile them. An AA change is a config change that needs
    ///   recompilation, so the caller must follow `set_anti_aliasing` with
    ///   `commit_load` (the one compile path); `commit_load`'s
    ///   `reconcile_material_variants` → `ensure_scene_pipelines` then
    ///   recompiles exactly the new config's `(msaa, mipmaps)` variant.
    ///   (Pre-load-transaction this happened reactively in the render
    ///   preamble; that preamble is gone.)
    /// - **Non-material passes** (geometry / HZB / picker / transparent /
    ///   effects / lines / shadows) have no scene-compile path, so their
    ///   MSAA-variant recompiles stay here, awaited up front.
    ///
    /// Already-compiled variants from previous MSAA states stay cached,
    /// so toggling back-and-forth pays the compile cost only on the first
    /// transition in each direction.
    pub async fn set_anti_aliasing(&mut self, aa: AntiAliasing) -> Result<()> {
        // Race policy per https://github.com/dakom/awsm-renderer/pull/99: config-change
        // APIs return NotReady when called before build() finishes its
        // eager batch. The frontends already structure their renderer
        // lifecycle to call this post-`build().await`; this just makes
        // the contract explicit.
        if !self.build_complete {
            return Err(crate::error::AwsmError::NotReady);
        }
        // No-op fast path — caller asked for the state we're
        // already in. The bind-group recreate marks are skipped too;
        // there's nothing for them to invalidate.
        if self.anti_aliasing == aa {
            return Ok(());
        }
        let prev_msaa_on = self.anti_aliasing.has_msaa_checked()?;
        self.anti_aliasing = aa;
        let new_msaa_on = self.anti_aliasing.has_msaa_checked()?;

        // MSAA off → on transition: allocate `material_edge_buffers`
        // + `material_edge_layout_uniform` if they're not already
        // resident. The multisampled classify bind-group layout
        // statically includes the edge bindings (slots 4..=9) at
        // boot, but the buffers themselves are only allocated when
        // MSAA is on at build time (see `lib.rs`'s `edge_resolve_enabled`
        // gate). Without this allocation, the next frame's
        // `BindGroupCreate::AntiAliasingChange`-driven recreate of
        // the multisampled classify bind group has nothing to bind
        // into slots 4..=9 → WebGPU rejects the bind-group create
        // with a "required entry missing" validation error.
        //
        // Gated on `edge_resolve_supported` (matches the build-site's
        // `edge_resolve_enabled`). MSAA-on transition on a device
        // that doesn't support the full edge_resolve dispatch wiring
        // is a no-op here — the multisampled classify layout was
        // built without slots 4..=9 to match (see
        // `create_bind_group_layout_key`'s `edge_emit_supported` gate),
        // so the base 4-entry bind group is valid.
        if !prev_msaa_on
            && new_msaa_on
            && self.material_edge_buffers.is_none()
            && crate::edge_resolve_supported(&self.gpu)
        {
            let bucket_count = self.dynamic_materials.bucket_entries_cached().len() as u32;
            use crate::render_passes::material_opaque::edge_buffers::{
                build_edge_layout_uniform, MaterialEdgeBuffers,
            };
            let edge_buffers = MaterialEdgeBuffers::new(&self.gpu, bucket_count)?;
            let max_edge_budget = edge_buffers.max_edge_budget;
            let (uniform, _bytes) =
                build_edge_layout_uniform(&self.gpu, bucket_count, max_edge_budget)?;
            self.material_edge_buffers = Some(edge_buffers);
            self.material_edge_layout_uniform = Some(uniform);
        }

        self.bind_groups
            .mark_create(BindGroupCreate::AntiAliasingChange);
        self.bind_groups
            .mark_create(BindGroupCreate::TextureViewRecreate);

        // Material pipelines (classify / opaque / per-shader edge resolve):
        // flag the reconcile + reset the ensure fingerprint so the caller's
        // follow-up `commit_load` (the one compile path) recompiles the active
        // `(msaa, mipmaps)` variant for every live bucket against the new config
        // (clearing the stale layout-keyed caches + bumping generations to drop
        // in-flight old-config resolutions). render() no longer drives this —
        // `set_anti_aliasing` MUST be followed by `commit_load`.
        self.last_ensured_bucket_layout = None;
        self.materials.mark_variants_dirty();

        // ── Non-material compute passes (HZB + picker): no render-driven
        //    re-ensure exists for these, so their MSAA-variant recompiles
        //    stay here. Each builder returns the cache keys for the NEW
        //    config; already-compiled variants resolve as cache hits.

        // HZB descriptors — only present when `features.gpu_culling`
        // is on.
        let hzb_descs = if let Some(hzb) = self.render_passes.hzb.as_ref() {
            Some(
                crate::render_passes::hzb::pipeline::HzbPipelines::build_descriptors_for_config(
                    &self.gpu,
                    &mut self.bind_group_layouts,
                    &mut self.pipeline_layouts,
                    &mut self.shaders,
                    &hzb.bind_groups,
                    &self.anti_aliasing,
                )
                .await?,
            )
        } else {
            None
        };

        // Picker descriptors — only present when the picker has been
        // lazily compiled (Block B.4: `self.picker` stays `None` until
        // first `pick()` query even when `features.picking == true`).
        // When the picker isn't yet built, this block is skipped — the
        // next `pick()` compiles it for the live AA config. When it IS
        // built, returns the picker's BGLs + the (single) pipeline cache
        // key for the new MSAA; the previously-compiled variant on
        // `self.picker` is preserved via `merge_resolved`.
        let picker_descs = if let Some(picker) = self.picker.as_ref() {
            let _ = picker; // bind for clarity; we only need to know it's Some
            Some(
                crate::picker::Picker::build_descriptors(
                    &self.gpu,
                    &mut self.bind_group_layouts,
                    &mut self.pipeline_layouts,
                    &mut self.shaders,
                    &self.anti_aliasing,
                )
                .await?,
            )
        } else {
            None
        };

        // Batched compute compile for the HZB + picker keys (union).
        use crate::pipelines::compute_pipeline::ComputePipelineCacheKey;
        let mut compute_jobs: Vec<ComputePipelineCacheKey> = Vec::new();
        let hzb_range = hzb_descs.as_ref().map(|d| {
            let start = compute_jobs.len();
            compute_jobs.extend(d.pipeline_cache_keys.iter().cloned());
            start..compute_jobs.len()
        });
        let picker_range = picker_descs.as_ref().map(|d| {
            let start = compute_jobs.len();
            compute_jobs.extend(d.pipeline_cache_keys.iter().cloned());
            start..compute_jobs.len()
        });

        let resolved = if compute_jobs.is_empty() {
            Vec::new()
        } else {
            self.pipelines
                .compute
                .ensure_keys(
                    &self.gpu,
                    &self.shaders,
                    &self.pipeline_layouts,
                    compute_jobs,
                )
                .await?
        };

        // Merge resolved pipelines into the per-pass caches (sync slotmap
        // inserts; previously-compiled variants preserved).
        if let (Some(hzb_descs), Some(hzb_range), Some(hzb_pass)) =
            (hzb_descs, hzb_range, self.render_passes.hzb.as_mut())
        {
            hzb_pass
                .pipelines
                .merge_resolved(hzb_descs.slot, resolved[hzb_range].to_vec());
        }
        if let (Some(picker_descs), Some(picker_range), Some(picker)) =
            (picker_descs, picker_range, self.picker.as_mut())
        {
            picker.merge_resolved(picker_descs.slot, resolved[picker_range].to_vec());
        }

        // ── Phase 4b: geometry MSAA branch recompile (lazy-pool).
        //    Skip when the new MSAA's branch is already populated
        //    (the user previously toggled this way and back). When
        //    new, build 9 render pipelines + 3 shaders for just the
        //    new branch and fold into the existing nested struct.
        let multisampled_geometry = self.anti_aliasing.has_msaa_checked()?;
        if !self
            .render_passes
            .geometry
            .pipelines
            .has_branch_for(&self.anti_aliasing)
        {
            // Phase 4b.i: shader compile batch for the new branch's 3 keys.
            let geometry_shader_keys_needed =
                crate::render_passes::geometry::pipeline::GeometryPipelines::shader_cache_keys(
                    multisampled_geometry,
                );
            self.shaders
                .ensure_keys(&self.gpu, geometry_shader_keys_needed)
                .await?;

            // Phase 4b.ii: build the new branch's 9 render pipeline
            // descriptors. Reuses the same RenderPassInitContext
            // shape the cold-boot path uses.
            let mut ctx = crate::render_passes::RenderPassInitContext {
                gpu: &self.gpu,
                bind_group_layouts: &mut self.bind_group_layouts,
                pipeline_layouts: &mut self.pipeline_layouts,
                pipelines: &mut self.pipelines,
                shaders: &mut self.shaders,
                render_texture_formats: &mut self.render_textures.formats,
                textures: &mut self.textures,
                features: &self.features,
                anti_aliasing: &self.anti_aliasing,
                post_processing: &self.post_processing,
                prep_config: &self.prep_config,
                max_edge_budget: self.material_edge_buffers.as_ref().map(|b| b.max_edge_budget).unwrap_or(crate::render_passes::material_opaque::edge_buffers::DEFAULT_MAX_EDGE_BUDGET_DESKTOP),
            };
            let geometry_descs =
                crate::render_passes::geometry::pipeline::GeometryPipelines::build_descriptors(
                    &mut ctx,
                    &self.render_passes.geometry.bind_groups,
                    multisampled_geometry,
                )
                .await?;

            // Phase 4b.iii: batch render pipeline compile.
            let geometry_pipeline_keys = self
                .pipelines
                .render
                .ensure_keys(
                    &self.gpu,
                    &self.shaders,
                    &self.pipeline_layouts,
                    geometry_descs.pipeline_cache_keys.clone(),
                )
                .await?;

            // Phase 4b.iv: merge into the existing struct (preserves
            // any previously-populated MSAA branch).
            self.render_passes
                .geometry
                .pipelines
                .merge_resolved(&geometry_descs, geometry_pipeline_keys)?;
        }

        // ── Phase 5: transparent pipelines depend on per-mesh
        //    attributes AND AA settings — recompile every live
        //    mesh's variant. Batched inside `set_render_pipeline_keys_batched`.
        let mut requests: Vec<
            crate::render_passes::material_transparent::pipeline::TransparentMeshPipelineRequest,
        > = Vec::new();
        for (mesh_key, mesh) in self.meshes.iter() {
            // Only transparent-pass meshes get a transparent pipeline — an
            // opaque (incl. opaque-dynamic) material can't compile against the
            // transparent fragment contract.
            if !self.materials.is_transparency_pass(mesh.material_key) {
                continue;
            }
            let buffer_info_key = self.meshes.buffer_info_key(mesh_key)?;
            let writes_depth = self.materials.transparent_writes_depth(mesh.material_key);
            let (base, pbr_features) = self.materials.transparent_variant(mesh.material_key);
            let dynamic_shader_id = matches!(base, crate::dynamic_materials::ShadingBase::Custom)
                .then(|| self.materials.shader_id(mesh.material_key));
            let dynamic_shader =
                dynamic_shader_id.and_then(|id| self.dynamic_materials.shader_info_for(id));
            let dynamic_vertex_shader =
                dynamic_shader_id.and_then(|id| self.dynamic_materials.vertex_shader_info_for(id));
            requests.push(
                crate::render_passes::material_transparent::pipeline::TransparentMeshPipelineRequest {
                    mesh,
                    mesh_key,
                    buffer_info_key,
                    writes_depth,
                    base,
                    pbr_features,
                    dynamic_shader_id,
                    dynamic_shader,
                    dynamic_vertex_shader,
                },
            );
        }
        self.render_passes
            .material_transparent
            .pipelines
            .set_render_pipeline_keys_batched(
                &self.gpu,
                requests,
                &mut self.shaders,
                &mut self.pipelines,
                &self.render_passes.material_transparent.bind_groups,
                &self.pipeline_layouts,
                &self.meshes.buffer_infos,
                &self.anti_aliasing,
                &self.textures,
                &self.render_textures.formats,
            )
            .await?;

        // ── Phase 6: effects pass (post-processing) — its own
        //    batched ensure inside `set_render_pipeline_keys`.
        self.render_passes
            .effects
            .pipelines
            .set_render_pipeline_keys(
                &self.anti_aliasing,
                &self.post_processing,
                &self.gpu,
                &mut self.shaders,
                &mut self.pipelines,
                &self.pipeline_layouts,
                &self.render_textures.formats,
            )
            .await?;

        // NOTE: the MSAA edge-resolve pipeline set is layout-level (its cache
        // keys embed the whole bucket list); the `mark_variants_dirty` flag set
        // above makes the caller's follow-up `commit_load` →
        // `ensure_scene_pipelines` → `launch_edge_resolve_compile` rebuild it
        // for the new config. `multisampled_geometry` (computed in Phase 4b for
        // the geometry branch's lazy-pool selector) equals `new_msaa_on`; assert
        // that invariant once for documentation.
        debug_assert_eq!(new_msaa_on, multisampled_geometry);

        // ── Phase 8 (Block B.3): line pipelines lazy ensure. Cold-boot
        //    leaves the 4 line-pipeline variants uncompiled; the first
        //    `add_line_*` flips `pipelines_compile_requested`. Recompile
        //    on MSAA flip is a no-op once variants populate.
        if !self.lines.is_empty() || self.lines.pipelines_compile_requested() {
            self.ensure_line_pipelines_compiled().await?;
        }

        // ── Phase 9 (Block B.1 + B.2): shadow pipeline compile. Caster
        //    + EVSM pipelines are MSAA-invariant (depth-only fragment +
        //    compute) so a flip doesn't itself require recompile, but
        //    use this `.await` as a convenient moment to land any
        //    pending compile if a shadow caster is registered and
        //    pipelines aren't yet compiled. No-op when nothing to do.
        self.ensure_shadow_pipelines_compiled().await?;

        Ok(())
    }
}