aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-render/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fparkan-render/src/lib.rs')
-rw-r--r--crates/fparkan-render/src/lib.rs1165
1 files changed, 2 insertions, 1163 deletions
diff --git a/crates/fparkan-render/src/lib.rs b/crates/fparkan-render/src/lib.rs
index 96ccc2a..7e3c1f8 100644
--- a/crates/fparkan-render/src/lib.rs
+++ b/crates/fparkan-render/src/lib.rs
@@ -1,26 +1,5 @@
#![forbid(unsafe_code)]
-#![cfg_attr(
- test,
- allow(
- clippy::cast_possible_truncation,
- clippy::cast_possible_wrap,
- clippy::cast_precision_loss,
- clippy::expect_used,
- clippy::float_cmp,
- clippy::identity_op,
- clippy::too_many_lines,
- clippy::uninlined_format_args,
- clippy::map_unwrap_or,
- clippy::needless_raw_string_hashes,
- clippy::semicolon_if_nothing_returned,
- clippy::type_complexity,
- clippy::panic,
- clippy::unwrap_used
- )
-)]
-//! Backend-neutral render commands and deterministic captures.
-
-use fparkan_world::OriginalObjectId;
+//! Camera mathematics and graphics pipeline state.
/// A 64-byte transform block returned by the original Terrain camera ABI.
///
@@ -217,7 +196,7 @@ impl LegacyD3d7Projection {
// Ngi32 converts these signed viewport dimensions into single-precision
// arithmetic before building the legacy matrix.
- #[allow(clippy::cast_precision_loss)]
+
let aspect = (width as f32) / (height as f32);
let half_fov = self.field_of_view_radians * 0.5;
let cosine = half_fov.cos();
@@ -338,112 +317,6 @@ impl LegacyIron3dEulerTransform {
}
}
-/// Raw camera state observed through the original Terrain camera interface.
-///
-/// Selector 0 supplies the currently active transform and selector 2 supplies
-/// its paired transform. Keeping both lets a later compatibility layer derive
-/// a view/projection convention without re-reading the legacy process.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct RawCameraPose {
- /// Transform returned by selector 0.
- pub selector0: RawCameraTransform,
- /// Transform returned by selector 2.
- pub selector2: RawCameraTransform,
-}
-
-/// Raw projection state observed through the original `CBufferingCamera` ABI.
-///
-/// The five-float context block is intentionally represented as original words:
-/// its indices are observed, but their semantic labels have not yet all been
-/// recovered. The field-of-view value is the type-0 input to `tan(fov / 2)`.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct RawCameraProjection {
- /// Viewport rectangle as `(left, top, right, bottom)`.
- pub viewport: [i32; 4],
- /// Original projection selector.
- pub projection_type: u32,
- /// Original IEEE-754 FOV value in radians.
- pub field_of_view_radians_bits: u32,
- /// Exact five-float context block returned by the primary renderer.
- pub context_words: [u32; 5],
-}
-
-impl RawCameraProjection {
- /// Returns the type-0 FOV input in radians.
- #[must_use]
- pub fn field_of_view_radians(self) -> f32 {
- f32::from_bits(self.field_of_view_radians_bits)
- }
-
- /// Returns the five context values without assigning semantic labels.
- #[must_use]
- pub fn context_values(self) -> [f32; 5] {
- self.context_words.map(f32::from_bits)
- }
-}
-
-/// Immutable camera data visible to command generation.
-#[derive(Clone, Debug, PartialEq)]
-pub struct CameraSnapshot {
- /// View matrix, row-major.
- pub view: [f32; 16],
- /// Projection matrix, row-major.
- pub projection: [f32; 16],
- /// Optional unconverted source-camera state.
- ///
- /// This does not alter rendering until the original matrix and projection
- /// conventions have been recovered; it preserves the ABI boundary for the
- /// runtime adapter and deterministic captures.
- pub raw_pose: Option<RawCameraPose>,
- /// Optional unconverted source-projection state.
- ///
- /// This is retained independently of `projection`, because its legacy
- /// context words have not yet been mapped to a Vulkan clip convention.
- pub raw_projection: Option<RawCameraProjection>,
-}
-
-impl Default for CameraSnapshot {
- fn default() -> Self {
- Self {
- view: identity_transform(),
- projection: identity_transform(),
- raw_pose: None,
- raw_projection: None,
- }
- }
-}
-
-/// Draw id.
-#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
-pub struct DrawId(pub u64);
-
-/// GPU mesh id.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct GpuMeshId(pub u64);
-
-/// GPU material id.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct GpuMaterialId(pub u64);
-
-/// Render phase.
-#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
-pub enum RenderPhase {
- /// Terrain.
- Terrain,
- /// Opaque.
- Opaque,
- /// Alpha test.
- AlphaTest,
- /// Transparent.
- Transparent,
- /// Effects.
- Effects,
- /// Debug.
- Debug,
- /// UI.
- Ui,
-}
-
/// Fixed-function blend behaviour represented without a graphics API type.
///
/// This is a compatibility contract, not yet a decoded MAT0 mapping.
@@ -531,546 +404,6 @@ impl From<LegacyPipelineState> for PipelineKey {
}
}
-/// Index range.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct IndexRange {
- /// Start.
- pub start: u32,
- /// Count.
- pub count: u32,
-}
-
-/// A draw candidate in an immutable render snapshot.
-#[derive(Clone, Debug, PartialEq)]
-pub struct RenderSnapshotDraw {
- /// Draw id.
- pub id: DrawId,
- /// Phase.
- pub phase: RenderPhase,
- /// Object id.
- pub object_id: Option<OriginalObjectId>,
- /// Mesh.
- pub mesh: GpuMeshId,
- /// Material table after WEAR/MAT0 fallback resolution.
- pub material_slots: Vec<GpuMaterialId>,
- /// Batch material index into [`Self::material_slots`].
- pub material_index: u16,
- /// Fixed-function state resolved for this draw.
- pub pipeline_state: LegacyPipelineState,
- /// Node transform matrix, row-major.
- pub transform: [f32; 16],
- /// Index range.
- pub range: IndexRange,
- /// Stable sort order.
- pub stable_order: u64,
-}
-
-/// Immutable backend-neutral render snapshot.
-#[derive(Clone, Debug, Default, PartialEq)]
-pub struct RenderSnapshot {
- /// Camera data for the frame.
- pub camera: CameraSnapshot,
- /// Draw candidates gathered from world/assets.
- pub draws: Vec<RenderSnapshotDraw>,
-}
-
-/// Command generation profile.
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub struct RenderProfile {
- /// Include UI phase commands when present.
- pub include_ui: bool,
-}
-
-/// Draw command.
-#[derive(Clone, Debug, PartialEq)]
-pub struct DrawCommand {
- /// Draw id.
- pub id: DrawId,
- /// Phase.
- pub phase: RenderPhase,
- /// Object id.
- pub object_id: Option<OriginalObjectId>,
- /// Mesh.
- pub mesh: GpuMeshId,
- /// Material.
- pub material: GpuMaterialId,
- /// Canonical graphics-pipeline variant.
- pub pipeline_key: PipelineKey,
- /// Transform matrix, row-major.
- pub transform: [f32; 16],
- /// Index range.
- pub range: IndexRange,
- /// Stable sort order.
- pub stable_order: u64,
-}
-
-/// Render command.
-#[derive(Clone, Debug, PartialEq)]
-pub enum RenderCommand {
- /// Begin frame.
- BeginFrame,
- /// Draw.
- Draw(DrawCommand),
- /// End frame.
- EndFrame,
-}
-
-/// Render command list.
-#[derive(Clone, Debug, Default, PartialEq)]
-pub struct RenderCommandList {
- /// Commands.
- pub commands: Vec<RenderCommand>,
-}
-
-/// Optional render command validation limits.
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub struct RenderValidationLimits {
- /// Exclusive upper bound for GPU mesh ids.
- pub mesh_count: Option<u64>,
- /// Exclusive upper bound for index ranges.
- pub index_count: Option<u32>,
-}
-
-/// Frame output.
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub struct FrameOutput;
-
-/// Render error.
-#[derive(Debug)]
-pub enum RenderError {
- /// Invalid range.
- InvalidRange,
- /// Invalid command stream framing or ordering.
- InvalidCommandStream {
- /// Command index.
- index: usize,
- /// Contextual error message.
- message: &'static str,
- },
- /// Invalid draw range with command-generation context.
- InvalidDrawRange {
- /// Draw id.
- draw_id: DrawId,
- /// Stable sort order.
- stable_order: u64,
- /// Range start.
- start: u32,
- /// Range count.
- count: u32,
- },
- /// Index range arithmetic overflow.
- IndexRangeOverflow {
- /// Draw id.
- draw_id: DrawId,
- /// Range start.
- start: u32,
- /// Range count.
- count: u32,
- },
- /// Index range exceeds validation limits.
- IndexRangeOutOfBounds {
- /// Draw id.
- draw_id: DrawId,
- /// Exclusive index limit.
- index_count: u32,
- /// Range end.
- end: u32,
- },
- /// Mesh id exceeds validation limits.
- MeshOutOfBounds {
- /// Draw id.
- draw_id: DrawId,
- /// Mesh id.
- mesh: GpuMeshId,
- /// Exclusive mesh limit.
- mesh_count: u64,
- },
- /// Draw transform contains a non-finite value.
- NonFiniteTransform {
- /// Draw id.
- draw_id: DrawId,
- /// Matrix element index.
- element: usize,
- },
- /// Draw commands are not ordered by phase, stable order and draw id.
- PhaseOrderViolation {
- /// Draw id.
- draw_id: DrawId,
- /// Previous phase.
- previous: RenderPhase,
- /// Current phase.
- current: RenderPhase,
- },
- /// A batch material index did not resolve through the material table.
- MaterialIndexOutOfBounds {
- /// Draw id.
- draw_id: DrawId,
- /// Requested material index.
- material_index: u16,
- /// Available material slots.
- material_count: usize,
- },
-}
-
-impl std::fmt::Display for RenderError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::InvalidRange => write!(f, "render command contains an empty index range"),
- Self::InvalidCommandStream { index, message } => {
- write!(
- f,
- "render command stream is invalid at command {index}: {message}"
- )
- }
- Self::InvalidDrawRange {
- draw_id,
- stable_order,
- start,
- count,
- } => write!(
- f,
- "draw {} has invalid index range start={} count={} at stable order {}",
- draw_id.0, start, count, stable_order
- ),
- Self::IndexRangeOverflow {
- draw_id,
- start,
- count,
- } => write!(
- f,
- "draw {} index range overflows start={} count={}",
- draw_id.0, start, count
- ),
- Self::IndexRangeOutOfBounds {
- draw_id,
- index_count,
- end,
- } => write!(
- f,
- "draw {} index range ends at {} but mesh has {} indices",
- draw_id.0, end, index_count
- ),
- Self::MeshOutOfBounds {
- draw_id,
- mesh,
- mesh_count,
- } => write!(
- f,
- "draw {} references mesh {} but only {} meshes are available",
- draw_id.0, mesh.0, mesh_count
- ),
- Self::NonFiniteTransform { draw_id, element } => write!(
- f,
- "draw {} has non-finite transform element {}",
- draw_id.0, element
- ),
- Self::PhaseOrderViolation {
- draw_id,
- previous,
- current,
- } => write!(
- f,
- "draw {} phase order regressed from {:?} to {:?}",
- draw_id.0, previous, current
- ),
- Self::MaterialIndexOutOfBounds {
- draw_id,
- material_index,
- material_count,
- } => write!(
- f,
- "draw {} references material index {} but only {} material slots are available",
- draw_id.0, material_index, material_count
- ),
- }
- }
-}
-
-impl std::error::Error for RenderError {}
-
-/// Builds a deterministic command list from an immutable render snapshot.
-///
-/// # Errors
-///
-/// Returns [`RenderError`] when a draw has an invalid index range or a material
-/// index that cannot be resolved through its material slot table.
-pub fn build_commands(
- snapshot: &RenderSnapshot,
- profile: RenderProfile,
-) -> Result<RenderCommandList, RenderError> {
- let mut draws = snapshot
- .draws
- .iter()
- .filter(|draw| profile.include_ui || draw.phase != RenderPhase::Ui)
- .collect::<Vec<_>>();
- draws.sort_by_key(|draw| (draw.phase, draw.stable_order, draw.id));
-
- let mut commands = Vec::with_capacity(draws.len() + 2);
- commands.push(RenderCommand::BeginFrame);
- for draw in draws {
- if draw.range.count == 0 {
- return Err(RenderError::InvalidDrawRange {
- draw_id: draw.id,
- stable_order: draw.stable_order,
- start: draw.range.start,
- count: draw.range.count,
- });
- }
- validate_index_range(draw.id, draw.range)?;
- validate_transform(draw.id, &draw.transform)?;
- let material = draw
- .material_slots
- .get(usize::from(draw.material_index))
- .copied()
- .ok_or(RenderError::MaterialIndexOutOfBounds {
- draw_id: draw.id,
- material_index: draw.material_index,
- material_count: draw.material_slots.len(),
- })?;
- commands.push(RenderCommand::Draw(DrawCommand {
- id: draw.id,
- phase: draw.phase,
- object_id: draw.object_id,
- mesh: draw.mesh,
- material,
- pipeline_key: draw.pipeline_state.into(),
- transform: draw.transform,
- range: draw.range,
- stable_order: draw.stable_order,
- }));
- }
- commands.push(RenderCommand::EndFrame);
- Ok(RenderCommandList { commands })
-}
-
-/// Backend port.
-pub trait RenderBackend {
- /// Executes commands.
- ///
- /// # Errors
- ///
- /// Returns [`RenderError`] when the command stream is malformed for the
- /// backend.
- fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError>;
-}
-
-/// Marker trait for backends that execute draws against a live GPU.
-///
-/// Planning and capture-only backends must not implement this trait.
-pub trait GpuRenderBackend: RenderBackend {}
-
-/// Backend that validates commands and intentionally produces no pixels.
-#[derive(Clone, Debug, Default)]
-pub struct NullBackend;
-
-impl RenderBackend for NullBackend {
- fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
- validate_command_list(commands)?;
- Ok(FrameOutput)
- }
-}
-
-/// Backend that stores deterministic command captures for verification.
-#[derive(Clone, Debug, Default)]
-pub struct RecordingBackend {
- captures: Vec<Vec<u8>>,
-}
-
-impl RecordingBackend {
- /// Returns all captures in submission order.
- #[must_use]
- pub fn captures(&self) -> &[Vec<u8>] {
- &self.captures
- }
-
- /// Returns the most recent capture.
- #[must_use]
- pub fn last_capture(&self) -> Option<&[u8]> {
- self.captures.last().map(Vec::as_slice)
- }
-
- /// Clears stored captures without changing backend behavior.
- pub fn clear(&mut self) {
- self.captures.clear();
- }
-}
-
-impl RenderBackend for RecordingBackend {
- fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
- let capture = canonical_capture(commands)?;
- self.captures.push(capture);
- Ok(FrameOutput)
- }
-}
-
-/// Builds a canonical capture.
-///
-/// # Errors
-///
-/// Returns [`RenderError`] when a draw command contains an invalid index range.
-pub fn canonical_capture(commands: &RenderCommandList) -> Result<Vec<u8>, RenderError> {
- validate_command_list(commands)?;
- let mut out = Vec::new();
- for command in &commands.commands {
- match command {
- RenderCommand::BeginFrame => out.extend_from_slice(b"B\n"),
- RenderCommand::EndFrame => out.extend_from_slice(b"E\n"),
- RenderCommand::Draw(draw) => {
- out.extend_from_slice(
- format!(
- "D,{:?},{},{},{},{},{}\n",
- draw.phase,
- draw.id.0,
- draw.mesh.0,
- draw.material.0,
- draw.pipeline_key.packed(),
- draw.stable_order
- )
- .as_bytes(),
- );
- }
- }
- }
- Ok(out)
-}
-
-/// Validates a render command list without backend-specific resource limits.
-///
-/// # Errors
-///
-/// Returns [`RenderError`] when framing, ordering or draw data is invalid.
-pub fn validate_command_list(commands: &RenderCommandList) -> Result<(), RenderError> {
- validate_command_list_with_limits(commands, RenderValidationLimits::default())
-}
-
-/// Validates a render command list with optional backend resource limits.
-///
-/// # Errors
-///
-/// Returns [`RenderError`] when framing, ordering, draw data or resource bounds
-/// are invalid.
-pub fn validate_command_list_with_limits(
- commands: &RenderCommandList,
- limits: RenderValidationLimits,
-) -> Result<(), RenderError> {
- let Some(first) = commands.commands.first() else {
- return Err(RenderError::InvalidCommandStream {
- index: 0,
- message: "empty command list",
- });
- };
- if !matches!(first, RenderCommand::BeginFrame) {
- return Err(RenderError::InvalidCommandStream {
- index: 0,
- message: "first command must be BeginFrame",
- });
- }
- if commands.commands.len() < 2 {
- return Err(RenderError::InvalidCommandStream {
- index: 0,
- message: "frame must end with EndFrame",
- });
- }
- let end_index = commands.commands.len() - 1;
- if !matches!(commands.commands[end_index], RenderCommand::EndFrame) {
- return Err(RenderError::InvalidCommandStream {
- index: end_index,
- message: "last command must be EndFrame",
- });
- }
-
- let mut previous_key: Option<(RenderPhase, u64, DrawId)> = None;
- for (index, command) in commands.commands.iter().enumerate() {
- match command {
- RenderCommand::BeginFrame if index == 0 => {}
- RenderCommand::BeginFrame => {
- return Err(RenderError::InvalidCommandStream {
- index,
- message: "nested BeginFrame is not allowed",
- });
- }
- RenderCommand::EndFrame if index == end_index => {}
- RenderCommand::EndFrame => {
- return Err(RenderError::InvalidCommandStream {
- index,
- message: "EndFrame before final command is not allowed",
- });
- }
- RenderCommand::Draw(draw) => {
- validate_draw_command(draw, limits)?;
- let key = (draw.phase, draw.stable_order, draw.id);
- if let Some(previous) = previous_key {
- if key < previous {
- return Err(RenderError::PhaseOrderViolation {
- draw_id: draw.id,
- previous: previous.0,
- current: draw.phase,
- });
- }
- }
- previous_key = Some(key);
- }
- }
- }
- Ok(())
-}
-
-fn validate_draw_command(
- draw: &DrawCommand,
- limits: RenderValidationLimits,
-) -> Result<(), RenderError> {
- if draw.range.count == 0 {
- return Err(RenderError::InvalidRange);
- }
- let end = validate_index_range(draw.id, draw.range)?;
- validate_transform(draw.id, &draw.transform)?;
- if let Some(mesh_count) = limits.mesh_count {
- if draw.mesh.0 >= mesh_count {
- return Err(RenderError::MeshOutOfBounds {
- draw_id: draw.id,
- mesh: draw.mesh,
- mesh_count,
- });
- }
- }
- if let Some(index_count) = limits.index_count {
- if end > index_count {
- return Err(RenderError::IndexRangeOutOfBounds {
- draw_id: draw.id,
- index_count,
- end,
- });
- }
- }
- Ok(())
-}
-
-fn validate_index_range(draw_id: DrawId, range: IndexRange) -> Result<u32, RenderError> {
- range
- .start
- .checked_add(range.count)
- .ok_or(RenderError::IndexRangeOverflow {
- draw_id,
- start: range.start,
- count: range.count,
- })
-}
-
-fn validate_transform(draw_id: DrawId, transform: &[f32; 16]) -> Result<(), RenderError> {
- for (element, value) in transform.iter().enumerate() {
- if !value.is_finite() {
- return Err(RenderError::NonFiniteTransform { draw_id, element });
- }
- }
- Ok(())
-}
-
-fn identity_transform() -> [f32; 16] {
- [
- 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
- ]
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -1098,49 +431,6 @@ mod tests {
}
#[test]
- fn raw_camera_pose_preserves_words_and_extracts_confirmed_translation() {
- let mut active = [0_u32; 16];
- active[0] = 0x7FC0_0001;
- active[3] = 491.562_5_f32.to_bits();
- active[7] = 761.550_8_f32.to_bits();
- active[11] = 7.361_0_f32.to_bits();
- let paired = [0xA5A5_5A5A; 16];
-
- let pose = RawCameraPose {
- selector0: RawCameraTransform { words: active },
- selector2: RawCameraTransform { words: paired },
- };
-
- assert_eq!(pose.selector0.words, active);
- assert_eq!(pose.selector2.words, paired);
- assert_eq!(
- pose.selector0.translation(),
- [491.562_5, 761.550_8, 7.361_0]
- );
- assert_eq!(CameraSnapshot::default().raw_pose, None);
- }
-
- #[test]
- fn raw_camera_projection_preserves_live_context_words_without_labeling_them() {
- let projection = RawCameraProjection {
- viewport: [0, 0, 1024, 768],
- projection_type: 0,
- field_of_view_radians_bits: 1.04_f32.to_bits(),
- context_words: [
- 0.0_f32.to_bits(),
- 0.5_f32.to_bits(),
- 700.0_f32.to_bits(),
- 0.1_f32.to_bits(),
- 0.99_f32.to_bits(),
- ],
- };
-
- assert_eq!(projection.field_of_view_radians(), 1.04);
- assert_eq!(projection.context_values(), [0.0, 0.5, 700.0, 0.1, 0.99]);
- assert_eq!(CameraSnapshot::default().raw_projection, None);
- }
-
- #[test]
fn raw_camera_transform_inverts_only_non_singular_affine_blocks() {
let source = [
0.0, -1.0, 0.0, 433.544_7, 0.948_985, 0.0, 0.315_322, 652.292_5, -0.315_322, 0.0,
@@ -1259,455 +549,4 @@ mod tests {
None
);
}
-
- fn snapshot_draw(
- id: u64,
- phase: RenderPhase,
- material_index: u16,
- stable_order: u64,
- ) -> RenderSnapshotDraw {
- RenderSnapshotDraw {
- id: DrawId(id),
- phase,
- object_id: Some(OriginalObjectId(u32::try_from(id).expect("id fits"))),
- mesh: GpuMeshId(10 + id),
- material_slots: vec![GpuMaterialId(31), GpuMaterialId(37)],
- material_index,
- pipeline_state: LegacyPipelineState::default(),
- transform: identity_transform(),
- range: IndexRange { start: 0, count: 3 },
- stable_order,
- }
- }
-
- #[test]
- fn capture_is_stable() {
- let list = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(1),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(2),
- material: GpuMaterialId(3),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: [0.0; 16],
- range: IndexRange { start: 0, count: 3 },
- stable_order: 4,
- }),
- RenderCommand::EndFrame,
- ],
- };
- assert_eq!(
- canonical_capture(&list).expect("capture"),
- b"B\nD,Opaque,1,2,3,0,4\nE\n"
- );
- }
-
- #[test]
- fn pipeline_key_is_explicit_stable_and_sensitive_to_pipeline_structure() {
- let base = LegacyPipelineState::default();
- assert_eq!(PipelineKey::from(base).packed(), 0);
-
- let variant = LegacyPipelineState {
- blend: LegacyBlendMode::SourceAlpha,
- depth: LegacyDepthMode::TestReadOnly,
- cull: LegacyCullMode::BackFace,
- alpha_test: true,
- };
- assert_eq!(PipelineKey::from(variant).packed(), 0b00_101_101);
- assert_ne!(PipelineKey::from(base), PipelineKey::from(variant));
- }
-
- #[test]
- fn alpha_test_flag_changes_key_without_encoding_material_threshold() {
- let opaque = LegacyPipelineState::default();
- let alpha_test = LegacyPipelineState {
- alpha_test: true,
- ..opaque
- };
- assert_eq!(PipelineKey::from(alpha_test).packed(), 0b10_0000);
- }
-
- #[test]
- fn null_backend_validates_without_capture() {
- let mut backend = NullBackend;
- let invalid = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(1),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(2),
- material: GpuMaterialId(3),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: [0.0; 16],
- range: IndexRange { start: 0, count: 0 },
- stable_order: 4,
- }),
- RenderCommand::EndFrame,
- ],
- };
-
- assert!(matches!(
- backend.execute(&invalid),
- Err(RenderError::InvalidRange)
- ));
- }
-
- #[test]
- fn recording_backend_stores_captures() {
- let mut backend = RecordingBackend::default();
- let list = RenderCommandList {
- commands: vec![RenderCommand::BeginFrame, RenderCommand::EndFrame],
- };
-
- backend.execute(&list).expect("execute");
- backend.execute(&list).expect("execute");
-
- assert_eq!(backend.captures().len(), 2);
- assert_eq!(backend.last_capture(), Some(&b"B\nE\n"[..]));
- backend.clear();
- assert!(backend.captures().is_empty());
- }
-
- #[test]
- fn one_snapshot_draw_produces_one_draw_command() -> Result<(), RenderError> {
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![snapshot_draw(1, RenderPhase::Opaque, 0, 10)],
- };
-
- let commands = build_commands(&snapshot, RenderProfile::default())?;
-
- assert!(matches!(commands.commands[0], RenderCommand::BeginFrame));
- assert!(matches!(commands.commands[2], RenderCommand::EndFrame));
- let RenderCommand::Draw(draw) = &commands.commands[1] else {
- panic!("expected draw");
- };
- assert_eq!(draw.id, DrawId(1));
- assert_eq!(draw.mesh, GpuMeshId(11));
- assert_eq!(draw.range, IndexRange { start: 0, count: 3 });
- Ok(())
- }
-
- #[test]
- fn material_index_maps_through_resolved_material_slots() -> Result<(), RenderError> {
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![snapshot_draw(2, RenderPhase::Opaque, 1, 10)],
- };
-
- let commands = build_commands(&snapshot, RenderProfile::default())?;
-
- let RenderCommand::Draw(draw) = &commands.commands[1] else {
- panic!("expected draw");
- };
- assert_eq!(draw.material, GpuMaterialId(37));
- Ok(())
- }
-
- #[test]
- fn node_transform_is_retained() -> Result<(), RenderError> {
- let mut draw = snapshot_draw(3, RenderPhase::Opaque, 0, 10);
- draw.transform[3] = 12.5;
- draw.transform[7] = -4.0;
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![draw],
- };
-
- let commands = build_commands(&snapshot, RenderProfile::default())?;
-
- let RenderCommand::Draw(draw) = &commands.commands[1] else {
- panic!("expected draw");
- };
- assert_eq!(draw.transform[3], 12.5);
- assert_eq!(draw.transform[7], -4.0);
- Ok(())
- }
-
- #[test]
- fn command_order_uses_phase_then_stable_key() -> Result<(), RenderError> {
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![
- snapshot_draw(3, RenderPhase::Transparent, 0, 0),
- snapshot_draw(2, RenderPhase::Opaque, 0, 20),
- snapshot_draw(1, RenderPhase::Opaque, 0, 10),
- ],
- };
-
- let commands = build_commands(&snapshot, RenderProfile::default())?;
- let capture = canonical_capture(&commands)?;
-
- assert_eq!(
- capture,
- b"B\nD,Opaque,1,11,31,0,10\nD,Opaque,2,12,31,0,20\nD,Transparent,3,13,31,0,0\nE\n"
- );
- Ok(())
- }
-
- #[test]
- fn command_capture_independent_of_snapshot_construction_order() -> Result<(), RenderError> {
- let forward = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![
- snapshot_draw(1, RenderPhase::Opaque, 0, 10),
- snapshot_draw(2, RenderPhase::Opaque, 1, 20),
- ],
- };
- let reverse = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![
- snapshot_draw(2, RenderPhase::Opaque, 1, 20),
- snapshot_draw(1, RenderPhase::Opaque, 0, 10),
- ],
- };
-
- assert_eq!(
- canonical_capture(&build_commands(&forward, RenderProfile::default())?)?,
- canonical_capture(&build_commands(&reverse, RenderProfile::default())?)?
- );
- Ok(())
- }
-
- #[test]
- fn invalid_range_returns_contextual_error() {
- let mut draw = snapshot_draw(9, RenderPhase::Opaque, 0, 10);
- draw.range = IndexRange { start: 4, count: 0 };
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![draw],
- };
-
- assert!(matches!(
- build_commands(&snapshot, RenderProfile::default()),
- Err(RenderError::InvalidDrawRange {
- draw_id: DrawId(9),
- stable_order: 10,
- start: 4,
- count: 0
- })
- ));
- }
-
- #[test]
- fn command_validation_rejects_bad_frame_framing() {
- let missing_begin = RenderCommandList {
- commands: vec![RenderCommand::EndFrame],
- };
- assert!(matches!(
- validate_command_list(&missing_begin),
- Err(RenderError::InvalidCommandStream {
- index: 0,
- message: "first command must be BeginFrame"
- })
- ));
-
- let nested = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::BeginFrame,
- RenderCommand::EndFrame,
- ],
- };
- assert!(matches!(
- validate_command_list(&nested),
- Err(RenderError::InvalidCommandStream {
- index: 1,
- message: "nested BeginFrame is not allowed"
- })
- ));
- }
-
- #[test]
- fn command_validation_rejects_nonfinite_transform_and_range_overflow() {
- let mut draw = snapshot_draw(10, RenderPhase::Opaque, 0, 10);
- draw.transform[5] = f32::NAN;
- let nonfinite = build_commands(
- &RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![draw],
- },
- RenderProfile::default(),
- );
- assert!(matches!(
- nonfinite,
- Err(RenderError::NonFiniteTransform {
- draw_id: DrawId(10),
- element: 5
- })
- ));
-
- let list = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(11),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(2),
- material: GpuMaterialId(3),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: identity_transform(),
- range: IndexRange {
- start: u32::MAX,
- count: 1,
- },
- stable_order: 4,
- }),
- RenderCommand::EndFrame,
- ],
- };
- assert!(matches!(
- validate_command_list(&list),
- Err(RenderError::IndexRangeOverflow {
- draw_id: DrawId(11),
- start: u32::MAX,
- count: 1
- })
- ));
- }
-
- #[test]
- fn command_validation_checks_order_and_resource_bounds() {
- let ordered = build_commands(
- &RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![snapshot_draw(1, RenderPhase::Opaque, 0, 10)],
- },
- RenderProfile::default(),
- )
- .expect("commands");
- assert!(matches!(
- validate_command_list_with_limits(
- &ordered,
- RenderValidationLimits {
- mesh_count: Some(5),
- index_count: Some(16)
- }
- ),
- Err(RenderError::MeshOutOfBounds {
- draw_id: DrawId(1),
- mesh: GpuMeshId(11),
- mesh_count: 5
- })
- ));
-
- let out_of_bounds = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(12),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(2),
- material: GpuMaterialId(3),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: identity_transform(),
- range: IndexRange {
- start: 14,
- count: 3,
- },
- stable_order: 4,
- }),
- RenderCommand::EndFrame,
- ],
- };
- assert!(matches!(
- validate_command_list_with_limits(
- &out_of_bounds,
- RenderValidationLimits {
- mesh_count: Some(5),
- index_count: Some(16)
- }
- ),
- Err(RenderError::IndexRangeOutOfBounds {
- draw_id: DrawId(12),
- index_count: 16,
- end: 17
- })
- ));
-
- let unordered = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(1),
- phase: RenderPhase::Transparent,
- object_id: None,
- mesh: GpuMeshId(1),
- material: GpuMaterialId(1),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: identity_transform(),
- range: IndexRange { start: 0, count: 3 },
- stable_order: 0,
- }),
- RenderCommand::Draw(DrawCommand {
- id: DrawId(2),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(1),
- material: GpuMaterialId(1),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: identity_transform(),
- range: IndexRange { start: 0, count: 3 },
- stable_order: 0,
- }),
- RenderCommand::EndFrame,
- ],
- };
- assert!(matches!(
- validate_command_list(&unordered),
- Err(RenderError::PhaseOrderViolation {
- draw_id: DrawId(2),
- previous: RenderPhase::Transparent,
- current: RenderPhase::Opaque
- })
- ));
- }
-
- #[test]
- fn render_error_display_is_actionable() {
- assert_eq!(
- RenderError::InvalidDrawRange {
- draw_id: DrawId(9),
- stable_order: 10,
- start: 4,
- count: 0
- }
- .to_string(),
- "draw 9 has invalid index range start=4 count=0 at stable order 10"
- );
- assert_eq!(
- RenderError::MaterialIndexOutOfBounds {
- draw_id: DrawId(7),
- material_index: 3,
- material_count: 2
- }
- .to_string(),
- "draw 7 references material index 3 but only 2 material slots are available"
- );
- }
-
- #[test]
- fn ui_phase_is_excluded_until_requested() -> Result<(), RenderError> {
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![
- snapshot_draw(1, RenderPhase::Opaque, 0, 10),
- snapshot_draw(2, RenderPhase::Ui, 0, 20),
- ],
- };
-
- let default_commands = build_commands(&snapshot, RenderProfile::default())?;
- let ui_commands = build_commands(&snapshot, RenderProfile { include_ui: true })?;
-
- assert_eq!(default_commands.commands.len(), 3);
- assert_eq!(ui_commands.commands.len(), 4);
- Ok(())
- }
}