feat(render): add deterministic pipeline state keys
This commit is contained in:
@@ -10,7 +10,8 @@ use crate::shader_manifest::{
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
use fparkan_platform::{DepthStencilSupport, RenderRequest};
|
use fparkan_platform::{DepthStencilSupport, RenderRequest};
|
||||||
use fparkan_render::{
|
use fparkan_render::{
|
||||||
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderCommand, RenderPhase,
|
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, LegacyPipelineState, RenderCommand,
|
||||||
|
RenderPhase,
|
||||||
};
|
};
|
||||||
use fparkan_render::{RenderBackend, RenderError};
|
use fparkan_render::{RenderBackend, RenderError};
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ fn planning_backend_tracks_render_request_and_simulated_present() -> Result<(),
|
|||||||
object_id: None,
|
object_id: None,
|
||||||
mesh: GpuMeshId(1),
|
mesh: GpuMeshId(1),
|
||||||
material: GpuMaterialId(2),
|
material: GpuMaterialId(2),
|
||||||
|
pipeline_key: LegacyPipelineState::default().into(),
|
||||||
transform: [1.0; 16],
|
transform: [1.0; 16],
|
||||||
range: IndexRange { start: 0, count: 3 },
|
range: IndexRange { start: 0, count: 3 },
|
||||||
stable_order: 7,
|
stable_order: 7,
|
||||||
@@ -75,6 +77,7 @@ fn frame_submission_plan_json_is_stable() -> Result<(), RenderError> {
|
|||||||
object_id: None,
|
object_id: None,
|
||||||
mesh: GpuMeshId(1),
|
mesh: GpuMeshId(1),
|
||||||
material: GpuMaterialId(2),
|
material: GpuMaterialId(2),
|
||||||
|
pipeline_key: LegacyPipelineState::default().into(),
|
||||||
transform: [1.0; 16],
|
transform: [1.0; 16],
|
||||||
range: IndexRange { start: 0, count: 3 },
|
range: IndexRange { start: 0, count: 3 },
|
||||||
stable_order: 7,
|
stable_order: 7,
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ fn render_snapshot_with_assets(
|
|||||||
mesh,
|
mesh,
|
||||||
material_slots: vec![material],
|
material_slots: vec![material],
|
||||||
material_index: 0,
|
material_index: 0,
|
||||||
|
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
||||||
transform: identity_transform(index_to_f32(index)),
|
transform: identity_transform(index_to_f32(index)),
|
||||||
range: IndexRange { start: 0, count: 3 },
|
range: IndexRange { start: 0, count: 3 },
|
||||||
stable_order,
|
stable_order,
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ impl ViewerModelService {
|
|||||||
mesh: GpuMeshId(1),
|
mesh: GpuMeshId(1),
|
||||||
material_slots: vec![GpuMaterialId(7)],
|
material_slots: vec![GpuMaterialId(7)],
|
||||||
material_index: 0,
|
material_index: 0,
|
||||||
|
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
||||||
transform: identity_transform(),
|
transform: identity_transform(),
|
||||||
range: IndexRange { start: 0, count: 3 },
|
range: IndexRange { start: 0, count: 3 },
|
||||||
stable_order: 0,
|
stable_order: 0,
|
||||||
|
|||||||
@@ -71,6 +71,93 @@ pub enum RenderPhase {
|
|||||||
Ui,
|
Ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fixed-function blend behaviour represented without a graphics API type.
|
||||||
|
///
|
||||||
|
/// This is a compatibility contract, not yet a decoded MAT0 mapping.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum LegacyBlendMode {
|
||||||
|
/// Do not blend the fragment with the existing colour.
|
||||||
|
#[default]
|
||||||
|
Opaque,
|
||||||
|
/// Blend using source alpha.
|
||||||
|
SourceAlpha,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Depth-buffer behaviour represented without a graphics API type.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum LegacyDepthMode {
|
||||||
|
/// No depth attachment is used.
|
||||||
|
#[default]
|
||||||
|
Disabled,
|
||||||
|
/// Test depth and write passing fragments.
|
||||||
|
TestWrite,
|
||||||
|
/// Test depth without modifying it.
|
||||||
|
TestReadOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triangle culling behaviour represented without a graphics API type.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum LegacyCullMode {
|
||||||
|
/// Keep both front- and back-facing triangles.
|
||||||
|
#[default]
|
||||||
|
Disabled,
|
||||||
|
/// Cull back-facing triangles.
|
||||||
|
BackFace,
|
||||||
|
/// Cull front-facing triangles.
|
||||||
|
FrontFace,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy fixed-function state that changes graphics-pipeline structure.
|
||||||
|
///
|
||||||
|
/// Alpha reference is deliberately not present: it is dynamic material data,
|
||||||
|
/// whereas this state records only whether an alpha-test shader variant is used.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct LegacyPipelineState {
|
||||||
|
/// Colour blend mode.
|
||||||
|
pub blend: LegacyBlendMode,
|
||||||
|
/// Depth test/write mode.
|
||||||
|
pub depth: LegacyDepthMode,
|
||||||
|
/// Face culling mode.
|
||||||
|
pub cull: LegacyCullMode,
|
||||||
|
/// Whether alpha-test shader logic is enabled.
|
||||||
|
pub alpha_test: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canonical, backend-neutral key for a graphics-pipeline variant.
|
||||||
|
///
|
||||||
|
/// The value is explicitly packed rather than hashed, so captures and caches
|
||||||
|
/// remain stable across processes and Rust toolchain updates.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct PipelineKey(u8);
|
||||||
|
|
||||||
|
impl PipelineKey {
|
||||||
|
/// Returns the canonical packed representation.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn packed(self) -> u8 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LegacyPipelineState> for PipelineKey {
|
||||||
|
fn from(state: LegacyPipelineState) -> Self {
|
||||||
|
let blend = match state.blend {
|
||||||
|
LegacyBlendMode::Opaque => 0,
|
||||||
|
LegacyBlendMode::SourceAlpha => 1,
|
||||||
|
};
|
||||||
|
let depth = match state.depth {
|
||||||
|
LegacyDepthMode::Disabled => 0,
|
||||||
|
LegacyDepthMode::TestWrite => 1,
|
||||||
|
LegacyDepthMode::TestReadOnly => 2,
|
||||||
|
};
|
||||||
|
let cull = match state.cull {
|
||||||
|
LegacyCullMode::Disabled => 0,
|
||||||
|
LegacyCullMode::BackFace => 1,
|
||||||
|
LegacyCullMode::FrontFace => 2,
|
||||||
|
};
|
||||||
|
Self(blend | (depth << 1) | (cull << 3) | (u8::from(state.alpha_test) << 5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Index range.
|
/// Index range.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub struct IndexRange {
|
pub struct IndexRange {
|
||||||
@@ -95,6 +182,8 @@ pub struct RenderSnapshotDraw {
|
|||||||
pub material_slots: Vec<GpuMaterialId>,
|
pub material_slots: Vec<GpuMaterialId>,
|
||||||
/// Batch material index into [`Self::material_slots`].
|
/// Batch material index into [`Self::material_slots`].
|
||||||
pub material_index: u16,
|
pub material_index: u16,
|
||||||
|
/// Fixed-function state resolved for this draw.
|
||||||
|
pub pipeline_state: LegacyPipelineState,
|
||||||
/// Node transform matrix, row-major.
|
/// Node transform matrix, row-major.
|
||||||
pub transform: [f32; 16],
|
pub transform: [f32; 16],
|
||||||
/// Index range.
|
/// Index range.
|
||||||
@@ -132,6 +221,8 @@ pub struct DrawCommand {
|
|||||||
pub mesh: GpuMeshId,
|
pub mesh: GpuMeshId,
|
||||||
/// Material.
|
/// Material.
|
||||||
pub material: GpuMaterialId,
|
pub material: GpuMaterialId,
|
||||||
|
/// Canonical graphics-pipeline variant.
|
||||||
|
pub pipeline_key: PipelineKey,
|
||||||
/// Transform matrix, row-major.
|
/// Transform matrix, row-major.
|
||||||
pub transform: [f32; 16],
|
pub transform: [f32; 16],
|
||||||
/// Index range.
|
/// Index range.
|
||||||
@@ -369,6 +460,7 @@ pub fn build_commands(
|
|||||||
object_id: draw.object_id,
|
object_id: draw.object_id,
|
||||||
mesh: draw.mesh,
|
mesh: draw.mesh,
|
||||||
material,
|
material,
|
||||||
|
pipeline_key: draw.pipeline_state.into(),
|
||||||
transform: draw.transform,
|
transform: draw.transform,
|
||||||
range: draw.range,
|
range: draw.range,
|
||||||
stable_order: draw.stable_order,
|
stable_order: draw.stable_order,
|
||||||
@@ -453,8 +545,13 @@ pub fn canonical_capture(commands: &RenderCommandList) -> Result<Vec<u8>, Render
|
|||||||
RenderCommand::Draw(draw) => {
|
RenderCommand::Draw(draw) => {
|
||||||
out.extend_from_slice(
|
out.extend_from_slice(
|
||||||
format!(
|
format!(
|
||||||
"D,{:?},{},{},{},{}\n",
|
"D,{:?},{},{},{},{},{}\n",
|
||||||
draw.phase, draw.id.0, draw.mesh.0, draw.material.0, draw.stable_order
|
draw.phase,
|
||||||
|
draw.id.0,
|
||||||
|
draw.mesh.0,
|
||||||
|
draw.material.0,
|
||||||
|
draw.pipeline_key.packed(),
|
||||||
|
draw.stable_order
|
||||||
)
|
)
|
||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
);
|
);
|
||||||
@@ -618,6 +715,7 @@ mod tests {
|
|||||||
mesh: GpuMeshId(10 + id),
|
mesh: GpuMeshId(10 + id),
|
||||||
material_slots: vec![GpuMaterialId(31), GpuMaterialId(37)],
|
material_slots: vec![GpuMaterialId(31), GpuMaterialId(37)],
|
||||||
material_index,
|
material_index,
|
||||||
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
transform: identity_transform(),
|
transform: identity_transform(),
|
||||||
range: IndexRange { start: 0, count: 3 },
|
range: IndexRange { start: 0, count: 3 },
|
||||||
stable_order,
|
stable_order,
|
||||||
@@ -635,6 +733,7 @@ mod tests {
|
|||||||
object_id: None,
|
object_id: None,
|
||||||
mesh: GpuMeshId(2),
|
mesh: GpuMeshId(2),
|
||||||
material: GpuMaterialId(3),
|
material: GpuMaterialId(3),
|
||||||
|
pipeline_key: LegacyPipelineState::default().into(),
|
||||||
transform: [0.0; 16],
|
transform: [0.0; 16],
|
||||||
range: IndexRange { start: 0, count: 3 },
|
range: IndexRange { start: 0, count: 3 },
|
||||||
stable_order: 4,
|
stable_order: 4,
|
||||||
@@ -644,10 +743,35 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
canonical_capture(&list).expect("capture"),
|
canonical_capture(&list).expect("capture"),
|
||||||
b"B\nD,Opaque,1,2,3,4\nE\n"
|
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]
|
#[test]
|
||||||
fn null_backend_validates_without_capture() {
|
fn null_backend_validates_without_capture() {
|
||||||
let mut backend = NullBackend;
|
let mut backend = NullBackend;
|
||||||
@@ -660,6 +784,7 @@ mod tests {
|
|||||||
object_id: None,
|
object_id: None,
|
||||||
mesh: GpuMeshId(2),
|
mesh: GpuMeshId(2),
|
||||||
material: GpuMaterialId(3),
|
material: GpuMaterialId(3),
|
||||||
|
pipeline_key: LegacyPipelineState::default().into(),
|
||||||
transform: [0.0; 16],
|
transform: [0.0; 16],
|
||||||
range: IndexRange { start: 0, count: 0 },
|
range: IndexRange { start: 0, count: 0 },
|
||||||
stable_order: 4,
|
stable_order: 4,
|
||||||
@@ -762,7 +887,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
capture,
|
capture,
|
||||||
b"B\nD,Opaque,1,11,31,10\nD,Opaque,2,12,31,20\nD,Transparent,3,13,31,0\nE\n"
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -868,6 +993,7 @@ mod tests {
|
|||||||
object_id: None,
|
object_id: None,
|
||||||
mesh: GpuMeshId(2),
|
mesh: GpuMeshId(2),
|
||||||
material: GpuMaterialId(3),
|
material: GpuMaterialId(3),
|
||||||
|
pipeline_key: LegacyPipelineState::default().into(),
|
||||||
transform: identity_transform(),
|
transform: identity_transform(),
|
||||||
range: IndexRange {
|
range: IndexRange {
|
||||||
start: u32::MAX,
|
start: u32::MAX,
|
||||||
@@ -922,6 +1048,7 @@ mod tests {
|
|||||||
object_id: None,
|
object_id: None,
|
||||||
mesh: GpuMeshId(2),
|
mesh: GpuMeshId(2),
|
||||||
material: GpuMaterialId(3),
|
material: GpuMaterialId(3),
|
||||||
|
pipeline_key: LegacyPipelineState::default().into(),
|
||||||
transform: identity_transform(),
|
transform: identity_transform(),
|
||||||
range: IndexRange {
|
range: IndexRange {
|
||||||
start: 14,
|
start: 14,
|
||||||
@@ -956,6 +1083,7 @@ mod tests {
|
|||||||
object_id: None,
|
object_id: None,
|
||||||
mesh: GpuMeshId(1),
|
mesh: GpuMeshId(1),
|
||||||
material: GpuMaterialId(1),
|
material: GpuMaterialId(1),
|
||||||
|
pipeline_key: LegacyPipelineState::default().into(),
|
||||||
transform: identity_transform(),
|
transform: identity_transform(),
|
||||||
range: IndexRange { start: 0, count: 3 },
|
range: IndexRange { start: 0, count: 3 },
|
||||||
stable_order: 0,
|
stable_order: 0,
|
||||||
@@ -966,6 +1094,7 @@ mod tests {
|
|||||||
object_id: None,
|
object_id: None,
|
||||||
mesh: GpuMeshId(1),
|
mesh: GpuMeshId(1),
|
||||||
material: GpuMaterialId(1),
|
material: GpuMaterialId(1),
|
||||||
|
pipeline_key: LegacyPipelineState::default().into(),
|
||||||
transform: identity_transform(),
|
transform: identity_transform(),
|
||||||
range: IndexRange { start: 0, count: 3 },
|
range: IndexRange { start: 0, count: 3 },
|
||||||
stable_order: 0,
|
stable_order: 0,
|
||||||
|
|||||||
@@ -829,6 +829,24 @@ draw_indexed(item.batch.base_vertex,
|
|||||||
item.batch.index_count);
|
item.batch.index_count);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Текущий контракт pipeline key
|
||||||
|
|
||||||
|
`fparkan-render` теперь переносит вместе с каждым backend-neutral draw
|
||||||
|
`LegacyPipelineState` и детерминированный `PipelineKey`. Ключ не хешируется:
|
||||||
|
он явно упаковывает blend mode, depth mode, cull mode и флаг alpha-test, поэтому
|
||||||
|
одинаковые snapshots дают одинаковый capture и будущий Vulkan cache не зависит
|
||||||
|
от версии Rust или процесса. Alpha reference намеренно не входит в key: это
|
||||||
|
dynamic material constant, а не структурный вариант graphics pipeline.
|
||||||
|
|
||||||
|
Пока текущий статический Vulkan viewer создаёт только базовое состояние
|
||||||
|
`opaque + depth disabled + cull disabled + alpha-test disabled`; ключ уже
|
||||||
|
передаётся через render command и canonical capture, но Vulkan pipeline cache
|
||||||
|
ещё не выбирает variants по нему. Также не установлен источник значений для
|
||||||
|
Batch20/MAT0: их поля нельзя объявлять blend/depth/cull mapping без dynamic
|
||||||
|
capture или дополнительного дизассемблирования. Поэтому этот контракт —
|
||||||
|
подготовленная граница совместимости, а не заявление о готовой parity
|
||||||
|
fixed-function state.
|
||||||
|
|
||||||
После последнего world pass renderer закрывает сцену и выводит back buffer.
|
После последнего world pass renderer закрывает сцену и выводит back buffer.
|
||||||
World3D снимает `in_render`, восстанавливает временный viewport state и вызывает
|
World3D снимает `in_render`, восстанавливает временный viewport state и вызывает
|
||||||
`on_end_render` у active objects. Только после этого допустимо освобождать
|
`on_end_render` у active objects. Только после этого допустимо освобождать
|
||||||
|
|||||||
Reference in New Issue
Block a user