refactor: centralize fullscreen graph policy
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:
@@ -946,8 +946,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
let transition_sockets: &[(&str, u16)] = match contracts[i].key {
|
||||
"pipeline" => &[("colorTarget", 0), ("depthTarget", 1)],
|
||||
"fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur" | "bloom_composite"
|
||||
| "luminance_edge" => &[("colorTarget", 0)],
|
||||
_ if contracts[i].fullscreen_policy.is_some() => &[("colorTarget", 0)],
|
||||
_ => continue,
|
||||
};
|
||||
for &(input_socket, output_ordinal) in transition_sockets {
|
||||
@@ -1118,12 +1117,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
format!("nodes[{i}].inputs"),
|
||||
));
|
||||
}
|
||||
} else if contracts[i].key != "frame_out"
|
||||
&& contracts[i]
|
||||
.inputs
|
||||
.iter()
|
||||
.any(|input| matches!(input.role, InputRole::SampledTexture))
|
||||
{
|
||||
} else if contracts[i].fullscreen_policy.is_some() {
|
||||
let hazard = contracts[i].inputs.iter().filter(|input| matches!(input.role, InputRole::SampledTexture)).any(|input| matches!((version_of.get(&bound[i][input.name].producer), version_of.get(&OutputKey(i, 0))), (Some((sf, _, _)), Some((tf, _, _))) if sf == tf));
|
||||
if hazard {
|
||||
return Err(error(
|
||||
@@ -1213,13 +1207,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
|
||||
for i in 0..graph.nodes.len() {
|
||||
if !live.contains(&i)
|
||||
|| contracts[i].key == "frame_out"
|
||||
|| !contracts[i]
|
||||
.inputs
|
||||
.iter()
|
||||
.any(|input| matches!(input.role, InputRole::SampledTexture))
|
||||
{
|
||||
if !live.contains(&i) || contracts[i].fullscreen_policy.is_none() {
|
||||
continue;
|
||||
}
|
||||
let source_key = bound[i]["source"].producer;
|
||||
@@ -1244,7 +1232,9 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let authored_target_ok = target_descriptor.is_some_and(|descriptor| {
|
||||
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(descriptor)
|
||||
});
|
||||
let bloom_input_ok = if contracts[i].key == "bloom_composite" {
|
||||
let bloom_input_ok = if contracts[i].fullscreen_policy
|
||||
== Some(FullscreenPolicy::BloomComposite)
|
||||
{
|
||||
let bloom_key = bound[i]["bloom"].producer;
|
||||
let Some(&(bloom_family_id, _, _)) = version_of.get(&bloom_key) else {
|
||||
return Err(error(
|
||||
@@ -1264,8 +1254,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let source_is_full_surface = matches!(&source_descriptor.extent, NormalizedTextureExtent::SurfaceRelative { width, height, depth_or_array_layers: 1 } if *width == Ratio { numerator:1, denominator:1 } && *height == Ratio { numerator:1, denominator:1 });
|
||||
let target_matches_source = target_descriptor
|
||||
.is_some_and(|descriptor| descriptor.extent == source_descriptor.extent);
|
||||
let descriptor_ok = match contracts[i].key {
|
||||
"fullscreen_copy" => {
|
||||
let descriptor_ok = match contracts[i].fullscreen_policy {
|
||||
Some(FullscreenPolicy::Copy) => {
|
||||
target_descriptor.is_none() && source_is_full_surface
|
||||
|| target_descriptor.is_some_and(|descriptor| {
|
||||
descriptor.format != TextureFormat::Depth32Float
|
||||
@@ -1273,15 +1263,17 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
&& descriptor.extent == source_descriptor.extent
|
||||
})
|
||||
}
|
||||
"tone_map" => target_descriptor.is_some_and(|descriptor| {
|
||||
Some(FullscreenPolicy::ToneMap) => target_descriptor.is_some_and(|descriptor| {
|
||||
descriptor.format != TextureFormat::Depth32Float
|
||||
&& descriptor.format != TextureFormat::R32Float
|
||||
&& is_single_view_d2(descriptor)
|
||||
&& descriptor.extent == source_descriptor.extent
|
||||
}),
|
||||
"bloom_extract" => authored_target_ok,
|
||||
"bloom_blur" | "luminance_edge" => authored_target_ok && target_matches_source,
|
||||
"bloom_composite" => authored_target_ok && target_matches_source && bloom_input_ok,
|
||||
Some(FullscreenPolicy::BloomExtract) => authored_target_ok,
|
||||
Some(FullscreenPolicy::HdrSameExtent) => authored_target_ok && target_matches_source,
|
||||
Some(FullscreenPolicy::BloomComposite) => {
|
||||
authored_target_ok && target_matches_source && bloom_input_ok
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !source_ok || !descriptor_ok {
|
||||
@@ -1650,21 +1642,19 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}),
|
||||
}
|
||||
}
|
||||
"fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur" | "bloom_composite"
|
||||
| "luminance_edge" => {
|
||||
_ if contracts[i].fullscreen_policy.is_some() => {
|
||||
let color = output_ids[&OutputKey(i, 0)];
|
||||
let load = NormalizedColorLoad::Clear {
|
||||
value: [0.0, 0.0, 0.0, 0.0],
|
||||
};
|
||||
accesses.push(CompiledAccess {
|
||||
socket: "source".into(),
|
||||
resource: input_resource("source"),
|
||||
mode: AccessMode::SampledTexture,
|
||||
});
|
||||
if contracts[i].key == "bloom_composite" {
|
||||
for input in contracts[i]
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|input| input.role == InputRole::SampledTexture)
|
||||
{
|
||||
accesses.push(CompiledAccess {
|
||||
socket: "bloom".into(),
|
||||
resource: input_resource("bloom"),
|
||||
socket: input.name.into(),
|
||||
resource: input_resource(input.name),
|
||||
mode: AccessMode::SampledTexture,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,15 @@ pub enum ExecutionClass {
|
||||
Frame,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FullscreenPolicy {
|
||||
Copy,
|
||||
ToneMap,
|
||||
HdrSameExtent,
|
||||
BloomExtract,
|
||||
BloomComposite,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InputCardinality {
|
||||
@@ -81,6 +90,8 @@ pub struct Contract {
|
||||
pub inputs: &'static [InputSocketContract],
|
||||
pub outputs: &'static [OutputSocketContract],
|
||||
pub inherently_observable: bool,
|
||||
#[serde(skip)]
|
||||
pub fullscreen_policy: Option<FullscreenPolicy>,
|
||||
}
|
||||
|
||||
use SemanticType::*;
|
||||
@@ -268,6 +279,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: NONE_IN,
|
||||
outputs: MESH_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "texture",
|
||||
@@ -276,6 +288,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: NONE_IN,
|
||||
outputs: TEXTURE_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "frustum_cull",
|
||||
@@ -284,6 +297,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: CULL_IN,
|
||||
outputs: CULLED_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "mesh_query",
|
||||
@@ -292,6 +306,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: QUERY_IN,
|
||||
outputs: DRAW_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "pipeline_registry",
|
||||
@@ -300,6 +315,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: REGISTRY_IN,
|
||||
outputs: ACTIVATION_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "pipeline",
|
||||
@@ -308,6 +324,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: PIPELINE_IN,
|
||||
outputs: PIPELINE_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "fullscreen_copy",
|
||||
@@ -316,6 +333,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::Copy),
|
||||
},
|
||||
Contract {
|
||||
key: "tone_map",
|
||||
@@ -324,6 +342,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::ToneMap),
|
||||
},
|
||||
Contract {
|
||||
key: "bloom_extract",
|
||||
@@ -332,6 +351,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::BloomExtract),
|
||||
},
|
||||
Contract {
|
||||
key: "bloom_blur",
|
||||
@@ -340,6 +360,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "bloom_composite",
|
||||
@@ -348,6 +369,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: BLOOM_COMPOSITE_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::BloomComposite),
|
||||
},
|
||||
Contract {
|
||||
key: "luminance_edge",
|
||||
@@ -356,6 +378,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "frame_out",
|
||||
@@ -364,6 +387,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inputs: FRAME_OUT_IN,
|
||||
outputs: NONE_OUT,
|
||||
inherently_observable: true,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -256,20 +256,13 @@ fn valid_pipeline_name(name: &str) -> bool {
|
||||
}
|
||||
|
||||
fn execution_supported(key: &str) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
"frustum_cull"
|
||||
| "mesh_query"
|
||||
| "pipeline_registry"
|
||||
| "pipeline"
|
||||
| "fullscreen_copy"
|
||||
| "tone_map"
|
||||
| "bloom_extract"
|
||||
| "bloom_blur"
|
||||
| "bloom_composite"
|
||||
| "luminance_edge"
|
||||
| "frame_out"
|
||||
)
|
||||
contract(key).is_some_and(|contract| {
|
||||
contract.fullscreen_policy.is_some()
|
||||
|| matches!(
|
||||
key,
|
||||
"frustum_cull" | "mesh_query" | "pipeline_registry" | "pipeline" | "frame_out"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn resource_is_mesh(graph: &CompiledGraph, id: u32) -> bool {
|
||||
@@ -326,19 +319,12 @@ fn validate_fullscreen_execution(
|
||||
graph: &CompiledGraph,
|
||||
i: usize,
|
||||
execution: &CompiledExecution,
|
||||
contract: &Contract,
|
||||
) -> Result<(), GraphError> {
|
||||
let key = execution.executor.key.as_str();
|
||||
if !matches!(
|
||||
key,
|
||||
"fullscreen_copy"
|
||||
| "tone_map"
|
||||
| "bloom_extract"
|
||||
| "bloom_blur"
|
||||
| "bloom_composite"
|
||||
| "luminance_edge"
|
||||
) {
|
||||
let Some(policy) = contract.fullscreen_policy else {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let path = |field| format!("executions[{i}].{field}");
|
||||
let valid_parameters = match (key, &execution.parameters) {
|
||||
("fullscreen_copy", NormalizedParameters::FullscreenCopy) => true,
|
||||
@@ -372,16 +358,12 @@ fn validate_fullscreen_execution(
|
||||
path("parameters"),
|
||||
));
|
||||
}
|
||||
let expected_inputs = if key == "bloom_composite" {
|
||||
&["source", "bloom", "colorTarget"][..]
|
||||
} else {
|
||||
&["source", "colorTarget"][..]
|
||||
};
|
||||
let expected_inputs: Vec<_> = contract.inputs.iter().map(|input| input.name).collect();
|
||||
if execution.inputs.len() != expected_inputs.len()
|
||||
|| execution
|
||||
.inputs
|
||||
.iter()
|
||||
.zip(expected_inputs)
|
||||
.zip(&expected_inputs)
|
||||
.any(|(v, s)| v.socket != *s)
|
||||
{
|
||||
return Err(invalid("fullscreen inputs mismatch", path("inputs")));
|
||||
@@ -410,7 +392,18 @@ fn validate_fullscreen_execution(
|
||||
{
|
||||
return Err(invalid("fullscreen attachment mismatch", path("kind")));
|
||||
}
|
||||
let sampled_count = expected_inputs.len() - 1;
|
||||
let sampled_inputs: Vec<_> = contract
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|input| input.role == InputRole::SampledTexture)
|
||||
.collect();
|
||||
let sampled_count = sampled_inputs.len();
|
||||
if contract.inputs[..sampled_count]
|
||||
.iter()
|
||||
.any(|input| input.role != InputRole::SampledTexture)
|
||||
{
|
||||
return Err(invalid("fullscreen inputs mismatch", path("inputs")));
|
||||
}
|
||||
if execution.accesses.len() != sampled_count + 1
|
||||
|| execution.inputs[..sampled_count]
|
||||
.iter()
|
||||
@@ -501,13 +494,13 @@ fn validate_fullscreen_execution(
|
||||
single_view_d2(d) && d.format == TextureFormat::Rgba16Float
|
||||
};
|
||||
let descriptors_valid = hdr(source)
|
||||
&& match key {
|
||||
"fullscreen_copy" => {
|
||||
&& match policy {
|
||||
FullscreenPolicy::Copy => {
|
||||
single_view_d2(target_d)
|
||||
&& target_d.format != TextureFormat::Depth32Float
|
||||
&& target_d.extent == source.extent
|
||||
}
|
||||
"tone_map" => {
|
||||
FullscreenPolicy::ToneMap => {
|
||||
single_view_d2(target_d)
|
||||
&& !matches!(
|
||||
target_d.format,
|
||||
@@ -515,14 +508,13 @@ fn validate_fullscreen_execution(
|
||||
)
|
||||
&& target_d.extent == source.extent
|
||||
}
|
||||
"bloom_extract" => hdr(target_d),
|
||||
"bloom_blur" | "luminance_edge" => hdr(target_d) && target_d.extent == source.extent,
|
||||
"bloom_composite" => {
|
||||
FullscreenPolicy::BloomExtract => hdr(target_d),
|
||||
FullscreenPolicy::HdrSameExtent => hdr(target_d) && target_d.extent == source.extent,
|
||||
FullscreenPolicy::BloomComposite => {
|
||||
hdr(target_d)
|
||||
&& target_d.extent == source.extent
|
||||
&& texture_descriptor(graph, execution.inputs[1].resource).is_some_and(hdr)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !descriptors_valid {
|
||||
return Err(invalid(
|
||||
@@ -769,7 +761,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
referenced.insert(access.resource);
|
||||
}
|
||||
validate_compute_execution(graph, i, execution)?;
|
||||
validate_fullscreen_execution(graph, i, execution)?;
|
||||
validate_fullscreen_execution(graph, i, execution, contract)?;
|
||||
for resource in referenced {
|
||||
uses.get_mut(resource as usize)
|
||||
.ok_or_else(|| {
|
||||
@@ -950,8 +942,8 @@ pub fn prepare_runtime_plan(
|
||||
}
|
||||
}
|
||||
"pipeline_registry" | "pipeline" => {}
|
||||
"fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur" | "bloom_composite"
|
||||
| "luminance_edge" => {}
|
||||
_ if contract(&execution.executor.key)
|
||||
.is_some_and(|contract| contract.fullscreen_policy.is_some()) => {}
|
||||
"frustum_cull" => {}
|
||||
"frame_out" => {
|
||||
if frame_out_index.replace(i).is_some() {
|
||||
|
||||
@@ -847,6 +847,31 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
||||
("frame_out", 1),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
CONTRACTS
|
||||
.iter()
|
||||
.map(|contract| (contract.key, contract.fullscreen_policy))
|
||||
.collect::<Vec<_>>(),
|
||||
[
|
||||
("mesh", None),
|
||||
("texture", None),
|
||||
("frustum_cull", None),
|
||||
("mesh_query", None),
|
||||
("pipeline_registry", None),
|
||||
("pipeline", None),
|
||||
("fullscreen_copy", Some(FullscreenPolicy::Copy)),
|
||||
("tone_map", Some(FullscreenPolicy::ToneMap)),
|
||||
("bloom_extract", Some(FullscreenPolicy::BloomExtract)),
|
||||
("bloom_blur", Some(FullscreenPolicy::HdrSameExtent)),
|
||||
("bloom_composite", Some(FullscreenPolicy::BloomComposite)),
|
||||
("luminance_edge", Some(FullscreenPolicy::HdrSameExtent)),
|
||||
("frame_out", None),
|
||||
]
|
||||
);
|
||||
for contract in CONTRACTS {
|
||||
let serialized = serde_json::to_value(contract).unwrap();
|
||||
assert!(serialized.get("fullscreenPolicy").is_none());
|
||||
}
|
||||
let mesh = contract("mesh").unwrap();
|
||||
assert_eq!(mesh.execution, ExecutionClass::Source);
|
||||
assert!(mesh.inputs.is_empty());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@group(0) @binding(0) var source_texture: texture_2d<f32>;
|
||||
@group(0) @binding(1) var second_texture: texture_2d<f32>;
|
||||
@group(0) @binding(2) var linear_clamp: sampler;
|
||||
struct Parameters { a: vec4<f32>, b: vec4<f32> }
|
||||
struct Parameters { values: array<vec4<f32>, 8> }
|
||||
@group(0) @binding(3) var<uniform> parameters: Parameters;
|
||||
|
||||
struct VertexOut { @builtin(position) position: vec4<f32>, @location(0) uv: vec2<f32> }
|
||||
@@ -20,19 +20,19 @@ fn linear_to_srgb(x: vec3<f32>) -> vec3<f32> {
|
||||
let low = x * 12.92; let high = 1.055 * pow(x, vec3(1.0 / 2.4)) - vec3(0.055);
|
||||
return select(high, low, x <= vec3(0.0031308));
|
||||
}
|
||||
@fragment fn fs_tone_map(in: VertexOut) -> @location(0) vec4<f32> { let c=sample_source(in.uv); return vec4(linear_to_srgb(aces(c.rgb * parameters.a.x)), c.a); }
|
||||
@fragment fn fs_tone_map(in: VertexOut) -> @location(0) vec4<f32> { let c=sample_source(in.uv); return vec4(linear_to_srgb(aces(c.rgb * parameters.values[0].x)), c.a); }
|
||||
@fragment fn fs_bloom_extract(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
let c=sample_source(in.uv); let brightness=max(c.r,max(c.g,c.b)); let knee=max(parameters.a.y,0.00001); let soft=clamp((brightness-parameters.a.x+knee)/(2.0*knee),0.0,1.0); let contribution=max(brightness-parameters.a.x,0.0)+soft*soft*knee; return vec4(c.rgb*contribution/max(brightness,0.00001),1.0);
|
||||
let c=sample_source(in.uv); let brightness=max(c.r,max(c.g,c.b)); let knee=max(parameters.values[0].y,0.00001); let soft=clamp((brightness-parameters.values[0].x+knee)/(2.0*knee),0.0,1.0); let contribution=max(brightness-parameters.values[0].x,0.0)+soft*soft*knee; return vec4(c.rgb*contribution/max(brightness,0.00001),1.0);
|
||||
}
|
||||
@fragment fn fs_bloom_blur(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
let size=vec2<f32>(textureDimensions(source_texture)); let step=parameters.a.xy*parameters.a.z/size;
|
||||
let size=vec2<f32>(textureDimensions(source_texture)); let step=parameters.values[0].xy*parameters.values[0].z/size;
|
||||
var c=sample_source(in.uv)*0.227027; c+=sample_source(in.uv+step*1.384615)*0.316216; c+=sample_source(in.uv-step*1.384615)*0.316216; c+=sample_source(in.uv+step*3.230769)*0.070270; c+=sample_source(in.uv-step*3.230769)*0.070270; return c;
|
||||
}
|
||||
@fragment fn fs_bloom_composite(in: VertexOut) -> @location(0) vec4<f32> { let c=sample_source(in.uv); return vec4(c.rgb+textureSampleLevel(second_texture,linear_clamp,in.uv,0.0).rgb*parameters.a.x,c.a); }
|
||||
@fragment fn fs_bloom_composite(in: VertexOut) -> @location(0) vec4<f32> { let c=sample_source(in.uv); return vec4(c.rgb+textureSampleLevel(second_texture,linear_clamp,in.uv,0.0).rgb*parameters.values[0].x,c.a); }
|
||||
fn luminance(c: vec3<f32>) -> f32 { return dot(c,vec3(0.2126,0.7152,0.0722)); }
|
||||
@fragment fn fs_luminance_edge(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
let d=1.0/vec2<f32>(textureDimensions(source_texture)); var gx=0.0; var gy=0.0;
|
||||
gx += -luminance(sample_source(in.uv+d*vec2(-1.0,-1.0)).rgb)+luminance(sample_source(in.uv+d*vec2(1.0,-1.0)).rgb); gx += -2.0*luminance(sample_source(in.uv+d*vec2(-1.0,0.0)).rgb)+2.0*luminance(sample_source(in.uv+d*vec2(1.0,0.0)).rgb); gx += -luminance(sample_source(in.uv+d*vec2(-1.0,1.0)).rgb)+luminance(sample_source(in.uv+d*vec2(1.0,1.0)).rgb);
|
||||
gy += -luminance(sample_source(in.uv+d*vec2(-1.0,-1.0)).rgb)-2.0*luminance(sample_source(in.uv+d*vec2(0.0,-1.0)).rgb)-luminance(sample_source(in.uv+d*vec2(1.0,-1.0)).rgb); gy += luminance(sample_source(in.uv+d*vec2(-1.0,1.0)).rgb)+2.0*luminance(sample_source(in.uv+d*vec2(0.0,1.0)).rgb)+luminance(sample_source(in.uv+d*vec2(1.0,1.0)).rgb);
|
||||
let edge=clamp(length(vec2(gx,gy))*parameters.a.x,0.0,1.0); return vec4(vec3(edge),1.0);
|
||||
let edge=clamp(length(vec2(gx,gy))*parameters.values[0].x,0.0,1.0); return vec4(vec3(edge),1.0);
|
||||
}
|
||||
|
||||
+128
-49
@@ -27,6 +27,104 @@ pub use pipeline_library::PipelineLibrary;
|
||||
|
||||
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||
|
||||
#[repr(C, align(16))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct FullscreenUniforms {
|
||||
values: [[f32; 4]; 8],
|
||||
}
|
||||
|
||||
fn pack_fullscreen_uniforms(
|
||||
key: &str,
|
||||
parameters: &crate::render_graph::NormalizedParameters,
|
||||
) -> Option<FullscreenUniforms> {
|
||||
use crate::render_graph::NormalizedParameters;
|
||||
let first = match (key, parameters) {
|
||||
(
|
||||
"fullscreen_copy" | "frame_out",
|
||||
NormalizedParameters::FullscreenCopy | NormalizedParameters::FrameOut,
|
||||
) => [0.; 4],
|
||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => [*exposure, 0., 0., 0.],
|
||||
("bloom_extract", NormalizedParameters::BloomExtract { threshold, knee }) => {
|
||||
[*threshold, *knee, 0., 0.]
|
||||
}
|
||||
("bloom_blur", NormalizedParameters::BloomBlur { direction, radius }) => {
|
||||
[direction[0], direction[1], *radius, 0.]
|
||||
}
|
||||
("bloom_composite", NormalizedParameters::BloomComposite { intensity }) => {
|
||||
[*intensity, 0., 0., 0.]
|
||||
}
|
||||
("luminance_edge", NormalizedParameters::LuminanceEdge { strength }) => {
|
||||
[*strength, 0., 0., 0.]
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let mut values = [[0.; 4]; 8];
|
||||
values[0] = first;
|
||||
Some(FullscreenUniforms { values })
|
||||
}
|
||||
|
||||
fn resolve_fullscreen_entry(key: &str) -> Option<&'static str> {
|
||||
match key {
|
||||
"fullscreen_copy" | "frame_out" => Some("fs_copy"),
|
||||
"tone_map" => Some("fs_tone_map"),
|
||||
"bloom_extract" => Some("fs_bloom_extract"),
|
||||
"bloom_blur" => Some("fs_bloom_blur"),
|
||||
"bloom_composite" => Some("fs_bloom_composite"),
|
||||
"luminance_edge" => Some("fs_luminance_edge"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod fullscreen_tests {
|
||||
use super::*;
|
||||
use crate::render_graph::NormalizedParameters;
|
||||
|
||||
#[test]
|
||||
fn fullscreen_uniform_abi_and_packer_are_fixed() {
|
||||
assert_eq!(std::mem::size_of::<FullscreenUniforms>(), 128);
|
||||
assert_eq!(std::mem::align_of::<FullscreenUniforms>(), 16);
|
||||
let packed = pack_fullscreen_uniforms(
|
||||
"bloom_blur",
|
||||
&NormalizedParameters::BloomBlur {
|
||||
direction: [0.0, 1.0],
|
||||
radius: 3.0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packed.values[0], [0.0, 1.0, 3.0, 0.0]);
|
||||
assert!(packed.values[1..].iter().all(|value| *value == [0.0; 4]));
|
||||
assert_eq!(bytemuck::bytes_of(&packed).len(), 128);
|
||||
assert!(
|
||||
pack_fullscreen_uniforms("tone_map", &NormalizedParameters::FullscreenCopy).is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_entries_are_explicit() {
|
||||
assert_eq!(resolve_fullscreen_entry("fullscreen_copy"), Some("fs_copy"));
|
||||
assert_eq!(resolve_fullscreen_entry("frame_out"), Some("fs_copy"));
|
||||
assert_eq!(resolve_fullscreen_entry("tone_map"), Some("fs_tone_map"));
|
||||
assert_eq!(
|
||||
resolve_fullscreen_entry("bloom_extract"),
|
||||
Some("fs_bloom_extract")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_fullscreen_entry("bloom_blur"),
|
||||
Some("fs_bloom_blur")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_fullscreen_entry("bloom_composite"),
|
||||
Some("fs_bloom_composite")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_fullscreen_entry("luminance_edge"),
|
||||
Some("fs_luminance_edge")
|
||||
);
|
||||
assert_eq!(resolve_fullscreen_entry("unknown"), None);
|
||||
}
|
||||
}
|
||||
|
||||
struct GpuTextureSlot {
|
||||
_texture: wgpu::Texture,
|
||||
view: wgpu::TextureView,
|
||||
@@ -840,7 +938,7 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
min_binding_size: wgpu::BufferSize::new(128),
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
@@ -872,11 +970,14 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
});
|
||||
let mut executions = Vec::new();
|
||||
for (index, execution) in graph.executions.iter().enumerate() {
|
||||
let contract = crate::render_graph::contract(&execution.executor.key)
|
||||
.ok_or_else(|| fail("executor contract missing"))?;
|
||||
match execution.executor.key.as_str() {
|
||||
"frustum_cull" => executions.push(PreparedExecution::FrustumCull),
|
||||
"mesh_query" => executions.push(PreparedExecution::MeshQuery),
|
||||
"frame_out" | "fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur"
|
||||
| "bloom_composite" | "luminance_edge" => {
|
||||
_ if execution.executor.key == "frame_out"
|
||||
|| contract.fullscreen_policy.is_some() =>
|
||||
{
|
||||
let frame_out = execution.executor.key == "frame_out";
|
||||
let (source, second) = if frame_out {
|
||||
let ExecutionKind::FrameOut { color } = execution.kind else {
|
||||
@@ -884,51 +985,37 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
};
|
||||
(color, color)
|
||||
} else {
|
||||
match execution.inputs.as_slice() {
|
||||
[source, _color_target] => (source.resource, source.resource),
|
||||
[source, bloom, _color_target]
|
||||
if execution.executor.key == "bloom_composite" =>
|
||||
{
|
||||
(source.resource, bloom.resource)
|
||||
}
|
||||
let sampled: Vec<_> = contract
|
||||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, input)| {
|
||||
input.role == crate::render_graph::InputRole::SampledTexture
|
||||
})
|
||||
.map(|(index, _)| {
|
||||
execution.inputs.get(index).map(|input| input.resource)
|
||||
})
|
||||
.collect::<Option<_>>()
|
||||
.ok_or_else(|| fail("fullscreen inputs mismatch"))?;
|
||||
match (contract.fullscreen_policy, sampled.as_slice()) {
|
||||
(
|
||||
Some(crate::render_graph::FullscreenPolicy::BloomComposite),
|
||||
[source, second],
|
||||
) => (*source, *second),
|
||||
(Some(_), [source]) => (*source, *source),
|
||||
_ => return Err(fail("fullscreen inputs mismatch")),
|
||||
}
|
||||
};
|
||||
let values: [f32; 8] =
|
||||
match (execution.executor.key.as_str(), &execution.parameters) {
|
||||
(
|
||||
"fullscreen_copy" | "frame_out",
|
||||
NormalizedParameters::FullscreenCopy
|
||||
| NormalizedParameters::FrameOut,
|
||||
) => [0.; 8],
|
||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => {
|
||||
[*exposure, 0., 0., 0., 0., 0., 0., 0.]
|
||||
}
|
||||
(
|
||||
"bloom_extract",
|
||||
NormalizedParameters::BloomExtract { threshold, knee },
|
||||
) => [*threshold, *knee, 0., 0., 0., 0., 0., 0.],
|
||||
(
|
||||
"bloom_blur",
|
||||
NormalizedParameters::BloomBlur { direction, radius },
|
||||
) => [direction[0], direction[1], *radius, 0., 0., 0., 0., 0.],
|
||||
(
|
||||
"bloom_composite",
|
||||
NormalizedParameters::BloomComposite { intensity },
|
||||
) => [*intensity, 0., 0., 0., 0., 0., 0., 0.],
|
||||
(
|
||||
"luminance_edge",
|
||||
NormalizedParameters::LuminanceEdge { strength },
|
||||
) => [*strength, 0., 0., 0., 0., 0., 0., 0.],
|
||||
_ => return Err(fail("executor parameters mismatch")),
|
||||
};
|
||||
let values =
|
||||
pack_fullscreen_uniforms(&execution.executor.key, &execution.parameters)
|
||||
.ok_or_else(|| fail("executor parameters mismatch"))?;
|
||||
use wgpu::util::DeviceExt;
|
||||
let uniform =
|
||||
self.context
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some(" post parameters"),
|
||||
contents: bytemuck::cast_slice(&values),
|
||||
contents: bytemuck::bytes_of(&values),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let target_format = if frame_out {
|
||||
@@ -960,16 +1047,8 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
.descriptor
|
||||
.format
|
||||
};
|
||||
let entry = match execution.executor.key.as_str() {
|
||||
"fullscreen_copy" => "fs_copy",
|
||||
"tone_map" => "fs_tone_map",
|
||||
"bloom_extract" => "fs_bloom_extract",
|
||||
"bloom_blur" => "fs_bloom_blur",
|
||||
"bloom_composite" => "fs_bloom_composite",
|
||||
"luminance_edge" => "fs_luminance_edge",
|
||||
"frame_out" => "fs_copy",
|
||||
_ => return Err(fail("fullscreen executor mismatch")),
|
||||
};
|
||||
let entry = resolve_fullscreen_entry(&execution.executor.key)
|
||||
.ok_or_else(|| fail("fullscreen executor mismatch"))?;
|
||||
let pipeline = self.context.device.create_render_pipeline(
|
||||
&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(" post pipeline"),
|
||||
|
||||
@@ -172,7 +172,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
if (
|
||||
!exactKeys(n, nodeKeys) ||
|
||||
n.known !== true ||
|
||||
n.typeVersion !== 1 ||
|
||||
n.typeVersion !== descriptor.version ||
|
||||
typeof n.muted !== "boolean" ||
|
||||
typeof n.collapsed !== "boolean" ||
|
||||
typeof n.label !== "string" ||
|
||||
@@ -340,7 +340,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
value: {
|
||||
id: n.id,
|
||||
state: n.muted ? "muted" : "enabled",
|
||||
executor: { key: n.typeId, version: 1 },
|
||||
executor: { key: n.typeId, version: descriptor.version },
|
||||
parameters,
|
||||
inputs: {},
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ const texture = {
|
||||
};
|
||||
export const semanticCatalog = Object.freeze({
|
||||
mesh: {
|
||||
version: 1,
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: {
|
||||
@@ -39,6 +40,7 @@ export const semanticCatalog = Object.freeze({
|
||||
parameters: {},
|
||||
},
|
||||
texture: {
|
||||
version: 1,
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: { texture: o("texture") },
|
||||
@@ -60,6 +62,7 @@ export const semanticCatalog = Object.freeze({
|
||||
},
|
||||
},
|
||||
frustum_cull: {
|
||||
version: 1,
|
||||
execution: "compute",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
@@ -74,6 +77,7 @@ export const semanticCatalog = Object.freeze({
|
||||
parameters: { cameraSelection: "active" },
|
||||
},
|
||||
mesh_query: {
|
||||
version: 1,
|
||||
execution: "compute",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
@@ -87,12 +91,14 @@ export const semanticCatalog = Object.freeze({
|
||||
},
|
||||
},
|
||||
pipeline_registry: {
|
||||
version: 1,
|
||||
execution: "cpu_preparation",
|
||||
inputs: { pipelineIndices: i("pipeline_index_stream") },
|
||||
outputs: { activation: o("pipeline_activation") },
|
||||
parameters: {},
|
||||
},
|
||||
pipeline: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
@@ -105,6 +111,7 @@ export const semanticCatalog = Object.freeze({
|
||||
parameters: { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] },
|
||||
},
|
||||
fullscreen_copy: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
@@ -114,6 +121,7 @@ export const semanticCatalog = Object.freeze({
|
||||
parameters: {},
|
||||
},
|
||||
tone_map: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
@@ -123,6 +131,7 @@ export const semanticCatalog = Object.freeze({
|
||||
parameters: { exposure: 1 },
|
||||
},
|
||||
bloom_extract: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
@@ -132,6 +141,7 @@ export const semanticCatalog = Object.freeze({
|
||||
parameters: { threshold: 1, knee: 0.5 },
|
||||
},
|
||||
bloom_blur: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
@@ -141,6 +151,7 @@ export const semanticCatalog = Object.freeze({
|
||||
parameters: { direction: [1, 0], radius: 1 },
|
||||
},
|
||||
bloom_composite: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
@@ -151,6 +162,7 @@ export const semanticCatalog = Object.freeze({
|
||||
parameters: { intensity: 1 },
|
||||
},
|
||||
luminance_edge: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
@@ -160,6 +172,7 @@ export const semanticCatalog = Object.freeze({
|
||||
parameters: { strength: 2 },
|
||||
},
|
||||
frame_out: {
|
||||
version: 1,
|
||||
execution: "frame",
|
||||
inputs: { color: i("texture") },
|
||||
outputs: {},
|
||||
@@ -390,7 +403,7 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
return [
|
||||
key,
|
||||
{
|
||||
version: 1,
|
||||
version: c.version,
|
||||
title: key.replaceAll("_", " "),
|
||||
behavior: "standard",
|
||||
style: c.execution,
|
||||
@@ -427,7 +440,7 @@ export const descriptors = Object.fromEntries(
|
||||
Object.entries(semanticCatalog).map(([key, c]) => [
|
||||
key,
|
||||
{
|
||||
version: 1,
|
||||
version: c.version,
|
||||
inputs: c.inputs,
|
||||
outputs: c.outputs,
|
||||
parameters: c.parameters,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { descriptors } from "./catalog.js";
|
||||
|
||||
const input = (node, socket) => ({ node, socket });
|
||||
const node = (id, key, parameters = {}, inputs = {}) => ({
|
||||
id, state: "enabled", executor: { key, version: 1 }, parameters, inputs,
|
||||
id, state: "enabled", executor: { key, version: descriptors[key].version }, parameters, inputs,
|
||||
});
|
||||
const texture = (format, scale = 1) => ({
|
||||
texture: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
nodeDefinitions,
|
||||
GRAPH_ID,
|
||||
CATALOG_VERSION,
|
||||
descriptors,
|
||||
socketTypes,
|
||||
} from "../static/render-graph/catalog.js";
|
||||
import { culling } from "../static/render-graph/presets.js";
|
||||
@@ -93,6 +94,12 @@ function fixture() {
|
||||
};
|
||||
}
|
||||
test("catalog exhaustively mirrors all current contracts", () => {
|
||||
for (const [key, semantic] of Object.entries(semanticCatalog)) {
|
||||
assert.ok(Object.hasOwn(semantic, "version"));
|
||||
assert.equal(semantic.version, 1);
|
||||
assert.equal(nodeDefinitions[key].version, semantic.version);
|
||||
assert.equal(descriptors[key].version, semantic.version);
|
||||
}
|
||||
assert.deepEqual(
|
||||
Object.keys(semanticCatalog),
|
||||
[
|
||||
@@ -306,6 +313,7 @@ test("adapter rejects hostile shape, IDs, duplicates, catalog, sockets and type
|
||||
reject((x) => (x.nodes[0].id = "bad id"), "AUTHORING_ID");
|
||||
reject((x) => (x.nodes[1].id = x.nodes[0].id), "AUTHORING_ID_DUPLICATE");
|
||||
reject((x) => (x.nodes[0].typeId = "wat"), "AUTHORING_NODE_TYPE");
|
||||
reject((x) => (x.nodes[0].typeVersion = 2), "AUTHORING_NODE_INVALID");
|
||||
reject((x) => (x.nodes[0].sockets = []), "AUTHORING_SOCKET_SET");
|
||||
reject((x) => (x.links[0].toSocketId = "missing:x"), "AUTHORING_LINK");
|
||||
reject((x) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import * as presets from "../static/render-graph/presets.js";
|
||||
import { descriptors } from "../static/render-graph/catalog.js";
|
||||
|
||||
const order = [
|
||||
"midnight",
|
||||
@@ -168,6 +169,11 @@ test("presets have the exact canonical pipeline identities, schemas, and node se
|
||||
),
|
||||
);
|
||||
assert.ok(!graph.nodes.some((node) => node.id === "copy"));
|
||||
assert.ok(
|
||||
graph.nodes.every(
|
||||
(node) => node.executor.version === descriptors[node.executor.key].version,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user