feat: compile multisampled graph attachments
Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -723,9 +723,6 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
if p.texture.mip_level_count != 1 {
|
||||
return Err(unsupported("mipLevelCount"));
|
||||
}
|
||||
if p.texture.sample_count != 1 {
|
||||
return Err(unsupported("sampleCount"));
|
||||
}
|
||||
let depth_or_array_layers = match &p.texture.extent {
|
||||
TextureExtent::Absolute {
|
||||
depth_or_array_layers,
|
||||
@@ -1357,49 +1354,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
});
|
||||
}
|
||||
|
||||
// Every texture reader must execute before a successor overwrites the
|
||||
// physical allocation backing the older symbolic version.
|
||||
let mut reachability = HashMap::new();
|
||||
for (i, contract) in contracts.iter().enumerate() {
|
||||
if !live.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
for input in contract
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|input| matches!(input.role, InputRole::SampledTexture))
|
||||
{
|
||||
let key = bound[i][input.name].producer;
|
||||
if !version_of.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
let Some(next_indices) = transitions_by_target.get(&TransitionTargetKey::Authored(key))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let [next_index] = next_indices.as_slice() else {
|
||||
continue;
|
||||
};
|
||||
let next = transitions[*next_index];
|
||||
if i != next.writer_node
|
||||
&& !reaches(
|
||||
i,
|
||||
next.writer_node,
|
||||
&outgoing_edges,
|
||||
&edges,
|
||||
&live,
|
||||
&mut reachability,
|
||||
)
|
||||
{
|
||||
return Err(error(
|
||||
"GRAPH_RESOURCE_VERSION_INVALID",
|
||||
"older texture version may be read after its successor",
|
||||
format!("nodes[{i}].inputs.{}", input.name),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Same-pass hazards are global and precede every duplicate-writer diagnostic.
|
||||
for i in 0..graph.nodes.len() {
|
||||
if !live.contains(&i) {
|
||||
@@ -1462,6 +1416,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let Some(&(opposite_family, _, _)) = version_of.get(&opposite_output) else {
|
||||
continue;
|
||||
};
|
||||
let opposite_descriptor =
|
||||
known_family_descriptor(opposite_family, &families, &default_roots);
|
||||
let extent = if opposite_family as usize >= authored_family_count
|
||||
&& default_roots
|
||||
.get(opposite_family as usize - authored_family_count)
|
||||
@@ -1469,8 +1425,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
{
|
||||
Some(full_extent.clone())
|
||||
} else {
|
||||
known_family_descriptor(opposite_family, &families, &default_roots)
|
||||
.map(|descriptor| descriptor.extent.clone())
|
||||
opposite_descriptor.map(|descriptor| descriptor.extent.clone())
|
||||
};
|
||||
if let Some(extent) = extent {
|
||||
default_roots[index].descriptor =
|
||||
@@ -1479,7 +1434,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
format: default_roots[index].format,
|
||||
extent,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
sample_count: opposite_descriptor
|
||||
.map_or(1, |descriptor| descriptor.sample_count),
|
||||
view_formats: vec![],
|
||||
});
|
||||
changed = true;
|
||||
@@ -1490,6 +1446,100 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
}
|
||||
|
||||
// Classify sampled edges before descriptor and stale-version validation. A
|
||||
// demanded resolve is keyed by the exact symbolic output, not its family.
|
||||
let mut resolve_demands = BTreeSet::<OutputKey>::new();
|
||||
for edge in &edges {
|
||||
if !live.contains(&edge.to_node)
|
||||
|| contracts[edge.to_node].inputs[edge.consumer_input_ordinal as usize].role
|
||||
!= InputRole::SampledTexture
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let key = OutputKey(edge.from_node, edge.producer_output_ordinal);
|
||||
let Some(&(family, _, _)) = version_of.get(&key) else {
|
||||
if matches!(resolved.get(&key), Some(ResolvedTransition::Cyclic)) {
|
||||
continue;
|
||||
}
|
||||
if source_family
|
||||
.get(&TransitionTargetKey::Authored(key))
|
||||
.and_then(|&family| known_family_descriptor(family, &families, &default_roots))
|
||||
.is_some_and(|descriptor| descriptor.sample_count == 4)
|
||||
{
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"unproduced multisampled texture cannot be sampled",
|
||||
format!("nodes[{}].inputs.{}", edge.to_node, edge.to_socket),
|
||||
));
|
||||
}
|
||||
continue;
|
||||
};
|
||||
let Some(descriptor) = known_family_descriptor(family, &families, &default_roots) else {
|
||||
continue;
|
||||
};
|
||||
if descriptor.sample_count == 1 {
|
||||
continue;
|
||||
}
|
||||
if descriptor.format == TextureFormat::Depth32Float {
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"multisampled depth texture cannot be sampled",
|
||||
format!("nodes[{}].inputs.{}", edge.to_node, edge.to_socket),
|
||||
));
|
||||
}
|
||||
if contracts[edge.from_node].key != "pipeline" || edge.producer_output_ordinal != 0 {
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"multisampled color texture is not a produced pipeline color",
|
||||
format!("nodes[{}].inputs.{}", edge.to_node, edge.to_socket),
|
||||
));
|
||||
}
|
||||
resolve_demands.insert(key);
|
||||
}
|
||||
|
||||
// Every ordinary texture reader must execute before a successor overwrites
|
||||
// its allocation. Resolve demands read the attachment during its producer.
|
||||
let mut reachability = HashMap::new();
|
||||
for (i, contract) in contracts.iter().enumerate() {
|
||||
if !live.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
for input in contract
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|input| input.role == InputRole::SampledTexture)
|
||||
{
|
||||
let key = bound[i][input.name].producer;
|
||||
if resolve_demands.contains(&key) || !version_of.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
let Some(next_indices) = transitions_by_target.get(&TransitionTargetKey::Authored(key))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let [next_index] = next_indices.as_slice() else {
|
||||
continue;
|
||||
};
|
||||
let next = transitions[*next_index];
|
||||
if i != next.writer_node
|
||||
&& !reaches(
|
||||
i,
|
||||
next.writer_node,
|
||||
&outgoing_edges,
|
||||
&edges,
|
||||
&live,
|
||||
&mut reachability,
|
||||
)
|
||||
{
|
||||
return Err(error(
|
||||
"GRAPH_RESOURCE_VERSION_INVALID",
|
||||
"older texture version may be read after its successor",
|
||||
format!("nodes[{i}].inputs.{}", input.name),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate every independently resolved attachment before graph cycle reporting.
|
||||
for i in 0..graph.nodes.len() {
|
||||
if !live.contains(&i) || contracts[i].key != "pipeline" {
|
||||
@@ -1504,7 +1554,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let ok_depth = dd.is_none_or(|dd| {
|
||||
dd.dimension == TextureDimension::D2
|
||||
&& dd.format == TextureFormat::Depth32Float
|
||||
&& dd.sample_count == 1
|
||||
&& matches!(dd.sample_count, 1 | 4)
|
||||
&& dd.mip_level_count == 1
|
||||
&& dd.view_formats.is_empty()
|
||||
&& extent_layers(&dd.extent) == 1
|
||||
@@ -1512,7 +1562,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let ok_color = cd.is_none_or(|cd| {
|
||||
cd.dimension == TextureDimension::D2
|
||||
&& cd.format != TextureFormat::Depth32Float
|
||||
&& cd.sample_count == 1
|
||||
&& matches!(cd.sample_count, 1 | 4)
|
||||
&& cd.mip_level_count == 1
|
||||
&& cd.view_formats.is_empty()
|
||||
&& extent_layers(&cd.extent) == 1
|
||||
@@ -1588,7 +1638,9 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
_ => None,
|
||||
};
|
||||
let source_ok = source_descriptor.is_none_or(|descriptor| {
|
||||
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(descriptor)
|
||||
descriptor.format == TextureFormat::Rgba16Float
|
||||
&& (is_single_view_d2(descriptor)
|
||||
|| resolve_demands.contains(&bound[i]["source"].producer))
|
||||
});
|
||||
let target_ok = target_descriptor.is_none_or(|descriptor| {
|
||||
is_single_view_d2(descriptor)
|
||||
@@ -1605,7 +1657,9 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
});
|
||||
let bloom_ok = bloom_descriptor.is_none_or(|descriptor| {
|
||||
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(descriptor)
|
||||
descriptor.format == TextureFormat::Rgba16Float
|
||||
&& (is_single_view_d2(descriptor)
|
||||
|| resolve_demands.contains(&bound[i]["bloom"].producer))
|
||||
});
|
||||
let extent_ok = match contracts[i].fullscreen_policy {
|
||||
Some(FullscreenPolicy::Copy)
|
||||
@@ -1661,7 +1715,11 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let NormalizedParameters::FrameOut { dynamic_range, .. } = ¶ms[i] else {
|
||||
unreachable!()
|
||||
};
|
||||
if !frame_out_source_compatible(descriptor, dynamic_range) {
|
||||
let mut effective = descriptor.clone();
|
||||
if resolve_demands.contains(&key) {
|
||||
effective.sample_count = 1;
|
||||
}
|
||||
if !frame_out_source_compatible(&effective, dynamic_range) {
|
||||
let message = match dynamic_range {
|
||||
FrameDynamicRange::Hdr { .. } => "HDR frame output requires rgba16_float",
|
||||
FrameDynamicRange::Sdr => {
|
||||
@@ -1940,6 +1998,87 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
},
|
||||
});
|
||||
}
|
||||
// A multisampled pipeline color remains the authored attachment version. Sampling
|
||||
// that exact version instead addresses one compiler-owned fixed-function resolve.
|
||||
let mut resolve_resources = BTreeMap::new();
|
||||
for producer in resolve_demands {
|
||||
let (family, _, _) = version_of[&producer];
|
||||
let descriptor = family_descriptor(&families[family as usize]);
|
||||
let source_resource = output_ids[&producer];
|
||||
let root = resources.len() as u32;
|
||||
let resource = root + 1;
|
||||
let family_id = families.len() as u32;
|
||||
let mut resolved_descriptor = descriptor.clone();
|
||||
resolved_descriptor.sample_count = 1;
|
||||
resolve_resources.insert(producer, resource);
|
||||
resources.push(CompiledResource {
|
||||
original_node_index: producer.0 as u32,
|
||||
origin: ResourceOrigin::CompilerColorResolve {
|
||||
producer_node_index: producer.0 as u32,
|
||||
output_ordinal: producer.1,
|
||||
source_resource,
|
||||
},
|
||||
semantic_type: SemanticType::Texture,
|
||||
producer_execution: None,
|
||||
lifetime: None,
|
||||
plan: ResourcePlan::TextureSource {
|
||||
family: family_id,
|
||||
residency: TextureResidency::Transient,
|
||||
descriptor: resolved_descriptor.clone(),
|
||||
},
|
||||
});
|
||||
resources.push(CompiledResource {
|
||||
original_node_index: producer.0 as u32,
|
||||
origin: ResourceOrigin::CompilerColorResolve {
|
||||
producer_node_index: producer.0 as u32,
|
||||
output_ordinal: producer.1,
|
||||
source_resource,
|
||||
},
|
||||
semantic_type: SemanticType::Texture,
|
||||
producer_execution: None,
|
||||
lifetime: None,
|
||||
plan: ResourcePlan::Texture {
|
||||
family: family_id,
|
||||
version: 0,
|
||||
target: root,
|
||||
initialized: true,
|
||||
stored: true,
|
||||
allocation: None,
|
||||
},
|
||||
});
|
||||
families.push(TextureFamily {
|
||||
id: family_id,
|
||||
key: TextureFamilyKey {
|
||||
source_node: producer.0 as u32,
|
||||
source_socket: producer.1,
|
||||
},
|
||||
source: TextureFamilySource::CompilerColorResolve {
|
||||
resource: root,
|
||||
descriptor: resolved_descriptor,
|
||||
producer_node_index: producer.0 as u32,
|
||||
output_ordinal: producer.1,
|
||||
source_resource,
|
||||
},
|
||||
lifetime: Lifetime {
|
||||
first_use: 0,
|
||||
last_use: 0,
|
||||
},
|
||||
versions: vec![TextureVersion {
|
||||
version: 0,
|
||||
resource,
|
||||
target: root,
|
||||
initialized: true,
|
||||
stored: true,
|
||||
lifetime: Lifetime {
|
||||
first_use: 0,
|
||||
last_use: 0,
|
||||
},
|
||||
}],
|
||||
usage: vec![],
|
||||
allocation: None,
|
||||
aliasable: false,
|
||||
});
|
||||
}
|
||||
let mut executions = Vec::new();
|
||||
for &i in &order {
|
||||
if matches!(
|
||||
@@ -1948,12 +2087,32 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let input_resource = |s: &str| output_ids[&bound[i][s].producer];
|
||||
let input_resource = |s: &str| {
|
||||
let key = bound[i][s].producer;
|
||||
let resource = output_ids[&key];
|
||||
if contracts[i]
|
||||
.inputs
|
||||
.iter()
|
||||
.any(|input| input.name == s && input.role == InputRole::SampledTexture)
|
||||
{
|
||||
resolve_resources.get(&key).copied().unwrap_or(resource)
|
||||
} else {
|
||||
resource
|
||||
}
|
||||
};
|
||||
let mut inputs = Vec::new();
|
||||
for (input_ordinal, s) in contracts[i].inputs.iter().enumerate() {
|
||||
if s.role != InputRole::Expression {
|
||||
let resource = if let Some(b) = bound[i].get(s.name) {
|
||||
output_ids[&b.producer]
|
||||
let resource = output_ids[&b.producer];
|
||||
if s.role == InputRole::SampledTexture {
|
||||
resolve_resources
|
||||
.get(&b.producer)
|
||||
.copied()
|
||||
.unwrap_or(resource)
|
||||
} else {
|
||||
resource
|
||||
}
|
||||
} else if s.default_policy == InputDefaultPolicy::CompilerTexture {
|
||||
let key = TransitionTargetKey::CompilerDefaultInput {
|
||||
owner_node: i,
|
||||
@@ -1998,6 +2157,38 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
};
|
||||
let first_color = version_of[&OutputKey(i, 0)].1 == 0;
|
||||
let first_depth = version_of[&OutputKey(i, 1)].1 == 0;
|
||||
let resolve_target = resolve_resources.get(&OutputKey(i, 0)).copied();
|
||||
let source_store = if resolve_target.is_some()
|
||||
&& !edges.iter().any(|edge| {
|
||||
edge.from_node == i
|
||||
&& edge.producer_output_ordinal == 0
|
||||
&& matches!(
|
||||
contracts[edge.to_node].inputs
|
||||
[edge.consumer_input_ordinal as usize]
|
||||
.role,
|
||||
InputRole::ColorTarget { .. }
|
||||
)
|
||||
}) {
|
||||
StoreOp::Discard
|
||||
} else {
|
||||
StoreOp::Store
|
||||
};
|
||||
let stored = source_store == StoreOp::Store;
|
||||
if let ResourcePlan::Texture {
|
||||
family,
|
||||
version,
|
||||
stored: resource_stored,
|
||||
..
|
||||
} = &mut resources[color as usize].plan
|
||||
{
|
||||
*resource_stored = stored;
|
||||
if let Some(texture_version) = families
|
||||
.get_mut(*family as usize)
|
||||
.and_then(|family| family.versions.get_mut(*version as usize))
|
||||
{
|
||||
texture_version.stored = stored;
|
||||
}
|
||||
}
|
||||
let cl = if first_color {
|
||||
NormalizedColorLoad::Clear { value: clear }
|
||||
} else {
|
||||
@@ -2021,7 +2212,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
mode: AccessMode::ColorAttachment {
|
||||
location: 0,
|
||||
load: cl,
|
||||
store: StoreOp::Store,
|
||||
store: source_store,
|
||||
full_overwrite: first_color,
|
||||
},
|
||||
});
|
||||
@@ -2034,12 +2225,23 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
full_overwrite: first_depth,
|
||||
},
|
||||
});
|
||||
if let Some(resource) = resolve_target {
|
||||
accesses.push(CompiledAccess {
|
||||
socket: "colorResolve".into(),
|
||||
resource,
|
||||
mode: AccessMode::ColorResolve {
|
||||
source: color,
|
||||
location: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
ExecutionKind::Render {
|
||||
color_attachments: vec![ColorAttachmentPlan {
|
||||
resource: color,
|
||||
resolve_target,
|
||||
location: 0,
|
||||
load: cl,
|
||||
store: StoreOp::Store,
|
||||
store: source_store,
|
||||
}],
|
||||
depth_stencil: Some(DepthStencilAttachmentPlan {
|
||||
resource: depth,
|
||||
@@ -2077,6 +2279,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
ExecutionKind::Render {
|
||||
color_attachments: vec![ColorAttachmentPlan {
|
||||
resource: color,
|
||||
resolve_target: None,
|
||||
location: 0,
|
||||
load,
|
||||
store: StoreOp::Store,
|
||||
@@ -2374,6 +2577,11 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
for o in &e.outputs {
|
||||
resources[o.resource as usize].producer_execution = Some(ordinal as u32);
|
||||
}
|
||||
for access in &e.accesses {
|
||||
if matches!(access.mode, AccessMode::ColorResolve { .. }) {
|
||||
resources[access.resource as usize].producer_execution = Some(ordinal as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dense lifetimes touch bindings, outputs, and accesses.
|
||||
for (ordinal, e) in executions.iter().enumerate() {
|
||||
@@ -2414,7 +2622,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
TextureFamilySource::AuthoredTexture { residency, .. } => {
|
||||
residency == TextureResidency::Transient
|
||||
}
|
||||
TextureFamilySource::CompilerDefaultInput { .. } => true,
|
||||
TextureFamilySource::CompilerDefaultInput { .. }
|
||||
| TextureFamilySource::CompilerColorResolve { .. } => true,
|
||||
};
|
||||
f.aliasable = transient && f.versions.iter().all(|v| v.initialized);
|
||||
}
|
||||
@@ -2457,7 +2666,8 @@ fn extent_layers(e: &NormalizedTextureExtent) -> u32 {
|
||||
pub(super) fn family_descriptor(family: &TextureFamily) -> &NormalizedTextureDescriptor {
|
||||
match &family.source {
|
||||
TextureFamilySource::AuthoredTexture { descriptor, .. }
|
||||
| TextureFamilySource::CompilerDefaultInput { descriptor, .. } => descriptor,
|
||||
| TextureFamilySource::CompilerDefaultInput { descriptor, .. }
|
||||
| TextureFamilySource::CompilerColorResolve { descriptor, .. } => descriptor,
|
||||
}
|
||||
}
|
||||
pub(super) fn is_single_view_d2(descriptor: &NormalizedTextureDescriptor) -> bool {
|
||||
@@ -2504,6 +2714,9 @@ pub(super) fn texture_usage(
|
||||
AccessMode::DepthAttachment { .. } => {
|
||||
u.insert(TextureUsage::DepthAttachment);
|
||||
}
|
||||
AccessMode::ColorResolve { .. } => {
|
||||
u.insert(TextureUsage::ColorAttachment);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,9 +197,9 @@ macro_rules! ex {
|
||||
|
||||
pub static CONTRACTS: &[Contract] = &[
|
||||
c!("mesh", 2, Source, NONE_I, MESH_O, false, None),
|
||||
c!("texture", 1, Source, NONE_I, TEXTURE_O, false, None),
|
||||
c!("texture", 2, Source, NONE_I, TEXTURE_O, false, None),
|
||||
c!("frustum_cull", 2, Expression, CULL_I, CULL_O, false, None),
|
||||
c!("pipeline", 3, Render, PIPE_I, PIPE_O, false, None),
|
||||
c!("pipeline", 4, Render, PIPE_I, PIPE_O, false, None),
|
||||
ex!("and", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
|
||||
ex!("or", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
|
||||
ex!("not", ins!("operand":Bool), outs!("value":Bool)),
|
||||
|
||||
@@ -45,6 +45,11 @@ pub enum ResourceOrigin {
|
||||
socket: String,
|
||||
role: CompilerTextureRole,
|
||||
},
|
||||
CompilerColorResolve {
|
||||
producer_node_index: u32,
|
||||
output_ordinal: u16,
|
||||
source_resource: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
@@ -116,6 +121,7 @@ pub enum ExecutionKind {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ColorAttachmentPlan {
|
||||
pub resource: u32,
|
||||
pub resolve_target: Option<u32>,
|
||||
pub location: u32,
|
||||
pub load: NormalizedColorLoad,
|
||||
pub store: StoreOp,
|
||||
@@ -169,6 +175,10 @@ pub enum AccessMode {
|
||||
},
|
||||
IndirectRead,
|
||||
SampledTexture,
|
||||
ColorResolve {
|
||||
source: u32,
|
||||
location: u32,
|
||||
},
|
||||
ColorAttachment {
|
||||
location: u32,
|
||||
load: NormalizedColorLoad,
|
||||
@@ -375,6 +385,13 @@ pub enum TextureFamilySource {
|
||||
role: CompilerTextureRole,
|
||||
descriptor: NormalizedTextureDescriptor,
|
||||
},
|
||||
CompilerColorResolve {
|
||||
resource: u32,
|
||||
descriptor: NormalizedTextureDescriptor,
|
||||
producer_node_index: u32,
|
||||
output_ordinal: u16,
|
||||
source_resource: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
|
||||
@@ -396,7 +396,8 @@ fn texture_descriptor<'a>(
|
||||
};
|
||||
match &graph.texture_families.get(family as usize)?.source {
|
||||
TextureFamilySource::AuthoredTexture { descriptor, .. }
|
||||
| TextureFamilySource::CompilerDefaultInput { descriptor, .. } => Some(descriptor),
|
||||
| TextureFamilySource::CompilerDefaultInput { descriptor, .. }
|
||||
| TextureFamilySource::CompilerColorResolve { descriptor, .. } => Some(descriptor),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,6 +420,7 @@ fn single_view_d2(d: &NormalizedTextureDescriptor) -> bool {
|
||||
|
||||
fn validate_fullscreen_execution(
|
||||
graph: &CompiledGraph,
|
||||
producers: &[Option<u32>],
|
||||
i: usize,
|
||||
execution: &CompiledExecution,
|
||||
contract: &Contract,
|
||||
@@ -624,13 +626,8 @@ fn validate_fullscreen_execution(
|
||||
path("inputs"),
|
||||
));
|
||||
}
|
||||
let producer = graph.executions[..i].iter().position(|candidate| {
|
||||
candidate
|
||||
.outputs
|
||||
.iter()
|
||||
.any(|value| value.resource == input.resource)
|
||||
});
|
||||
if producer.is_none() {
|
||||
let producer = producers.get(input.resource as usize).copied().flatten();
|
||||
if !producer.is_some_and(|producer| producer < i as u32) {
|
||||
return Err(invalid(
|
||||
"fullscreen sampled producer must precede execution",
|
||||
path("inputs"),
|
||||
@@ -682,6 +679,115 @@ fn validate_fullscreen_execution(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_pipeline_resolve(
|
||||
graph: &CompiledGraph,
|
||||
producers: &[Option<u32>],
|
||||
family_index: usize,
|
||||
family: &TextureFamily,
|
||||
) -> Result<(), GraphError> {
|
||||
let TextureFamilySource::CompilerColorResolve {
|
||||
resource: root,
|
||||
descriptor,
|
||||
producer_node_index,
|
||||
output_ordinal,
|
||||
source_resource,
|
||||
} = &family.source
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let path = || format!("textureFamilies[{family_index}].source");
|
||||
let source = graph
|
||||
.resources
|
||||
.get(*source_resource as usize)
|
||||
.ok_or_else(|| invalid("resolve source is out of bounds", path()))?;
|
||||
let producer_index = producers
|
||||
.get(*source_resource as usize)
|
||||
.copied()
|
||||
.flatten()
|
||||
.ok_or_else(|| invalid("resolve source producer is missing", path()))?
|
||||
as usize;
|
||||
let producer = graph
|
||||
.executions
|
||||
.get(producer_index)
|
||||
.ok_or_else(|| invalid("resolve producer is out of bounds", path()))?;
|
||||
let source_family_id = match source.plan {
|
||||
ResourcePlan::Texture { family, .. } => family,
|
||||
_ => return Err(invalid("resolve source is not a texture version", path())),
|
||||
};
|
||||
let source_family = graph
|
||||
.texture_families
|
||||
.get(source_family_id as usize)
|
||||
.ok_or_else(|| invalid("resolve source family is out of bounds", path()))?;
|
||||
let source_descriptor = super::compiler::family_descriptor(source_family);
|
||||
let expected_descriptor = NormalizedTextureDescriptor {
|
||||
sample_count: 1,
|
||||
..source_descriptor.clone()
|
||||
};
|
||||
let origin_matches = |origin: &ResourceOrigin| {
|
||||
matches!(origin,
|
||||
ResourceOrigin::CompilerColorResolve {
|
||||
producer_node_index: node,
|
||||
output_ordinal: output,
|
||||
source_resource: source,
|
||||
} if node == producer_node_index && output == output_ordinal && source == source_resource)
|
||||
};
|
||||
let [version] = family.versions.as_slice() else {
|
||||
return Err(invalid("resolve family must have one version", path()));
|
||||
};
|
||||
let output = graph
|
||||
.resources
|
||||
.get(version.resource as usize)
|
||||
.ok_or_else(|| invalid("resolve output is out of bounds", path()))?;
|
||||
let exact_output = producer
|
||||
.outputs
|
||||
.get(*output_ordinal as usize)
|
||||
.is_some_and(|value| value.socket == "color" && value.resource == *source_resource)
|
||||
&& *output_ordinal == 0
|
||||
&& producer
|
||||
.outputs
|
||||
.iter()
|
||||
.filter(|value| value.resource == *source_resource)
|
||||
.count()
|
||||
== 1;
|
||||
let exact_access = producer
|
||||
.accesses
|
||||
.iter()
|
||||
.filter(|access| {
|
||||
matches!(access.mode,
|
||||
AccessMode::ColorResolve { source, location: 0 } if source == *source_resource)
|
||||
&& access.resource == version.resource
|
||||
&& access.socket == "colorResolve"
|
||||
})
|
||||
.count()
|
||||
== 1
|
||||
&& producer
|
||||
.accesses
|
||||
.iter()
|
||||
.filter(|access| matches!(access.mode, AccessMode::ColorResolve { .. }))
|
||||
.count()
|
||||
== 1;
|
||||
if producer.executor.key != "pipeline"
|
||||
|| producer.original_node_index != *producer_node_index
|
||||
|| !exact_output
|
||||
|| !matches!(&source.origin,
|
||||
ResourceOrigin::AuthoredOutput { node, socket, output_ordinal: 0 }
|
||||
if node == &producer.id && socket == "color")
|
||||
|| source.original_node_index != *producer_node_index
|
||||
|| !matches!(graph.resources.get(*root as usize), Some(CompiledResource { plan: ResourcePlan::TextureSource { .. }, origin, .. }) if origin_matches(origin))
|
||||
|| !origin_matches(&output.origin)
|
||||
|| version.version != 0
|
||||
|| version.target != *root
|
||||
|| output.producer_execution != Some(producer_index as u32)
|
||||
|| !exact_access
|
||||
|| family.id == source_family_id
|
||||
|| family.allocation == source_family.allocation
|
||||
|| descriptor != &expected_descriptor
|
||||
{
|
||||
return Err(invalid("pipeline color resolve is not canonical", path()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
fn texture_family_for_resource<'a>(
|
||||
graph: &'a CompiledGraph,
|
||||
@@ -722,6 +828,20 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
|
||||
let mut producers = vec![None; graph.resources.len()];
|
||||
let mut uses = vec![BTreeSet::new(); graph.resources.len()];
|
||||
fn claim_producer(
|
||||
producers: &mut [Option<u32>],
|
||||
resource: u32,
|
||||
execution: u32,
|
||||
path: String,
|
||||
) -> Result<(), GraphError> {
|
||||
let producer = producers
|
||||
.get_mut(resource as usize)
|
||||
.ok_or_else(|| invalid("execution producer is out of bounds", path.clone()))?;
|
||||
if producer.replace(execution).is_some() {
|
||||
return Err(invalid("resource has duplicate producers", path));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
for (i, execution) in graph.executions.iter().enumerate() {
|
||||
let contract = contract(&execution.executor.key).expect("supported executor has contract");
|
||||
if execution.executor.version != contract.version {
|
||||
@@ -731,18 +851,12 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
));
|
||||
}
|
||||
for output in &execution.outputs {
|
||||
let producer = producers.get_mut(output.resource as usize).ok_or_else(|| {
|
||||
invalid(
|
||||
"execution output is out of bounds",
|
||||
format!("executions[{i}].outputs"),
|
||||
)
|
||||
})?;
|
||||
if producer.replace(i as u32).is_some() {
|
||||
return Err(invalid(
|
||||
"resource has duplicate producers",
|
||||
format!("executions[{i}].outputs"),
|
||||
));
|
||||
}
|
||||
claim_producer(
|
||||
&mut producers,
|
||||
output.resource,
|
||||
i as u32,
|
||||
format!("executions[{i}].outputs"),
|
||||
)?;
|
||||
}
|
||||
let mut referenced = HashSet::new();
|
||||
for input in &execution.inputs {
|
||||
@@ -763,8 +877,16 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
));
|
||||
}
|
||||
referenced.insert(access.resource);
|
||||
if matches!(access.mode, AccessMode::ColorResolve { .. }) {
|
||||
claim_producer(
|
||||
&mut producers,
|
||||
access.resource,
|
||||
i as u32,
|
||||
format!("executions[{i}].accesses"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
validate_fullscreen_execution(graph, i, execution, contract)?;
|
||||
validate_fullscreen_execution(graph, &producers, i, execution, contract)?;
|
||||
for resource in referenced {
|
||||
uses.get_mut(resource as usize)
|
||||
.ok_or_else(|| {
|
||||
@@ -812,6 +934,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
let mut family_keys = HashSet::new();
|
||||
let mut source_claims = vec![0u8; graph.resources.len()];
|
||||
for (fi, family) in graph.texture_families.iter().enumerate() {
|
||||
validate_pipeline_resolve(graph, &producers, fi, family)?;
|
||||
if family.id as usize != fi || !family_keys.insert(family.key.clone()) {
|
||||
return Err(invalid(
|
||||
"texture family id/key is not unique and canonical",
|
||||
@@ -829,6 +952,11 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
descriptor,
|
||||
..
|
||||
} => (*resource, TextureResidency::Transient, descriptor),
|
||||
TextureFamilySource::CompilerColorResolve {
|
||||
resource,
|
||||
descriptor,
|
||||
..
|
||||
} => (*resource, TextureResidency::Transient, descriptor),
|
||||
};
|
||||
let source_resource = graph.resources.get(source as usize).ok_or_else(|| {
|
||||
invalid(
|
||||
@@ -894,7 +1022,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
== 1
|
||||
})
|
||||
&& owner_executions[0].executor.key == "pipeline"
|
||||
&& owner_executions[0].executor.version == 3
|
||||
&& owner_executions[0].executor.version == 4
|
||||
&& graph
|
||||
.executions
|
||||
.iter()
|
||||
@@ -926,6 +1054,26 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
"depthTarget"
|
||||
}
|
||||
}
|
||||
(
|
||||
TextureFamilySource::CompilerColorResolve {
|
||||
producer_node_index,
|
||||
output_ordinal,
|
||||
source_resource: resolve_source,
|
||||
..
|
||||
},
|
||||
ResourceOrigin::CompilerColorResolve {
|
||||
producer_node_index: origin_node,
|
||||
output_ordinal: origin_output,
|
||||
source_resource: origin_source,
|
||||
},
|
||||
) => {
|
||||
*producer_node_index == *origin_node
|
||||
&& *output_ordinal == *origin_output
|
||||
&& *resolve_source == *origin_source
|
||||
&& source_resource.semantic_type == SemanticType::Texture
|
||||
&& family.key.source_node == *producer_node_index
|
||||
&& family.key.source_socket == *output_ordinal
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !origin_ok {
|
||||
@@ -950,7 +1098,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
TextureFormat::Depth32Float
|
||||
}
|
||||
&& descriptor.mip_level_count == 1
|
||||
&& descriptor.sample_count == 1
|
||||
&& matches!(descriptor.sample_count, 1 | 4)
|
||||
&& descriptor.view_formats.is_empty()
|
||||
&& matches!(
|
||||
descriptor.extent,
|
||||
@@ -975,7 +1123,12 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
.executions
|
||||
.iter()
|
||||
.find(|execution| execution.original_node_index == *owner_node_index)
|
||||
.expect("origin validation found owner");
|
||||
.ok_or_else(|| {
|
||||
invalid(
|
||||
"compiler default owner is missing",
|
||||
format!("textureFamilies[{fi}].source"),
|
||||
)
|
||||
})?;
|
||||
let opposite_socket = if *role == CompilerTextureRole::ColorTarget {
|
||||
"depthTarget"
|
||||
} else {
|
||||
@@ -1000,27 +1153,31 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
)
|
||||
})?;
|
||||
let both_defaults = matches!(&opposite_family.source, TextureFamilySource::CompilerDefaultInput { owner_node_index: opposite_owner, .. } if opposite_owner == owner_node_index);
|
||||
let expected_extent = if both_defaults {
|
||||
NormalizedTextureExtent::SurfaceRelative {
|
||||
width: Ratio {
|
||||
numerator: 1,
|
||||
denominator: 1,
|
||||
let (expected_extent, expected_sample_count) = if both_defaults {
|
||||
(
|
||||
NormalizedTextureExtent::SurfaceRelative {
|
||||
width: Ratio {
|
||||
numerator: 1,
|
||||
denominator: 1,
|
||||
},
|
||||
height: Ratio {
|
||||
numerator: 1,
|
||||
denominator: 1,
|
||||
},
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
height: Ratio {
|
||||
numerator: 1,
|
||||
denominator: 1,
|
||||
},
|
||||
depth_or_array_layers: 1,
|
||||
}
|
||||
1,
|
||||
)
|
||||
} else {
|
||||
super::compiler::family_descriptor(opposite_family)
|
||||
.extent
|
||||
.clone()
|
||||
let opposite = super::compiler::family_descriptor(opposite_family);
|
||||
(opposite.extent.clone(), opposite.sample_count)
|
||||
};
|
||||
if descriptor.extent != expected_extent {
|
||||
if descriptor.extent != expected_extent
|
||||
|| descriptor.sample_count != expected_sample_count
|
||||
{
|
||||
return Err(invalid(
|
||||
"compiler default extent is not canonical",
|
||||
format!("textureFamilies[{fi}].source.descriptor.extent"),
|
||||
"compiler default descriptor inheritance is not canonical",
|
||||
format!("textureFamilies[{fi}].source.descriptor"),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1519,12 +1676,17 @@ pub fn prepare_runtime_plan(
|
||||
format!("executions[{i}].outputs"),
|
||||
));
|
||||
}
|
||||
let color_version = match graph
|
||||
let (color_version, color_stored, color_target) = match graph
|
||||
.resources
|
||||
.get(color_output.resource as usize)
|
||||
.map(|r| &r.plan)
|
||||
{
|
||||
Some(ResourcePlan::Texture { version, .. }) => *version,
|
||||
Some(ResourcePlan::Texture {
|
||||
version,
|
||||
stored,
|
||||
target,
|
||||
..
|
||||
}) => (*version, *stored, *target),
|
||||
_ => {
|
||||
return Err(invalid(
|
||||
"pipeline color output kind is invalid",
|
||||
@@ -1532,12 +1694,17 @@ pub fn prepare_runtime_plan(
|
||||
))
|
||||
}
|
||||
};
|
||||
let depth_version = match graph
|
||||
let (depth_version, depth_stored, depth_target) = match graph
|
||||
.resources
|
||||
.get(depth_output.resource as usize)
|
||||
.map(|r| &r.plan)
|
||||
{
|
||||
Some(ResourcePlan::Texture { version, .. }) => *version,
|
||||
Some(ResourcePlan::Texture {
|
||||
version,
|
||||
stored,
|
||||
target,
|
||||
..
|
||||
}) => (*version, *stored, *target),
|
||||
_ => {
|
||||
return Err(invalid(
|
||||
"pipeline depth output kind is invalid",
|
||||
@@ -1571,12 +1738,45 @@ pub fn prepare_runtime_plan(
|
||||
format!("executions[{i}].kind"),
|
||||
));
|
||||
}
|
||||
let [mesh_access, color_access, depth_access] = execution.accesses.as_slice() else {
|
||||
return Err(invalid(
|
||||
"pipeline access shape mismatch",
|
||||
format!("executions[{i}].accesses"),
|
||||
));
|
||||
let stored_op = |stored| {
|
||||
if stored {
|
||||
StoreOp::Store
|
||||
} else {
|
||||
StoreOp::Discard
|
||||
}
|
||||
};
|
||||
if color_attachment.store != stored_op(color_stored)
|
||||
|| depth_attachment.store != stored_op(depth_stored)
|
||||
|| (color_version > 0
|
||||
&& !matches!(
|
||||
graph.resources.get(color_target as usize).map(|r| &r.plan),
|
||||
Some(ResourcePlan::Texture { stored: true, .. })
|
||||
))
|
||||
|| (depth_version > 0
|
||||
&& !matches!(
|
||||
graph.resources.get(depth_target as usize).map(|r| &r.plan),
|
||||
Some(ResourcePlan::Texture { stored: true, .. })
|
||||
))
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline store metadata is not canonical",
|
||||
format!("executions[{i}].kind"),
|
||||
));
|
||||
}
|
||||
let (base_accesses, resolve_access) = match execution.accesses.as_slice() {
|
||||
[mesh, color, depth] => (&[mesh, color, depth][..], None),
|
||||
[mesh, color, depth, resolve] => (&[mesh, color, depth][..], Some(resolve)),
|
||||
_ => {
|
||||
return Err(invalid(
|
||||
"pipeline access shape mismatch",
|
||||
format!("executions[{i}].accesses"),
|
||||
));
|
||||
}
|
||||
};
|
||||
let [mesh_access, color_access, depth_access] = base_accesses else {
|
||||
unreachable!()
|
||||
};
|
||||
let expected_store = stored_op(color_stored);
|
||||
if mesh_access.socket != "mesh"
|
||||
|| mesh_access.resource != mesh_input.resource
|
||||
|| !matches!(mesh_access.mode, AccessMode::SemanticRead)
|
||||
@@ -1592,25 +1792,37 @@ pub fn prepare_runtime_plan(
|
||||
|| color_access.resource != color_output.resource
|
||||
|| !matches!(
|
||||
color_access.mode,
|
||||
AccessMode::ColorAttachment { location: 0, load, store: StoreOp::Store, full_overwrite }
|
||||
if load == color_attachment.load && full_overwrite == color_clear
|
||||
AccessMode::ColorAttachment { location: 0, load, store, full_overwrite }
|
||||
if load == color_attachment.load && store == expected_store && full_overwrite == color_clear
|
||||
)
|
||||
|| color_attachment.location != 0
|
||||
|| color_attachment.store != StoreOp::Store
|
||||
|| depth_access.socket != "depth"
|
||||
|| depth_access.resource != depth_output.resource
|
||||
|| !matches!(
|
||||
depth_access.mode,
|
||||
AccessMode::DepthAttachment { load, store: StoreOp::Store, full_overwrite }
|
||||
if load == depth_attachment.load && full_overwrite == depth_clear
|
||||
AccessMode::DepthAttachment { load, store, full_overwrite }
|
||||
if load == depth_attachment.load && store == stored_op(depth_stored) && full_overwrite == depth_clear
|
||||
)
|
||||
|| depth_attachment.store != StoreOp::Store
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline attachment accesses mismatch",
|
||||
format!("executions[{i}].accesses"),
|
||||
));
|
||||
}
|
||||
match (color_attachment.resolve_target, resolve_access) {
|
||||
(None, None) => {}
|
||||
(Some(target), Some(access))
|
||||
if access.socket == "colorResolve"
|
||||
&& access.resource == target
|
||||
&& matches!(access.mode, AccessMode::ColorResolve { source, location: 0 } if source == color_attachment.resource) =>
|
||||
{}
|
||||
_ => {
|
||||
return Err(invalid(
|
||||
"pipeline color resolve mismatch",
|
||||
format!("executions[{i}].accesses"),
|
||||
))
|
||||
}
|
||||
}
|
||||
let mesh_resource = graph
|
||||
.resources
|
||||
.get(mesh_input.resource as usize)
|
||||
@@ -1661,7 +1873,10 @@ pub fn prepare_runtime_plan(
|
||||
)
|
||||
})?;
|
||||
if color_descriptor.format == TextureFormat::Depth32Float
|
||||
|| !super::compiler::is_single_view_d2(color_descriptor)
|
||||
|| color_descriptor.dimension != TextureDimension::D2
|
||||
|| !matches!(color_descriptor.sample_count, 1 | 4)
|
||||
|| color_descriptor.mip_level_count != 1
|
||||
|| !color_descriptor.view_formats.is_empty()
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline color attachment descriptor is invalid",
|
||||
@@ -1676,14 +1891,20 @@ pub fn prepare_runtime_plan(
|
||||
)
|
||||
})?;
|
||||
if depth_descriptor.format != TextureFormat::Depth32Float
|
||||
|| !super::compiler::is_single_view_d2(depth_descriptor)
|
||||
|| depth_descriptor.dimension != TextureDimension::D2
|
||||
|| !matches!(depth_descriptor.sample_count, 1 | 4)
|
||||
|| depth_descriptor.mip_level_count != 1
|
||||
|| !depth_descriptor.view_formats.is_empty()
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline depth attachment descriptor is invalid",
|
||||
format!("executions[{i}].inputs"),
|
||||
));
|
||||
}
|
||||
if color_descriptor.extent != depth_descriptor.extent {
|
||||
if color_descriptor.extent != depth_descriptor.extent
|
||||
|| color_descriptor.sample_count != depth_descriptor.sample_count
|
||||
|| (color_descriptor.sample_count == 1 && color_attachment.resolve_target.is_some())
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline attachment extents mismatch",
|
||||
format!("executions[{i}].inputs"),
|
||||
@@ -1839,7 +2060,7 @@ pub fn prepare_runtime_plan(
|
||||
TextureResidency::Transient | TextureResidency::Persistent
|
||||
) || descriptor.dimension != TextureDimension::D2
|
||||
|| descriptor.mip_level_count != 1
|
||||
|| descriptor.sample_count != 1
|
||||
|| !matches!(descriptor.sample_count, 1 | 4)
|
||||
|| !matches!(
|
||||
descriptor.extent,
|
||||
NormalizedTextureExtent::Absolute {
|
||||
@@ -1865,13 +2086,25 @@ pub fn prepare_runtime_plan(
|
||||
}
|
||||
}
|
||||
TextureFamilySource::CompilerDefaultInput { descriptor, .. } => {
|
||||
if !super::compiler::is_single_view_d2(descriptor) || family.allocation.is_none() {
|
||||
if descriptor.dimension != TextureDimension::D2
|
||||
|| !matches!(descriptor.sample_count, 1 | 4)
|
||||
|| descriptor.mip_level_count != 1
|
||||
|| family.allocation.is_none()
|
||||
{
|
||||
return Err(invalid(
|
||||
"compiler default family is not canonical",
|
||||
format!("textureFamilies[{fi}]"),
|
||||
));
|
||||
}
|
||||
}
|
||||
TextureFamilySource::CompilerColorResolve { descriptor, .. } => {
|
||||
if !super::compiler::is_single_view_d2(descriptor) || family.allocation.is_none() {
|
||||
return Err(invalid(
|
||||
"compiler resolve family is not canonical",
|
||||
format!("textureFamilies[{fi}]"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (vi, version) in family.versions.iter().enumerate() {
|
||||
if version.version as usize != vi {
|
||||
@@ -1988,6 +2221,9 @@ pub fn prepare_runtime_plan(
|
||||
TextureFamilySource::CompilerDefaultInput { descriptor, .. } => {
|
||||
(descriptor, TextureResidency::Transient)
|
||||
}
|
||||
TextureFamilySource::CompilerColorResolve { descriptor, .. } => {
|
||||
(descriptor, TextureResidency::Transient)
|
||||
}
|
||||
};
|
||||
expected_usage.extend(family.usage.iter().copied());
|
||||
let kind_valid = match slot.kind {
|
||||
|
||||
@@ -12,7 +12,7 @@ fn input(node: &str, socket: &str) -> Value {
|
||||
|
||||
fn texture(id: &str, format: &str) -> Value {
|
||||
json!({
|
||||
"id": id, "state": "enabled", "executor": { "key": "texture", "version": 1 },
|
||||
"id": id, "state": "enabled", "executor": { "key": "texture", "version": 2 },
|
||||
"parameters": { "residency": "transient", "texture": {
|
||||
"dimension": "d2", "format": format,
|
||||
"extent": { "kind": "surface_relative", "width": { "numerator": 1, "denominator": 1 },
|
||||
@@ -28,6 +28,10 @@ fn set_extent_ratio(texture: &mut Value, numerator: u32, denominator: u32) {
|
||||
json!({"numerator":numerator,"denominator":denominator});
|
||||
}
|
||||
|
||||
fn set_sample_count(texture: &mut Value, sample_count: u32) {
|
||||
texture["parameters"]["texture"]["sampleCount"] = json!(sample_count);
|
||||
}
|
||||
|
||||
fn node(id: &str, key: &str, version: u32, parameters: Value, inputs: Value) -> Value {
|
||||
json!({ "id": id, "state": "enabled", "executor": { "key": key, "version": version },
|
||||
"parameters": parameters, "inputs": inputs })
|
||||
@@ -45,7 +49,7 @@ pub(crate) fn full_cull_graph() -> Value {
|
||||
node("visible", "not", 1, json!({"operandDefault":false}), json!({"operand":input("cull","isFrustumCulled")})),
|
||||
node("class", "and", 1, json!({"leftDefault":true,"rightDefault":true}),
|
||||
json!({"left":input("bits","bit0"),"right":input("visible","value")})),
|
||||
node("pipeline", "pipeline", 3,
|
||||
node("pipeline", "pipeline", 4,
|
||||
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"predicate":input("class","value"),"colorTarget":input("color","texture"),"depthTarget":input("depth","texture")})),
|
||||
node("frame", "frame_out", 3,
|
||||
@@ -58,7 +62,7 @@ pub(crate) fn full_cull_graph() -> Value {
|
||||
#[test]
|
||||
fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
|
||||
assert_eq!(contract("mesh").unwrap().version, 2);
|
||||
assert_eq!(contract("pipeline").unwrap().version, 3);
|
||||
assert_eq!(contract("pipeline").unwrap().version, 4);
|
||||
assert_eq!(
|
||||
contract("mesh")
|
||||
.unwrap()
|
||||
@@ -114,6 +118,204 @@ fn typed_graph_builds_one_dense_deterministic_traversal() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn msaa_pipeline_to_frame_out_materializes_distinct_canonical_resolve() {
|
||||
let mut value = full_cull_graph();
|
||||
set_sample_count(&mut value["nodes"][0], 4);
|
||||
set_sample_count(&mut value["nodes"][1], 4);
|
||||
let graph = compile_value(value).unwrap();
|
||||
let family = graph
|
||||
.texture_families
|
||||
.iter()
|
||||
.find(|family| {
|
||||
matches!(
|
||||
family.source,
|
||||
TextureFamilySource::CompilerColorResolve { .. }
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let TextureFamilySource::CompilerColorResolve {
|
||||
resource: root,
|
||||
source_resource,
|
||||
..
|
||||
} = family.source
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_ne!(root, family.versions[0].resource);
|
||||
assert_ne!(source_resource, family.versions[0].resource);
|
||||
assert_eq!(family.versions[0].target, root);
|
||||
assert!(matches!(
|
||||
graph.resources[root as usize].plan,
|
||||
ResourcePlan::TextureSource { .. }
|
||||
));
|
||||
assert!(
|
||||
validate_activatable(&graph).is_ok(),
|
||||
"{:?}",
|
||||
validate_activatable(&graph)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn msaa_resolve_can_feed_fullscreen_and_default_depth_inherits_four_samples() {
|
||||
let mut value = full_cull_graph();
|
||||
set_sample_count(&mut value["nodes"][0], 4);
|
||||
value["nodes"][8]["inputs"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("depthTarget");
|
||||
value["nodes"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.insert(9, texture("post_target", "rgba16_float"));
|
||||
value["nodes"].as_array_mut().unwrap().insert(
|
||||
10,
|
||||
node(
|
||||
"post",
|
||||
"saturation",
|
||||
1,
|
||||
json!({"saturation":1,"factor":1}),
|
||||
json!({
|
||||
"source":input("pipeline","color"),
|
||||
"colorTarget":input("post_target","texture")
|
||||
}),
|
||||
),
|
||||
);
|
||||
value["nodes"][11]["inputs"]["color"] = input("post", "color");
|
||||
|
||||
let graph = compile_value(value).unwrap();
|
||||
assert!(validate_activatable(&graph).is_ok());
|
||||
let default_depth = graph
|
||||
.texture_families
|
||||
.iter()
|
||||
.find(|family| {
|
||||
matches!(
|
||||
family.source,
|
||||
TextureFamilySource::CompilerDefaultInput {
|
||||
role: CompilerTextureRole::DepthTarget,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
super::compiler::family_descriptor(default_depth).sample_count,
|
||||
4
|
||||
);
|
||||
let resolve_output = graph
|
||||
.texture_families
|
||||
.iter()
|
||||
.find_map(|family| match family.source {
|
||||
TextureFamilySource::CompilerColorResolve { .. } => Some(family.versions[0].resource),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let post = graph
|
||||
.executions
|
||||
.iter()
|
||||
.find(|execution| execution.id == "post")
|
||||
.unwrap();
|
||||
assert_eq!(post.inputs[0].resource, resolve_output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn msaa_resolve_accepts_default_color_inferred_from_authored_depth() {
|
||||
let mut value = full_cull_graph();
|
||||
set_sample_count(&mut value["nodes"][1], 4);
|
||||
value["nodes"][8]["inputs"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("colorTarget");
|
||||
|
||||
let graph = compile_value(value).unwrap();
|
||||
let (resolve_descriptor, source_resource) = graph
|
||||
.texture_families
|
||||
.iter()
|
||||
.find_map(|family| match &family.source {
|
||||
TextureFamilySource::CompilerColorResolve {
|
||||
descriptor,
|
||||
source_resource,
|
||||
..
|
||||
} => Some((descriptor, *source_resource)),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(resolve_descriptor.sample_count, 1);
|
||||
let source_family = match graph.resources[source_resource as usize].plan {
|
||||
ResourcePlan::Texture { family, .. } => family,
|
||||
_ => panic!("expected resolve source texture version"),
|
||||
};
|
||||
assert!(matches!(
|
||||
&graph.texture_families[source_family as usize].source,
|
||||
TextureFamilySource::CompilerDefaultInput {
|
||||
role: CompilerTextureRole::ColorTarget,
|
||||
descriptor,
|
||||
..
|
||||
} if descriptor.sample_count == 4
|
||||
));
|
||||
assert!(
|
||||
validate_activatable(&graph).is_ok(),
|
||||
"{:?}",
|
||||
validate_activatable(&graph)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_rejects_resolve_origin_and_store_tampering() {
|
||||
let mut value = full_cull_graph();
|
||||
set_sample_count(&mut value["nodes"][0], 4);
|
||||
set_sample_count(&mut value["nodes"][1], 4);
|
||||
let graph = compile_value(value).unwrap();
|
||||
assert!(validate_activatable(&graph).is_ok());
|
||||
let family = graph
|
||||
.texture_families
|
||||
.iter()
|
||||
.find(|family| {
|
||||
matches!(
|
||||
family.source,
|
||||
TextureFamilySource::CompilerColorResolve { .. }
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let root = match family.source {
|
||||
TextureFamilySource::CompilerColorResolve { resource, .. } => resource,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let source = match family.source {
|
||||
TextureFamilySource::CompilerColorResolve {
|
||||
source_resource, ..
|
||||
} => source_resource,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut bad_origin = graph.clone();
|
||||
bad_origin.resources[root as usize].origin = ResourceOrigin::CompilerColorResolve {
|
||||
producer_node_index: u32::MAX,
|
||||
output_ordinal: 0,
|
||||
source_resource: source,
|
||||
};
|
||||
assert_runtime_plan_invalid(&bad_origin);
|
||||
|
||||
let mut bad_store = graph.clone();
|
||||
let ResourcePlan::Texture { stored, .. } = &mut bad_store.resources[source as usize].plan
|
||||
else {
|
||||
panic!("expected source texture version")
|
||||
};
|
||||
*stored = !*stored;
|
||||
assert_runtime_plan_invalid(&bad_store);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multisampled_depth_sample_is_rejected_at_exact_socket() {
|
||||
let mut value = full_cull_graph();
|
||||
set_sample_count(&mut value["nodes"][0], 4);
|
||||
set_sample_count(&mut value["nodes"][1], 4);
|
||||
value["nodes"][9]["inputs"]["color"] = input("pipeline", "depth");
|
||||
let error = compile_value(value).unwrap_err();
|
||||
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
|
||||
assert_eq!(error.details["path"], "nodes[9].inputs.color");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipeline_predicate_defaults_true_and_expression_edges_are_validated() {
|
||||
let mut graph = full_cull_graph();
|
||||
@@ -156,10 +358,10 @@ fn expression_provenance_rejects_cross_mesh_values() {
|
||||
fn implicit_pipeline_graph() -> Value {
|
||||
json!({ "schemaVersion": 2, "graphId": "implicit", "revision": 1, "nodes": [
|
||||
node("mesh", "mesh", 2, json!({}), json!({})),
|
||||
node("first", "pipeline", 3,
|
||||
node("first", "pipeline", 4,
|
||||
json!({"pipeline":"ground_plane","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh")})),
|
||||
node("second", "pipeline", 3,
|
||||
node("second", "pipeline", 4,
|
||||
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"colorTarget":input("first","color"),"depthTarget":input("first","depth")})),
|
||||
node("frame", "frame_out", 3,
|
||||
@@ -169,9 +371,9 @@ fn implicit_pipeline_graph() -> Value {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contract_v3_declares_strict_default_policies() {
|
||||
fn contract_v4_declares_strict_default_policies() {
|
||||
let pipeline = contract("pipeline").unwrap();
|
||||
assert_eq!(pipeline.version, 3);
|
||||
assert_eq!(pipeline.version, 4);
|
||||
assert_eq!(pipeline.inputs[0].default_policy, InputDefaultPolicy::None);
|
||||
assert_eq!(
|
||||
pipeline.inputs[1].default_policy,
|
||||
|
||||
@@ -174,7 +174,7 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: view(color.resource)?,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
resolve_target: color.resolve_target.map(view).transpose()?,
|
||||
ops: wgpu::Operations {
|
||||
load: match color.load {
|
||||
NormalizedColorLoad::Load => wgpu::LoadOp::Load,
|
||||
|
||||
@@ -45,6 +45,16 @@ fn device_feature_plan(profile: bool, supported: wgpu::Features) -> DeviceFeatur
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_planned_x4_attachment(
|
||||
is_depth: bool,
|
||||
resolve_required: bool,
|
||||
render_attachment: bool,
|
||||
multisample_x4: bool,
|
||||
multisample_resolve: bool,
|
||||
) -> bool {
|
||||
render_attachment && multisample_x4 && (is_depth || !resolve_required || multisample_resolve)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod device_feature_tests {
|
||||
use super::*;
|
||||
@@ -63,6 +73,22 @@ mod device_feature_tests {
|
||||
assert!(profiled.initial.contains(wgpu::Features::TIMESTAMP_QUERY));
|
||||
assert!(profiled.profiling_enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_x4_policy_distinguishes_depth_and_color_resolve() {
|
||||
assert!(supports_planned_x4_attachment(
|
||||
true, false, true, true, false
|
||||
));
|
||||
assert!(!supports_planned_x4_attachment(
|
||||
true, false, true, false, true
|
||||
));
|
||||
assert!(!supports_planned_x4_attachment(
|
||||
false, true, true, true, false
|
||||
));
|
||||
assert!(supports_planned_x4_attachment(
|
||||
false, true, true, true, true
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, align(16))]
|
||||
@@ -1692,6 +1718,72 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let resolve_formats: std::collections::BTreeSet<_> = graph
|
||||
.texture_families
|
||||
.iter()
|
||||
.filter_map(|family| match &family.source {
|
||||
TextureFamilySource::CompilerColorResolve {
|
||||
source_resource, ..
|
||||
} => {
|
||||
let source = graph.resources.get(*source_resource as usize)?;
|
||||
let source_family = match source.plan {
|
||||
ResourcePlan::Texture { family, .. } => family,
|
||||
_ => return None,
|
||||
};
|
||||
graph
|
||||
.texture_families
|
||||
.get(source_family as usize)
|
||||
.map(|family| match &family.source {
|
||||
TextureFamilySource::AuthoredTexture { descriptor, .. }
|
||||
| TextureFamilySource::CompilerDefaultInput { descriptor, .. }
|
||||
| TextureFamilySource::CompilerColorResolve { descriptor, .. } => {
|
||||
descriptor.format
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for (family_index, family) in graph.texture_families.iter().enumerate() {
|
||||
let descriptor = match &family.source {
|
||||
TextureFamilySource::AuthoredTexture { descriptor, .. }
|
||||
| TextureFamilySource::CompilerDefaultInput { descriptor, .. }
|
||||
| TextureFamilySource::CompilerColorResolve { descriptor, .. } => descriptor,
|
||||
};
|
||||
if descriptor.sample_count != 4 {
|
||||
continue;
|
||||
}
|
||||
let features = self
|
||||
.context
|
||||
.adapter
|
||||
.get_texture_format_features(texture_format(descriptor.format));
|
||||
let resolve_required = resolve_formats.contains(&descriptor.format)
|
||||
&& descriptor.format != TextureFormat::Depth32Float;
|
||||
if !supports_planned_x4_attachment(
|
||||
descriptor.format == TextureFormat::Depth32Float,
|
||||
resolve_required,
|
||||
features
|
||||
.allowed_usages
|
||||
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT),
|
||||
features
|
||||
.flags
|
||||
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4),
|
||||
features
|
||||
.flags
|
||||
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE),
|
||||
) {
|
||||
let source_path = match &family.source {
|
||||
TextureFamilySource::AuthoredTexture { .. } => "authored",
|
||||
TextureFamilySource::CompilerDefaultInput { .. } => "default",
|
||||
TextureFamilySource::CompilerColorResolve { .. } => "resolve",
|
||||
};
|
||||
return Err(GraphError::at(
|
||||
"GRAPH_UNSUPPORTED_FEATURE",
|
||||
"adapter does not support planned 4x MSAA attachment",
|
||||
format!("textureFamilies[{family_index}].source.{source_path}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut textures = Vec::with_capacity(runtime.allocations.classes.len());
|
||||
for class in &runtime.allocations.classes {
|
||||
let mut gpu_class = Vec::with_capacity(class.slots.len());
|
||||
@@ -2055,6 +2147,17 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
depth_format,
|
||||
compare,
|
||||
*depth_write_enabled,
|
||||
runtime.allocations.resource_allocations[color.resource as usize]
|
||||
.and_then(|a| {
|
||||
runtime
|
||||
.allocations
|
||||
.classes
|
||||
.get(a.class as usize)?
|
||||
.slots
|
||||
.get(a.slot as usize)
|
||||
})
|
||||
.map(|slot| slot.descriptor.sample_count)
|
||||
.ok_or_else(|| fail("color allocation invalid"))?,
|
||||
)
|
||||
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
|
||||
executions.push(PreparedExecution::Pipeline {
|
||||
|
||||
@@ -96,11 +96,17 @@ fn target_variant_spec(
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
depth_compare: wgpu::CompareFunction,
|
||||
depth_write: bool,
|
||||
sample_count: u32,
|
||||
) -> RenderPipelineSpec {
|
||||
let write_mask = spec
|
||||
.targets
|
||||
.first()
|
||||
.and_then(Option::as_ref)
|
||||
.map_or(wgpu::ColorWrites::ALL, |target| target.write_mask);
|
||||
spec.targets = vec![Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
write_mask,
|
||||
})];
|
||||
spec.depth_stencil = depth_format.map(|format| wgpu::DepthStencilState {
|
||||
format,
|
||||
@@ -109,6 +115,7 @@ fn target_variant_spec(
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
});
|
||||
spec.multisample.count = sample_count;
|
||||
spec
|
||||
}
|
||||
|
||||
@@ -379,14 +386,21 @@ impl PipelineLibrary {
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
depth_compare: wgpu::CompareFunction,
|
||||
depth_write: bool,
|
||||
sample_count: u32,
|
||||
) -> Result<wgpu::RenderPipeline, String> {
|
||||
let spec = self
|
||||
.specs
|
||||
.get(base.get() as usize)
|
||||
.cloned()
|
||||
.ok_or_else(|| "unknown base pipeline".to_owned())?;
|
||||
let spec =
|
||||
target_variant_spec(spec, color_format, depth_format, depth_compare, depth_write);
|
||||
let spec = target_variant_spec(
|
||||
spec,
|
||||
color_format,
|
||||
depth_format,
|
||||
depth_compare,
|
||||
depth_write,
|
||||
sample_count,
|
||||
);
|
||||
let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(" target variant"),
|
||||
source: wgpu::ShaderSource::Wgsl(spec.vertex.shader_source.as_str().into()),
|
||||
@@ -482,38 +496,46 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_spec_disables_blending_without_mutating_base() {
|
||||
let mut base = spec();
|
||||
base.primitive.cull_mode = Some(wgpu::Face::Front);
|
||||
base.multisample.count = 4;
|
||||
base.targets[0].as_mut().unwrap().write_mask = wgpu::ColorWrites::RED;
|
||||
let preserved_vertex = StageKey::from_stage(&base.vertex);
|
||||
let preserved_fragment = base.fragment.as_ref().map(StageKey::from_stage);
|
||||
assert!(base.targets[0].as_ref().unwrap().blend.is_some());
|
||||
let variant = target_variant_spec(
|
||||
base.clone(),
|
||||
wgpu::TextureFormat::Rgba16Float,
|
||||
None,
|
||||
wgpu::CompareFunction::Always,
|
||||
false,
|
||||
);
|
||||
assert_eq!(variant.targets[0].as_ref().unwrap().blend, None);
|
||||
assert_eq!(
|
||||
variant.targets[0].as_ref().unwrap().format,
|
||||
wgpu::TextureFormat::Rgba16Float
|
||||
);
|
||||
assert!(base.targets[0].as_ref().unwrap().blend.is_some());
|
||||
assert_eq!(variant.primitive, base.primitive);
|
||||
assert_eq!(variant.multisample, base.multisample);
|
||||
assert_eq!(StageKey::from_stage(&variant.vertex), preserved_vertex);
|
||||
assert_eq!(
|
||||
variant.fragment.as_ref().map(StageKey::from_stage),
|
||||
preserved_fragment
|
||||
);
|
||||
assert_eq!(
|
||||
variant.targets[0].as_ref().unwrap().write_mask,
|
||||
wgpu::ColorWrites::ALL
|
||||
);
|
||||
fn target_spec_preserves_base_state_for_both_sample_count_transitions() {
|
||||
for (from, to) in [(1, 4), (4, 1)] {
|
||||
let mut base = spec();
|
||||
base.primitive.cull_mode = Some(wgpu::Face::Front);
|
||||
base.multisample.count = from;
|
||||
base.multisample.alpha_to_coverage_enabled = true;
|
||||
base.targets[0].as_mut().unwrap().write_mask = wgpu::ColorWrites::RED;
|
||||
let base_key = base.key();
|
||||
let preserved_vertex = StageKey::from_stage(&base.vertex);
|
||||
let preserved_fragment = base.fragment.as_ref().map(StageKey::from_stage);
|
||||
assert!(base.targets[0].as_ref().unwrap().blend.is_some());
|
||||
let variant = target_variant_spec(
|
||||
base.clone(),
|
||||
wgpu::TextureFormat::Rgba16Float,
|
||||
None,
|
||||
wgpu::CompareFunction::Always,
|
||||
false,
|
||||
to,
|
||||
);
|
||||
assert_eq!(variant.targets[0].as_ref().unwrap().blend, None);
|
||||
assert_eq!(
|
||||
variant.targets[0].as_ref().unwrap().format,
|
||||
wgpu::TextureFormat::Rgba16Float
|
||||
);
|
||||
assert!(base.targets[0].as_ref().unwrap().blend.is_some());
|
||||
assert_eq!(variant.primitive, base.primitive);
|
||||
let mut expected_multisample = base.multisample;
|
||||
expected_multisample.count = to;
|
||||
assert_eq!(variant.multisample, expected_multisample);
|
||||
assert_eq!(StageKey::from_stage(&variant.vertex), preserved_vertex);
|
||||
assert_eq!(
|
||||
variant.fragment.as_ref().map(StageKey::from_stage),
|
||||
preserved_fragment
|
||||
);
|
||||
assert_eq!(
|
||||
variant.targets[0].as_ref().unwrap().write_mask,
|
||||
wgpu::ColorWrites::RED
|
||||
);
|
||||
assert_ne!(variant.key(), base_key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user