diff --git a/crates/dpvs_iw4/src/walk.rs b/crates/dpvs_iw4/src/walk.rs index 662dc79..5592f54 100644 --- a/crates/dpvs_iw4/src/walk.rs +++ b/crates/dpvs_iw4/src/walk.rs @@ -302,6 +302,9 @@ pub(crate) fn finalize_queued_hull_no_frustum(item: &mut Queued, view_dir: [f32; true } +/// Called for every portal visit of a cell, with the planes it was seen through. +pub type CellVisit<'a> = dyn FnMut(usize, &CellClipPlanes) + 'a; + pub fn visit_cells( graph: &CellPortalGraph<'_>, camera_cell: usize, @@ -313,6 +316,7 @@ pub fn visit_cells( scratch: &mut WalkScratch, mut cell_clips: Option<&mut [CellClipPlanes]>, bevels: Option<&PortalBevels>, + mut on_visit: Option<&mut CellVisit<'_>>, ) -> WalkStats { let mut stats = WalkStats::default(); let near = clip_planes.first().copied(); @@ -384,6 +388,15 @@ pub fn visit_cells( } } } + // A cell seen through several portals is visited once per portal, each visit with its + // own clip planes; `cell_clips` keeps only the first. Entities in the cell must be culled + // against every visit (IW4 queues one cell command per visit), or what shows only + // through a later portal vanishes. + if let Some(visit) = on_visit.as_mut() { + let mut planes = CellClipPlanes::EMPTY; + record_cell_clip(&item, clip_planes, &mut planes); + visit(cell, &planes); + } let cell_planes = queued_clip_planes(&item, clip_planes); let Some(edges) = graph.portals.get(cell).copied() else { if let Some(slot) = recycle { diff --git a/crates/render_frontend/src/adapters/anim/dyn_ent_brush.rs b/crates/render_frontend/src/adapters/anim/dyn_ent_brush.rs index 0f84a4c..88e512a 100644 --- a/crates/render_frontend/src/adapters/anim/dyn_ent_brush.rs +++ b/crates/render_frontend/src/adapters/anim/dyn_ent_brush.rs @@ -496,13 +496,22 @@ fn exec_cell_scene_ent_cmds( scene.scene_ent_walked = true; let bit_count = scene_ent_cell_walk_bits(GFX_CFG_ENT_COUNT); + // The planes this cell was seen through on this command's portal visit (see + // `CellFrustumWorkerCmd::visit`); the camera frustum when there are none. let cell_planes: Vec<[f32; 4]> = stats .as_ref() - .and_then(|s| { - s.cell_clips + .and_then(|s| match cmd.visit { + render_frontend::CELL_VISIT_FRUSTUM => None, + render_frontend::CELL_VISIT_FIRST => s + .cell_clips .get(cell) .filter(|c| c.plane_count > 0) - .map(|c| c.as_slice().to_vec()) + .map(|c| c.as_slice().to_vec()), + visit => s + .cell_clip_visits + .get(visit as usize - 1) + .filter(|(visited, c)| *visited as usize == cell && c.plane_count > 0) + .map(|(_, c)| c.as_slice().to_vec()), }) .unwrap_or_else(|| planes.clone()); let sphere_planes = diff --git a/crates/render_frontend/src/assemble/drawsurf/tess/glass.rs b/crates/render_frontend/src/assemble/drawsurf/tess/glass.rs index 716e4d1..9b5bdb8 100644 --- a/crates/render_frontend/src/assemble/drawsurf/tess/glass.rs +++ b/crates/render_frontend/src/assemble/drawsurf/tess/glass.rs @@ -763,7 +763,11 @@ pub(crate) fn enqueue_glass_model_lighting( let owner = ModelLightingOwner::Glass(draw.piece); draw.lighting_prev = u32::from(cache.handle_for(owner)); draw.pending_lighting = None; - if sphere_behind_frustum(draw.origin, GLASS_FRUSTUM_LIGHT_RADIUS, &planes) { + // The pane's own extent: a window much wider than 64 units can show while its centre + // is off screen, and a pane drawn without lighting takes probe 0 and the default + // lighting (the red-tinted windows on Terminal). + let radius = draw.radius.max(GLASS_FRUSTUM_LIGHT_RADIUS); + if sphere_behind_frustum(draw.origin, radius, &planes) { draw.lighting_handle = 0; draw.reflection_probe_index = 0; continue; diff --git a/crates/render_frontend/src/lib.rs b/crates/render_frontend/src/lib.rs index d61b8c7..74d4422 100644 --- a/crates/render_frontend/src/lib.rs +++ b/crates/render_frontend/src/lib.rs @@ -22,10 +22,10 @@ pub use prepare::gfx_scene::{ pub use prepare::scene::world::*; pub use prepare::sun::{add_bsp_sun_shadow_partition, pack_sun_shadow_caster_lists}; pub use prepare::worker_cmds::{ - AddWorkerCmd, BoundEntWorkerCmd, CellFrustumWorkerCmd, DpvsEntWorkerCmd, - SkinCachedStaticModelCmd, SpotShadowEntWorkerCmd, WORKER_CMD_BOUND_ENT, WORKER_CMD_BUSY, - WORKER_CMD_CELL_DYN_BRUSH, WORKER_CMD_CELL_DYN_MODEL, WORKER_CMD_CELL_SCENE_ENT, - WORKER_CMD_COUNT, WORKER_CMD_DPVS_ENT, WORKER_CMD_SMODELCACHE, WORKER_CMD_SPOT_SHADOW_ENT, - WorkerCmdBusy, WorkerCmdBusyInput, WorkerCmdError, WorkerCmdQueues, enqueue_cell_frustum_cmds, - worker_cmd_dispatch_skips_device_lost, + AddWorkerCmd, BoundEntWorkerCmd, CELL_VISIT_FIRST, CELL_VISIT_FRUSTUM, CellFrustumWorkerCmd, + DpvsEntWorkerCmd, SkinCachedStaticModelCmd, SpotShadowEntWorkerCmd, WORKER_CMD_BOUND_ENT, + WORKER_CMD_BUSY, WORKER_CMD_CELL_DYN_BRUSH, WORKER_CMD_CELL_DYN_MODEL, + WORKER_CMD_CELL_SCENE_ENT, WORKER_CMD_COUNT, WORKER_CMD_DPVS_ENT, WORKER_CMD_SMODELCACHE, + WORKER_CMD_SPOT_SHADOW_ENT, WorkerCmdBusy, WorkerCmdBusyInput, WorkerCmdError, WorkerCmdQueues, + enqueue_cell_frustum_cmds, worker_cmd_dispatch_skips_device_lost, }; diff --git a/crates/render_frontend/src/prepare/scene/cell_frustum_cmds.rs b/crates/render_frontend/src/prepare/scene/cell_frustum_cmds.rs index f2a095b..8fbb665 100644 --- a/crates/render_frontend/src/prepare/scene/cell_frustum_cmds.rs +++ b/crates/render_frontend/src/prepare/scene/cell_frustum_cmds.rs @@ -1,7 +1,8 @@ use bevy::prelude::*; use render_frontend::{ - AddWorkerCmd, CellFrustumWorkerCmd, WORKER_CMD_CELL_DYN_BRUSH, WORKER_CMD_CELL_DYN_MODEL, - WORKER_CMD_CELL_SCENE_ENT, enqueue_cell_frustum_cmds, + AddWorkerCmd, CELL_VISIT_FIRST, CELL_VISIT_FRUSTUM, CellFrustumWorkerCmd, + WORKER_CMD_CELL_DYN_BRUSH, WORKER_CMD_CELL_DYN_MODEL, WORKER_CMD_CELL_SCENE_ENT, + enqueue_cell_frustum_cmds, }; use crate::prepare::scene::cull::DpvsFrameStats; @@ -43,30 +44,65 @@ fn enqueue_cell_frustum_cmds_after_vis( let Some(stats) = stats.as_deref() else { return; }; - let frustum_n = stats.frustum_planes.len(); + let frustum_n = u8::try_from(stats.frustum_planes.len()).unwrap_or(u8::MAX); let cells = admitted_cell_indices(&stats.cell_vis, stats.cell_vis_count); - for cell in cells { - let (plane_count, plane_begin) = stats - .cell_clips - .get(cell as usize) - .filter(|c| c.plane_count > 0) - .map(|c| (c.plane_count, c.frustum_plane_count)) - .unwrap_or_else(|| { - let n = u8::try_from(frustum_n).unwrap_or(u8::MAX); - (n, n) - }); - let cmd = CellFrustumWorkerCmd { - cell, - plane_count, - plane_begin, - view: 0, + let mut enqueue = + |cmd: CellFrustumWorkerCmd| match enqueue_cell_frustum_cmds(&mut worker_cmds.queues, cmd) { + Ok(adds) if adds.contains(&AddWorkerCmd::OverflowInline) => false, + Err(_) => false, + Ok(_) => true, }; - match enqueue_cell_frustum_cmds(&mut worker_cmds.queues, cmd) { - Ok(adds) if adds.iter().any(|a| *a == AddWorkerCmd::OverflowInline) => { + // One command per portal visit, each culling the cell's entities against the planes it + // was seen through, as IW4 does: an entity seen through any of a cell's portals is drawn. + let visits = &stats.cell_clip_visits; + if !visits.is_empty() && visits.len() <= CELL_VISIT_BUDGET { + for (index, (cell, clip)) in visits.iter().enumerate() { + if !cells.contains(cell) || clip.plane_count == 0 { + continue; + } + let cmd = CellFrustumWorkerCmd { + visit: index as u32 + 1, + cell: *cell, + plane_count: clip.plane_count, + plane_begin: clip.frustum_plane_count, + view: 0, + }; + if !enqueue(cmd) { break; } - Err(_) => break, - Ok(_) => {} + } + return; + } + // No walk this frame (single cell, all cells, no eye cell), or too many visits to queue: + // one command per cell. With visits it falls back to the camera frustum, never to one + // visit's planes, so nothing a later portal shows is culled. + for cell in cells { + let first = stats + .cell_clips + .get(cell as usize) + .filter(|c| c.plane_count > 0 && visits.is_empty()); + let cmd = match first { + Some(c) => CellFrustumWorkerCmd { + visit: CELL_VISIT_FIRST, + cell, + plane_count: c.plane_count, + plane_begin: c.frustum_plane_count, + view: 0, + }, + None => CellFrustumWorkerCmd { + visit: CELL_VISIT_FRUSTUM, + cell, + plane_count: frustum_n, + plane_begin: frustum_n, + view: 0, + }, + }; + if !enqueue(cmd) { + break; } } } + +/// Portal visits queued as one cell command each; the cell queues hold 512 commands +/// (`0x1800` bytes of 12), so a frame with more visits falls back to one command per cell. +const CELL_VISIT_BUDGET: usize = 480; diff --git a/crates/render_frontend/src/prepare/scene/cull.rs b/crates/render_frontend/src/prepare/scene/cull.rs index 86eeae6..e3d9c3e 100644 --- a/crates/render_frontend/src/prepare/scene/cull.rs +++ b/crates/render_frontend/src/prepare/scene/cull.rs @@ -89,6 +89,10 @@ pub struct DpvsFrameStats { pub cell_clips: Vec, + /// Every portal visit of the walk, in order: the cell and the planes it was seen through. + /// A cell seen through several portals appears once per portal. + pub cell_clip_visits: Vec<(u32, CellClipPlanes)>, + pub cell_vis_all: bool, } @@ -146,6 +150,7 @@ pub fn apply_dpvs_cull( stats.cell_vis.clear(); stats.cell_vis_count = 0; stats.cell_clips.clear(); + stats.cell_clip_visits.clear(); stats.cell_vis_all = false; stats.view_prepared = 0; stats.lock_pvs = 0; @@ -222,6 +227,9 @@ pub fn apply_dpvs_cull( &mut scratch, Some(&mut cell_clips), bevels.as_ref(), + Some(&mut |visited: usize, planes: &CellClipPlanes| { + stats.cell_clip_visits.push((visited as u32, *planes)); + }), ); stats.cell_clips = cell_clips; } diff --git a/crates/render_frontend/src/prepare/worker_cmds.rs b/crates/render_frontend/src/prepare/worker_cmds.rs index 001407d..02a27a3 100644 --- a/crates/render_frontend/src/prepare/worker_cmds.rs +++ b/crates/render_frontend/src/prepare/worker_cmds.rs @@ -169,16 +169,26 @@ pub struct SkinCachedStaticModelCmd { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CellFrustumWorkerCmd { + /// Which planes cull the cell's entities: [`CELL_VISIT_FIRST`] the cell's first portal visit + /// (`DpvsFrameStats::cell_clips`), [`CELL_VISIT_FRUSTUM`] the camera frustum, else the + /// 1-based index of one portal visit in `DpvsFrameStats::cell_clip_visits`. + pub visit: u32, pub cell: u32, pub plane_count: u8, pub plane_begin: u8, pub view: u16, } +/// [`CellFrustumWorkerCmd::visit`]: the cell's first-visit planes. +pub const CELL_VISIT_FIRST: u32 = 0; +/// [`CellFrustumWorkerCmd::visit`]: the camera frustum alone. +pub const CELL_VISIT_FRUSTUM: u32 = u32::MAX; + impl CellFrustumWorkerCmd { #[must_use] pub fn to_bytes(self) -> [u8; 12] { let mut out = [0u8; 12]; + out[0..4].copy_from_slice(&self.visit.to_le_bytes()); out[4..8].copy_from_slice(&self.cell.to_le_bytes()); out[8] = self.plane_count; out[9] = self.plane_begin; @@ -190,6 +200,7 @@ impl CellFrustumWorkerCmd { pub fn from_bytes(data: &[u8]) -> Option { let bytes: [u8; 12] = data.try_into().ok()?; Some(Self { + visit: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), cell: u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), plane_count: bytes[8], plane_begin: bytes[9],