feat: move display transform into frame out
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:
@@ -38,8 +38,15 @@ struct PipelineParameters {
|
|||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct ToneMapParameters {
|
#[serde(rename_all = "camelCase")]
|
||||||
exposure: f32,
|
struct FrameOutParameters {
|
||||||
|
hdr_enabled: bool,
|
||||||
|
tone_mapper: ToneMapper,
|
||||||
|
exposure_stops: f32,
|
||||||
|
output_transfer: OutputTransfer,
|
||||||
|
scale_mode: ScaleMode,
|
||||||
|
filter: FrameFilter,
|
||||||
|
background_color: [f32; 4],
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
@@ -424,13 +431,6 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
|||||||
NormalizedParameters::FrustumCull { camera: p.camera }
|
NormalizedParameters::FrustumCull { camera: p.camera }
|
||||||
}
|
}
|
||||||
"fullscreen_copy" => empty!(NormalizedParameters::FullscreenCopy),
|
"fullscreen_copy" => empty!(NormalizedParameters::FullscreenCopy),
|
||||||
"tone_map" => {
|
|
||||||
let p: ToneMapParameters =
|
|
||||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
|
||||||
NormalizedParameters::ToneMap {
|
|
||||||
exposure: range(p.exposure, 0.0, 32.0, format!("{base}.exposure"))?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"color_balance" => {
|
"color_balance" => {
|
||||||
let p: ColorBalanceParameters =
|
let p: ColorBalanceParameters =
|
||||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||||
@@ -528,7 +528,36 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
|||||||
strength: range(p.strength, 0.0, 16.0, format!("{base}.strength"))?,
|
strength: range(p.strength, 0.0, 16.0, format!("{base}.strength"))?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"frame_out" => empty!(NormalizedParameters::FrameOut),
|
"frame_out" => {
|
||||||
|
let p: FrameOutParameters =
|
||||||
|
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||||
|
let exposure_stops = range(
|
||||||
|
p.exposure_stops,
|
||||||
|
-10.0,
|
||||||
|
10.0,
|
||||||
|
format!("{base}.exposureStops"),
|
||||||
|
)?;
|
||||||
|
let background_color = components(
|
||||||
|
p.background_color,
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
&format!("{base}.backgroundColor"),
|
||||||
|
)?;
|
||||||
|
NormalizedParameters::FrameOut {
|
||||||
|
dynamic_range: if p.hdr_enabled {
|
||||||
|
FrameDynamicRange::Hdr {
|
||||||
|
tone_mapper: p.tone_mapper,
|
||||||
|
exposure_stops,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FrameDynamicRange::Sdr
|
||||||
|
},
|
||||||
|
output_transfer: p.output_transfer,
|
||||||
|
scale_mode: p.scale_mode,
|
||||||
|
filter: p.filter,
|
||||||
|
background_color,
|
||||||
|
}
|
||||||
|
}
|
||||||
"texture" => {
|
"texture" => {
|
||||||
let p: TextureParameters =
|
let p: TextureParameters =
|
||||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||||
@@ -1378,12 +1407,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
&& descriptor.extent == source_descriptor.extent
|
&& descriptor.extent == source_descriptor.extent
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}),
|
|
||||||
Some(FullscreenPolicy::BloomExtract) => authored_target_ok,
|
Some(FullscreenPolicy::BloomExtract) => authored_target_ok,
|
||||||
Some(FullscreenPolicy::HdrSameExtent) => authored_target_ok && target_matches_source,
|
Some(FullscreenPolicy::HdrSameExtent) => authored_target_ok && target_matches_source,
|
||||||
Some(FullscreenPolicy::BloomComposite) => {
|
Some(FullscreenPolicy::BloomComposite) => {
|
||||||
@@ -1418,10 +1441,19 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
};
|
};
|
||||||
let TextureFamilySource::AuthoredTexture { descriptor, .. } =
|
let TextureFamilySource::AuthoredTexture { descriptor, .. } =
|
||||||
&families[family as usize].source;
|
&families[family as usize].source;
|
||||||
if !is_filterable_frame_color(descriptor) {
|
let NormalizedParameters::FrameOut { dynamic_range, .. } = ¶ms[i] else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
if !frame_out_source_compatible(descriptor, dynamic_range) {
|
||||||
|
let message = match dynamic_range {
|
||||||
|
FrameDynamicRange::Hdr { .. } => "HDR frame output requires rgba16_float",
|
||||||
|
FrameDynamicRange::Sdr => {
|
||||||
|
"SDR frame output requires a linear filterable color texture"
|
||||||
|
}
|
||||||
|
};
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_ILLEGAL_ACCESS",
|
"GRAPH_ILLEGAL_ACCESS",
|
||||||
"frame output requires a filterable single-view d2 color texture",
|
message,
|
||||||
format!("nodes[{i}].inputs.color"),
|
format!("nodes[{i}].inputs.color"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1903,17 +1935,20 @@ pub(super) fn is_single_view_d2(descriptor: &NormalizedTextureDescriptor) -> boo
|
|||||||
&& descriptor.sample_count == 1
|
&& descriptor.sample_count == 1
|
||||||
&& descriptor.mip_level_count == 1
|
&& descriptor.mip_level_count == 1
|
||||||
&& extent_layers(&descriptor.extent) == 1
|
&& extent_layers(&descriptor.extent) == 1
|
||||||
|
&& descriptor.view_formats.is_empty()
|
||||||
}
|
}
|
||||||
pub(super) fn is_filterable_frame_color(descriptor: &NormalizedTextureDescriptor) -> bool {
|
pub(super) fn frame_out_source_compatible(
|
||||||
|
descriptor: &NormalizedTextureDescriptor,
|
||||||
|
dynamic_range: &FrameDynamicRange,
|
||||||
|
) -> bool {
|
||||||
is_single_view_d2(descriptor)
|
is_single_view_d2(descriptor)
|
||||||
&& matches!(
|
&& match dynamic_range {
|
||||||
|
FrameDynamicRange::Hdr { .. } => descriptor.format == TextureFormat::Rgba16Float,
|
||||||
|
FrameDynamicRange::Sdr => matches!(
|
||||||
descriptor.format,
|
descriptor.format,
|
||||||
TextureFormat::Rgba8Unorm
|
TextureFormat::Rgba8Unorm | TextureFormat::Bgra8Unorm | TextureFormat::Rgba16Float
|
||||||
| TextureFormat::Rgba8UnormSrgb
|
),
|
||||||
| TextureFormat::Bgra8Unorm
|
}
|
||||||
| TextureFormat::Bgra8UnormSrgb
|
|
||||||
| TextureFormat::Rgba16Float
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
pub(super) fn texture_usage(
|
pub(super) fn texture_usage(
|
||||||
f: &TextureFamily,
|
f: &TextureFamily,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ pub enum ExecutionClass {
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum FullscreenPolicy {
|
pub enum FullscreenPolicy {
|
||||||
Copy,
|
Copy,
|
||||||
ToneMap,
|
|
||||||
HdrSameExtent,
|
HdrSameExtent,
|
||||||
BloomExtract,
|
BloomExtract,
|
||||||
BloomComposite,
|
BloomComposite,
|
||||||
@@ -335,15 +334,6 @@ pub static CONTRACTS: &[Contract] = &[
|
|||||||
inherently_observable: false,
|
inherently_observable: false,
|
||||||
fullscreen_policy: Some(FullscreenPolicy::Copy),
|
fullscreen_policy: Some(FullscreenPolicy::Copy),
|
||||||
},
|
},
|
||||||
Contract {
|
|
||||||
key: "tone_map",
|
|
||||||
version: 1,
|
|
||||||
execution: ExecutionClass::Render,
|
|
||||||
inputs: FULLSCREEN_COPY_IN,
|
|
||||||
outputs: FULLSCREEN_COPY_OUT,
|
|
||||||
inherently_observable: false,
|
|
||||||
fullscreen_policy: Some(FullscreenPolicy::ToneMap),
|
|
||||||
},
|
|
||||||
Contract {
|
Contract {
|
||||||
key: "color_balance",
|
key: "color_balance",
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -418,7 +408,7 @@ pub static CONTRACTS: &[Contract] = &[
|
|||||||
},
|
},
|
||||||
Contract {
|
Contract {
|
||||||
key: "frame_out",
|
key: "frame_out",
|
||||||
version: 1,
|
version: 2,
|
||||||
execution: ExecutionClass::Frame,
|
execution: ExecutionClass::Frame,
|
||||||
inputs: FRAME_OUT_IN,
|
inputs: FRAME_OUT_IN,
|
||||||
outputs: NONE_OUT,
|
outputs: NONE_OUT,
|
||||||
|
|||||||
@@ -209,9 +209,6 @@ pub enum NormalizedParameters {
|
|||||||
clear_color: [f64; 4],
|
clear_color: [f64; 4],
|
||||||
},
|
},
|
||||||
FullscreenCopy,
|
FullscreenCopy,
|
||||||
ToneMap {
|
|
||||||
exposure: f32,
|
|
||||||
},
|
|
||||||
ColorBalance {
|
ColorBalance {
|
||||||
mode: ColorBalanceMode,
|
mode: ColorBalanceMode,
|
||||||
factor: f32,
|
factor: f32,
|
||||||
@@ -258,7 +255,50 @@ pub enum NormalizedParameters {
|
|||||||
LuminanceEdge {
|
LuminanceEdge {
|
||||||
strength: f32,
|
strength: f32,
|
||||||
},
|
},
|
||||||
FrameOut,
|
FrameOut {
|
||||||
|
dynamic_range: FrameDynamicRange,
|
||||||
|
output_transfer: OutputTransfer,
|
||||||
|
scale_mode: ScaleMode,
|
||||||
|
filter: FrameFilter,
|
||||||
|
background_color: [f32; 4],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, serde::Deserialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum FrameDynamicRange {
|
||||||
|
Sdr,
|
||||||
|
Hdr {
|
||||||
|
tone_mapper: ToneMapper,
|
||||||
|
exposure_stops: f32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ToneMapper {
|
||||||
|
Aces,
|
||||||
|
Reinhard,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum OutputTransfer {
|
||||||
|
Srgb,
|
||||||
|
Linear,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ScaleMode {
|
||||||
|
Stretch,
|
||||||
|
Contain,
|
||||||
|
Cover,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum FrameFilter {
|
||||||
|
Linear,
|
||||||
|
Nearest,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||||
|
|||||||
@@ -330,9 +330,6 @@ fn validate_fullscreen_execution(
|
|||||||
let vector = |v: &[f32], min, max| v.iter().all(|x| scalar(x, min, max));
|
let vector = |v: &[f32], min, max| v.iter().all(|x| scalar(x, min, max));
|
||||||
let valid_parameters = match (key, &execution.parameters) {
|
let valid_parameters = match (key, &execution.parameters) {
|
||||||
("fullscreen_copy", NormalizedParameters::FullscreenCopy) => true,
|
("fullscreen_copy", NormalizedParameters::FullscreenCopy) => true,
|
||||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => {
|
|
||||||
exposure.is_finite() && (0.0..=32.0).contains(exposure)
|
|
||||||
}
|
|
||||||
(
|
(
|
||||||
"color_balance",
|
"color_balance",
|
||||||
NormalizedParameters::ColorBalance {
|
NormalizedParameters::ColorBalance {
|
||||||
@@ -566,14 +563,6 @@ fn validate_fullscreen_execution(
|
|||||||
&& target_d.format != TextureFormat::Depth32Float
|
&& target_d.format != TextureFormat::Depth32Float
|
||||||
&& target_d.extent == source.extent
|
&& target_d.extent == source.extent
|
||||||
}
|
}
|
||||||
FullscreenPolicy::ToneMap => {
|
|
||||||
single_view_d2(target_d)
|
|
||||||
&& !matches!(
|
|
||||||
target_d.format,
|
|
||||||
TextureFormat::Depth32Float | TextureFormat::R32Float
|
|
||||||
)
|
|
||||||
&& target_d.extent == source.extent
|
|
||||||
}
|
|
||||||
FullscreenPolicy::BloomExtract => hdr(target_d),
|
FullscreenPolicy::BloomExtract => hdr(target_d),
|
||||||
FullscreenPolicy::HdrSameExtent => hdr(target_d) && target_d.extent == source.extent,
|
FullscreenPolicy::HdrSameExtent => hdr(target_d) && target_d.extent == source.extent,
|
||||||
FullscreenPolicy::BloomComposite => {
|
FullscreenPolicy::BloomComposite => {
|
||||||
@@ -1477,11 +1466,40 @@ pub fn prepare_runtime_plan(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let frame_out = &graph.executions[frame_out_index];
|
let frame_out = &graph.executions[frame_out_index];
|
||||||
if !matches!(frame_out.parameters, NormalizedParameters::FrameOut) {
|
let NormalizedParameters::FrameOut {
|
||||||
|
dynamic_range,
|
||||||
|
output_transfer,
|
||||||
|
scale_mode: _,
|
||||||
|
filter: _,
|
||||||
|
background_color,
|
||||||
|
} = &frame_out.parameters
|
||||||
|
else {
|
||||||
return Err(invalid(
|
return Err(invalid(
|
||||||
"frame_out parameters mismatch",
|
"frame_out parameters mismatch",
|
||||||
format!("executions[{frame_out_index}].parameters"),
|
format!("executions[{frame_out_index}].parameters"),
|
||||||
));
|
));
|
||||||
|
};
|
||||||
|
let parameters_valid = background_color
|
||||||
|
.iter()
|
||||||
|
.all(|v| v.is_finite() && (0.0..=1.0).contains(v))
|
||||||
|
&& match dynamic_range {
|
||||||
|
FrameDynamicRange::Sdr => true,
|
||||||
|
FrameDynamicRange::Hdr { exposure_stops, .. } => {
|
||||||
|
exposure_stops.is_finite() && (-10.0..=10.0).contains(exposure_stops)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !parameters_valid {
|
||||||
|
return Err(invalid(
|
||||||
|
"frame_out parameters are out of range",
|
||||||
|
format!("executions[{frame_out_index}].parameters"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if *output_transfer == OutputTransfer::Linear && surface.format.is_srgb() {
|
||||||
|
return Err(error(
|
||||||
|
"GRAPH_SURFACE_INCOMPATIBLE",
|
||||||
|
"linear output transfer cannot target an sRGB surface",
|
||||||
|
format!("executions[{frame_out_index}].parameters.outputTransfer"),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let ExecutionKind::FrameOut { color } = frame_out.kind else {
|
let ExecutionKind::FrameOut { color } = frame_out.kind else {
|
||||||
return Err(invalid(
|
return Err(invalid(
|
||||||
@@ -1546,7 +1564,7 @@ pub fn prepare_runtime_plan(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let TextureFamilySource::AuthoredTexture { descriptor, .. } = &family.source;
|
let TextureFamilySource::AuthoredTexture { descriptor, .. } = &family.source;
|
||||||
if !super::compiler::is_filterable_frame_color(descriptor) {
|
if !super::compiler::frame_out_source_compatible(descriptor, dynamic_range) {
|
||||||
return Err(invalid(
|
return Err(invalid(
|
||||||
"frame_out texture descriptor is incompatible",
|
"frame_out texture descriptor is incompatible",
|
||||||
format!("textureFamilies[{}].source.descriptor", family.id),
|
format!("textureFamilies[{}].source.descriptor", family.id),
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ use serde_json::{json, Value};
|
|||||||
fn input(node: &str, socket: &str) -> Value {
|
fn input(node: &str, socket: &str) -> Value {
|
||||||
json!({"node":node,"socket":socket})
|
json!({"node":node,"socket":socket})
|
||||||
}
|
}
|
||||||
fn node(id: &str, key: &str, parameters: Value, inputs: Value) -> Value {
|
fn node(id: &str, key: &str, mut parameters: Value, inputs: Value) -> Value {
|
||||||
json!({"id":id,"state":"enabled","executor":{"key":key,"version":1},"parameters":parameters,"inputs":inputs})
|
if key == "frame_out" && parameters.as_object().is_some_and(|p| p.is_empty()) {
|
||||||
|
parameters = json!({"hdrEnabled":false,"toneMapper":"aces","exposureStops":0,"outputTransfer":"srgb","scaleMode":"stretch","filter":"linear","backgroundColor":[0,0,0,1]});
|
||||||
|
}
|
||||||
|
json!({"id":id,"state":"enabled","executor":{"key":key,"version":if key == "frame_out" { 2 } else { 1 }},"parameters":parameters,"inputs":inputs})
|
||||||
}
|
}
|
||||||
fn texture(format: &str, residency: &str) -> Value {
|
fn texture(format: &str, residency: &str) -> Value {
|
||||||
json!({"texture":{"dimension":"d2","format":format,"extent":{"kind":"surface_relative","width":{"numerator":1,"denominator":1},"height":{"numerator":1,"denominator":1},"depthOrArrayLayers":1},"mipLevelCount":1,"sampleCount":1,"viewFormats":[]},"residency":residency})
|
json!({"texture":{"dimension":"d2","format":format,"extent":{"kind":"surface_relative","width":{"numerator":1,"denominator":1},"height":{"numerator":1,"denominator":1},"depthOrArrayLayers":1},"mipLevelCount":1,"sampleCount":1,"viewFormats":[]},"residency":residency})
|
||||||
@@ -247,7 +250,7 @@ fn fullscreen_copy_parameters_are_exactly_empty() {
|
|||||||
let copy = node_index(&g, "copy");
|
let copy = node_index(&g, "copy");
|
||||||
g["nodes"][copy]["parameters"] = json!({"obsolete":true});
|
g["nodes"][copy]["parameters"] = json!({"obsolete":true});
|
||||||
assert_eq!(compile_error(g).code, "GRAPH_PARAMETERS_INVALID");
|
assert_eq!(compile_error(g).code, "GRAPH_PARAMETERS_INVALID");
|
||||||
assert_eq!(CONTRACTS.len(), 17);
|
assert_eq!(CONTRACTS.len(), 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -839,7 +842,6 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
|||||||
("pipeline_registry", 1),
|
("pipeline_registry", 1),
|
||||||
("pipeline", 1),
|
("pipeline", 1),
|
||||||
("fullscreen_copy", 1),
|
("fullscreen_copy", 1),
|
||||||
("tone_map", 1),
|
|
||||||
("color_balance", 1),
|
("color_balance", 1),
|
||||||
("exposure_contrast", 1),
|
("exposure_contrast", 1),
|
||||||
("saturation", 1),
|
("saturation", 1),
|
||||||
@@ -848,7 +850,7 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
|||||||
("bloom_blur", 1),
|
("bloom_blur", 1),
|
||||||
("bloom_composite", 1),
|
("bloom_composite", 1),
|
||||||
("luminance_edge", 1),
|
("luminance_edge", 1),
|
||||||
("frame_out", 1),
|
("frame_out", 2),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -864,7 +866,6 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
|||||||
("pipeline_registry", None),
|
("pipeline_registry", None),
|
||||||
("pipeline", None),
|
("pipeline", None),
|
||||||
("fullscreen_copy", Some(FullscreenPolicy::Copy)),
|
("fullscreen_copy", Some(FullscreenPolicy::Copy)),
|
||||||
("tone_map", Some(FullscreenPolicy::ToneMap)),
|
|
||||||
("color_balance", Some(FullscreenPolicy::HdrSameExtent)),
|
("color_balance", Some(FullscreenPolicy::HdrSameExtent)),
|
||||||
("exposure_contrast", Some(FullscreenPolicy::HdrSameExtent)),
|
("exposure_contrast", Some(FullscreenPolicy::HdrSameExtent)),
|
||||||
("saturation", Some(FullscreenPolicy::HdrSameExtent)),
|
("saturation", Some(FullscreenPolicy::HdrSameExtent)),
|
||||||
@@ -2083,6 +2084,41 @@ fn assert_runtime_path(graph: &CompiledGraph, path: impl AsRef<str>) {
|
|||||||
assert_eq!(error.details["path"], path.as_ref());
|
assert_eq!(error.details["path"], path.as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mutate_texture_descriptor(
|
||||||
|
graph: &mut CompiledGraph,
|
||||||
|
resource: u32,
|
||||||
|
mutate: impl FnOnce(&mut NormalizedTextureDescriptor),
|
||||||
|
) {
|
||||||
|
let (family, allocation) = match graph.resources[resource as usize].plan {
|
||||||
|
ResourcePlan::Texture {
|
||||||
|
family, allocation, ..
|
||||||
|
} => (family as usize, allocation.unwrap()),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
let TextureFamilySource::AuthoredTexture {
|
||||||
|
resource: source,
|
||||||
|
descriptor,
|
||||||
|
..
|
||||||
|
} = &mut graph.texture_families[family].source;
|
||||||
|
mutate(descriptor);
|
||||||
|
let descriptor = descriptor.clone();
|
||||||
|
let ResourcePlan::TextureSource {
|
||||||
|
descriptor: source_descriptor,
|
||||||
|
..
|
||||||
|
} = &mut graph.resources[*source as usize].plan
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
*source_descriptor = descriptor.clone();
|
||||||
|
let key = &mut graph.allocation_classes[allocation.class as usize].key;
|
||||||
|
key.dimension = descriptor.dimension;
|
||||||
|
key.format = descriptor.format;
|
||||||
|
key.extent = descriptor.extent;
|
||||||
|
key.mip_level_count = descriptor.mip_level_count;
|
||||||
|
key.sample_count = descriptor.sample_count;
|
||||||
|
key.view_formats = descriptor.view_formats;
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_rejects_coordinated_contract_and_texture_target_mutations() {
|
fn runtime_rejects_coordinated_contract_and_texture_target_mutations() {
|
||||||
let baseline = compile_graph(full_cull_graph());
|
let baseline = compile_graph(full_cull_graph());
|
||||||
@@ -2920,15 +2956,6 @@ fn runtime_rejects_coordinated_executor_frustum_and_fullscreen_mutations() {
|
|||||||
.position(|execution| execution.executor.key == "fullscreen_copy")
|
.position(|execution| execution.executor.key == "fullscreen_copy")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut graph = post.clone();
|
let mut graph = post.clone();
|
||||||
graph.executions[copy].executor.key = "tone_map".into();
|
|
||||||
assert_runtime_path(&graph, format!("executions[{copy}].parameters"));
|
|
||||||
for exposure in [f32::NAN, -0.1, 32.1] {
|
|
||||||
let mut graph = post.clone();
|
|
||||||
graph.executions[copy].executor.key = "tone_map".into();
|
|
||||||
graph.executions[copy].parameters = NormalizedParameters::ToneMap { exposure };
|
|
||||||
assert_runtime_path(&graph, format!("executions[{copy}].parameters"));
|
|
||||||
}
|
|
||||||
let mut graph = post.clone();
|
|
||||||
let ExecutionKind::Render {
|
let ExecutionKind::Render {
|
||||||
color_attachments, ..
|
color_attachments, ..
|
||||||
} = &mut graph.executions[copy].kind
|
} = &mut graph.executions[copy].kind
|
||||||
@@ -2943,6 +2970,242 @@ fn runtime_rejects_coordinated_executor_frustum_and_fullscreen_mutations() {
|
|||||||
assert_runtime_path(&graph, format!("executions[{copy}].kind"));
|
assert_runtime_path(&graph, format!("executions[{copy}].kind"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn frame_parameters(hdr: bool) -> Value {
|
||||||
|
json!({"hdrEnabled":hdr,"toneMapper":"reinhard","exposureStops":2,
|
||||||
|
"outputTransfer":"srgb","scaleMode":"contain","filter":"nearest",
|
||||||
|
"backgroundColor":[0.1,0.2,0.3,0.4]})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_out_v2_has_exact_seven_fields_normalizes_sdr_and_rejects_v1() {
|
||||||
|
let fields = [
|
||||||
|
"hdrEnabled",
|
||||||
|
"toneMapper",
|
||||||
|
"exposureStops",
|
||||||
|
"outputTransfer",
|
||||||
|
"scaleMode",
|
||||||
|
"filter",
|
||||||
|
"backgroundColor",
|
||||||
|
];
|
||||||
|
for field in fields {
|
||||||
|
let mut g = full_cull_graph();
|
||||||
|
let i = node_index(&g, "frame_out");
|
||||||
|
g["nodes"][i]["parameters"] = frame_parameters(false);
|
||||||
|
g["nodes"][i]["parameters"]
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.remove(field);
|
||||||
|
assert_eq!(
|
||||||
|
compile_error(g).code,
|
||||||
|
"GRAPH_PARAMETERS_INVALID",
|
||||||
|
"missing {field}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut g = full_cull_graph();
|
||||||
|
let i = node_index(&g, "frame_out");
|
||||||
|
g["nodes"][i]["parameters"] = frame_parameters(false);
|
||||||
|
g["nodes"][i]["parameters"]["extra"] = json!(0);
|
||||||
|
assert_eq!(compile_error(g).code, "GRAPH_PARAMETERS_INVALID");
|
||||||
|
for (field, bad) in [
|
||||||
|
("toneMapper", json!("bad")),
|
||||||
|
("exposureStops", json!(11)),
|
||||||
|
("backgroundColor", json!([0, 0, -0.1, 1])),
|
||||||
|
] {
|
||||||
|
let mut g = full_cull_graph();
|
||||||
|
let i = node_index(&g, "frame_out");
|
||||||
|
g["nodes"][i]["parameters"] = frame_parameters(false);
|
||||||
|
g["nodes"][i]["parameters"][field] = bad;
|
||||||
|
assert_eq!(
|
||||||
|
compile_error(g).code,
|
||||||
|
"GRAPH_PARAMETERS_INVALID",
|
||||||
|
"hidden {field}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut g = full_cull_graph();
|
||||||
|
let i = node_index(&g, "frame_out");
|
||||||
|
g["nodes"][i]["parameters"] = frame_parameters(false);
|
||||||
|
assert!(matches!(
|
||||||
|
execution(&compile_graph(g), "frame_out").parameters,
|
||||||
|
NormalizedParameters::FrameOut {
|
||||||
|
dynamic_range: FrameDynamicRange::Sdr,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
let mut g = full_cull_graph();
|
||||||
|
let i = node_index(&g, "frame_out");
|
||||||
|
g["nodes"][i]["executor"]["version"] = json!(1);
|
||||||
|
assert_eq!(compile_error(g).code, "GRAPH_EXECUTOR_VERSION_UNSUPPORTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_out_source_format_matrix_is_exact() {
|
||||||
|
let surface = RuntimeSurfaceContract {
|
||||||
|
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: vec![],
|
||||||
|
};
|
||||||
|
for (format, sdr, hdr) in [
|
||||||
|
("rgba8_unorm", true, false),
|
||||||
|
("bgra8_unorm", true, false),
|
||||||
|
("rgba16_float", true, true),
|
||||||
|
("rgba8_unorm_srgb", false, false),
|
||||||
|
("bgra8_unorm_srgb", false, false),
|
||||||
|
("r32_float", false, false),
|
||||||
|
("depth32_float", false, false),
|
||||||
|
] {
|
||||||
|
for (hdr_enabled, accepted) in [(false, sdr), (true, hdr)] {
|
||||||
|
let mut g = full_cull_graph();
|
||||||
|
let i = node_index(&g, "frame_out");
|
||||||
|
if format == "depth32_float" {
|
||||||
|
g["nodes"][i]["inputs"]["color"] = input("pipeline_main", "depth");
|
||||||
|
} else {
|
||||||
|
g["nodes"][0]["parameters"]["texture"]["format"] = json!(format);
|
||||||
|
}
|
||||||
|
g["nodes"][i]["parameters"] = frame_parameters(hdr_enabled);
|
||||||
|
match compile(serde_json::from_value(g).unwrap()) {
|
||||||
|
Ok(compiled) => {
|
||||||
|
assert!(
|
||||||
|
accepted,
|
||||||
|
"unexpected accept: hdr={hdr_enabled} format={format}"
|
||||||
|
);
|
||||||
|
prepare_runtime_plan(&compiled, surface.clone(), None).unwrap();
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
assert!(
|
||||||
|
!accepted,
|
||||||
|
"unexpected reject: hdr={hdr_enabled} format={format}"
|
||||||
|
);
|
||||||
|
let expected_message = if hdr_enabled {
|
||||||
|
"HDR frame output requires rgba16_float"
|
||||||
|
} else {
|
||||||
|
"SDR frame output requires a linear filterable color texture"
|
||||||
|
};
|
||||||
|
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
|
||||||
|
assert_eq!(error.details["path"], format!("nodes[{i}].inputs.color"));
|
||||||
|
assert_eq!(error.message, expected_message);
|
||||||
|
assert_eq!(error.details["message"], expected_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_rejects_every_frame_parameter_lane_and_coordinated_source_mutations() {
|
||||||
|
let baseline = compile_graph(full_cull_graph());
|
||||||
|
let i = baseline
|
||||||
|
.executions
|
||||||
|
.iter()
|
||||||
|
.position(|e| e.executor.key == "frame_out")
|
||||||
|
.unwrap();
|
||||||
|
for version in [1, 3] {
|
||||||
|
let mut g = baseline.clone();
|
||||||
|
g.executions[i].executor.version = version;
|
||||||
|
assert_runtime_path(&g, format!("executions[{i}].executor.version"));
|
||||||
|
}
|
||||||
|
for lane in 0..4 {
|
||||||
|
for value in [f32::NAN, -0.1, 1.1] {
|
||||||
|
let mut g = baseline.clone();
|
||||||
|
let NormalizedParameters::FrameOut {
|
||||||
|
background_color, ..
|
||||||
|
} = &mut g.executions[i].parameters
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
background_color[lane] = value;
|
||||||
|
assert_runtime_path(&g, format!("executions[{i}].parameters"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for exposure in [f32::NAN, -10.1, 10.1] {
|
||||||
|
let mut g = baseline.clone();
|
||||||
|
let NormalizedParameters::FrameOut { dynamic_range, .. } = &mut g.executions[i].parameters
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
*dynamic_range = FrameDynamicRange::Hdr {
|
||||||
|
tone_mapper: ToneMapper::Aces,
|
||||||
|
exposure_stops: exposure,
|
||||||
|
};
|
||||||
|
assert_runtime_path(&g, format!("executions[{i}].parameters"));
|
||||||
|
}
|
||||||
|
let ExecutionKind::FrameOut { color } = baseline.executions[i].kind else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
let family = match baseline.resources[color as usize].plan {
|
||||||
|
ResourcePlan::Texture { family, .. } => family,
|
||||||
|
_ => unreachable!(),
|
||||||
|
} as usize;
|
||||||
|
for format in [
|
||||||
|
TextureFormat::Rgba8UnormSrgb,
|
||||||
|
TextureFormat::Bgra8UnormSrgb,
|
||||||
|
TextureFormat::R32Float,
|
||||||
|
] {
|
||||||
|
let mut g = baseline.clone();
|
||||||
|
mutate_texture_descriptor(&mut g, color, |descriptor| descriptor.format = format);
|
||||||
|
assert_runtime_path(&g, format!("textureFamilies[{family}].source.descriptor"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sdr_to_hdr = baseline.clone();
|
||||||
|
let NormalizedParameters::FrameOut { dynamic_range, .. } =
|
||||||
|
&mut sdr_to_hdr.executions[i].parameters
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
*dynamic_range = FrameDynamicRange::Hdr {
|
||||||
|
tone_mapper: ToneMapper::Aces,
|
||||||
|
exposure_stops: 0.,
|
||||||
|
};
|
||||||
|
assert_runtime_path(
|
||||||
|
&sdr_to_hdr,
|
||||||
|
format!("textureFamilies[{family}].source.descriptor"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut hdr_value = full_cull_graph();
|
||||||
|
hdr_value["nodes"][0]["parameters"]["texture"]["format"] = json!("rgba16_float");
|
||||||
|
let frame_node = node_index(&hdr_value, "frame_out");
|
||||||
|
hdr_value["nodes"][frame_node]["parameters"] = frame_parameters(true);
|
||||||
|
let mut hdr_to_sdr = compile_graph(hdr_value);
|
||||||
|
let hdr_frame = hdr_to_sdr
|
||||||
|
.executions
|
||||||
|
.iter()
|
||||||
|
.position(|e| e.executor.key == "frame_out")
|
||||||
|
.unwrap();
|
||||||
|
let NormalizedParameters::FrameOut {
|
||||||
|
dynamic_range,
|
||||||
|
output_transfer,
|
||||||
|
..
|
||||||
|
} = &mut hdr_to_sdr.executions[hdr_frame].parameters
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
*dynamic_range = FrameDynamicRange::Sdr;
|
||||||
|
*output_transfer = OutputTransfer::Linear;
|
||||||
|
let linear_surface = RuntimeSurfaceContract {
|
||||||
|
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: vec![],
|
||||||
|
};
|
||||||
|
prepare_runtime_plan(&hdr_to_sdr, linear_surface.clone(), None).unwrap();
|
||||||
|
let error = prepare_runtime_plan(
|
||||||
|
&hdr_to_sdr,
|
||||||
|
RuntimeSurfaceContract {
|
||||||
|
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||||
|
..linear_surface
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(error.code, "GRAPH_SURFACE_INCOMPATIBLE");
|
||||||
|
assert_eq!(
|
||||||
|
error.details["path"],
|
||||||
|
format!("executions[{hdr_frame}].parameters.outputTransfer")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_rejects_mesh_query_predicate_shape_mutations() {
|
fn runtime_rejects_mesh_query_predicate_shape_mutations() {
|
||||||
let baseline = compile_graph(full_cull_graph());
|
let baseline = compile_graph(full_cull_graph());
|
||||||
@@ -2997,12 +3260,6 @@ fn runtime_rejects_fullscreen_parameter_and_sample_order_mutations() {
|
|||||||
.position(|execution| execution.executor.key == "fullscreen_copy")
|
.position(|execution| execution.executor.key == "fullscreen_copy")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let invalid_parameters = [
|
let invalid_parameters = [
|
||||||
(
|
|
||||||
"tone_map",
|
|
||||||
NormalizedParameters::ToneMap {
|
|
||||||
exposure: f32::INFINITY,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"bloom_extract",
|
"bloom_extract",
|
||||||
NormalizedParameters::BloomExtract {
|
NormalizedParameters::BloomExtract {
|
||||||
@@ -3130,55 +3387,23 @@ fn runtime_rechecks_pipeline_attachment_descriptors() {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.resource
|
.resource
|
||||||
};
|
};
|
||||||
let mutate_descriptor =
|
|
||||||
|graph: &mut CompiledGraph, resource: u32, mutate: fn(&mut NormalizedTextureDescriptor)| {
|
|
||||||
let (family, allocation) = match graph.resources[resource as usize].plan {
|
|
||||||
ResourcePlan::Texture {
|
|
||||||
family, allocation, ..
|
|
||||||
} => (family as usize, allocation.unwrap()),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
let TextureFamilySource::AuthoredTexture {
|
|
||||||
resource: source,
|
|
||||||
descriptor,
|
|
||||||
..
|
|
||||||
} = &mut graph.texture_families[family].source;
|
|
||||||
mutate(descriptor);
|
|
||||||
let descriptor = descriptor.clone();
|
|
||||||
let ResourcePlan::TextureSource {
|
|
||||||
descriptor: source_descriptor,
|
|
||||||
..
|
|
||||||
} = &mut graph.resources[*source as usize].plan
|
|
||||||
else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
*source_descriptor = descriptor.clone();
|
|
||||||
let key = &mut graph.allocation_classes[allocation.class as usize].key;
|
|
||||||
key.dimension = descriptor.dimension;
|
|
||||||
key.format = descriptor.format;
|
|
||||||
key.extent = descriptor.extent;
|
|
||||||
key.mip_level_count = descriptor.mip_level_count;
|
|
||||||
key.sample_count = descriptor.sample_count;
|
|
||||||
key.view_formats = descriptor.view_formats;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut graph = baseline.clone();
|
let mut graph = baseline.clone();
|
||||||
let color = attachment(&graph, "color");
|
let color = attachment(&graph, "color");
|
||||||
mutate_descriptor(&mut graph, color, |descriptor| {
|
mutate_texture_descriptor(&mut graph, color, |descriptor| {
|
||||||
descriptor.format = TextureFormat::Depth32Float;
|
descriptor.format = TextureFormat::Depth32Float;
|
||||||
});
|
});
|
||||||
assert_runtime_path(&graph, format!("executions[{pipeline}].inputs"));
|
assert_runtime_path(&graph, format!("executions[{pipeline}].inputs"));
|
||||||
|
|
||||||
let mut graph = baseline.clone();
|
let mut graph = baseline.clone();
|
||||||
let depth = attachment(&graph, "depth");
|
let depth = attachment(&graph, "depth");
|
||||||
mutate_descriptor(&mut graph, depth, |descriptor| {
|
mutate_texture_descriptor(&mut graph, depth, |descriptor| {
|
||||||
descriptor.format = TextureFormat::Rgba16Float;
|
descriptor.format = TextureFormat::Rgba16Float;
|
||||||
});
|
});
|
||||||
assert_runtime_path(&graph, format!("executions[{pipeline}].inputs"));
|
assert_runtime_path(&graph, format!("executions[{pipeline}].inputs"));
|
||||||
|
|
||||||
let mut graph = baseline;
|
let mut graph = baseline;
|
||||||
let color = attachment(&graph, "color");
|
let color = attachment(&graph, "color");
|
||||||
mutate_descriptor(&mut graph, color, |descriptor| {
|
mutate_texture_descriptor(&mut graph, color, |descriptor| {
|
||||||
descriptor.extent = NormalizedTextureExtent::Absolute {
|
descriptor.extent = NormalizedTextureExtent::Absolute {
|
||||||
width: 4,
|
width: 4,
|
||||||
height: 4,
|
height: 4,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ pub(crate) fn encode_compiled<T: Scene>(
|
|||||||
(
|
(
|
||||||
surface,
|
surface,
|
||||||
wgpu::Operations {
|
wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Load,
|
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||||
store: wgpu::StoreOp::Store,
|
store: wgpu::StoreOp::Store,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,10 +17,29 @@ fn aces(x: vec3<f32>) -> vec3<f32> {
|
|||||||
return clamp((x * (2.51 * x + vec3(0.03))) / (x * (2.43 * x + vec3(0.59)) + vec3(0.14)), vec3(0.0), vec3(1.0));
|
return clamp((x * (2.51 * x + vec3(0.03))) / (x * (2.43 * x + vec3(0.59)) + vec3(0.14)), vec3(0.0), vec3(1.0));
|
||||||
}
|
}
|
||||||
fn linear_to_srgb(x: vec3<f32>) -> vec3<f32> {
|
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);
|
let safe = clamp(x, vec3(0.0), vec3(1.0));
|
||||||
return select(high, low, x <= vec3(0.0031308));
|
let low = safe * 12.92; let high = 1.055 * pow(safe, vec3(1.0 / 2.4)) - vec3(0.055);
|
||||||
|
return select(high, low, safe <= vec3(0.0031308));
|
||||||
|
}
|
||||||
|
struct FrameCoordinates { uv: vec2<f32>, contained: bool }
|
||||||
|
fn frame_coordinates(position: vec2<f32>, surface: vec2<f32>, source: vec2<f32>, mode: f32) -> FrameCoordinates {
|
||||||
|
if mode < 0.5 { return FrameCoordinates(position / surface, true); }
|
||||||
|
let surface_aspect = surface.x / surface.y; let source_aspect = source.x / source.y; var size = surface;
|
||||||
|
if (mode < 1.5 && source_aspect > surface_aspect) || (mode > 1.5 && source_aspect < surface_aspect) { size.y = surface.x / source_aspect; } else { size.x = surface.y * source_aspect; }
|
||||||
|
let origin = (surface - size) * 0.5;
|
||||||
|
return FrameCoordinates((position - origin) / size, mode > 1.5 || (all(position >= origin) && all(position < origin + size)));
|
||||||
|
}
|
||||||
|
@fragment fn fs_frame_out(in: VertexOut) -> @location(0) vec4<f32> {
|
||||||
|
let surface=parameters.values[1].yz; let source=vec2<f32>(textureDimensions(source_texture));
|
||||||
|
let coordinates=frame_coordinates(in.position.xy,surface,source,parameters.values[1].x);
|
||||||
|
if !coordinates.contained {
|
||||||
|
var bg=parameters.values[2]; if parameters.values[0].w > 0.5 { bg=vec4(linear_to_srgb(bg.rgb),clamp(bg.a,0.0,1.0)); } return bg;
|
||||||
|
}
|
||||||
|
let sampled=sample_source(coordinates.uv); var rgb: vec3<f32>;
|
||||||
|
if parameters.values[0].x > 0.5 { rgb=max(sampled.rgb*exp2(parameters.values[0].z),vec3(0.0)); if parameters.values[0].y > 1.5 { rgb=aces(rgb); } else if parameters.values[0].y > 0.5 { rgb=rgb/(vec3(1.0)+rgb); } else { rgb=clamp(rgb,vec3(0.0),vec3(1.0)); } } else { rgb=clamp(sampled.rgb,vec3(0.0),vec3(1.0)); }
|
||||||
|
if parameters.values[0].w > 0.5 { rgb=linear_to_srgb(rgb); }
|
||||||
|
return vec4(clamp(rgb,vec3(0.0),vec3(1.0)),clamp(sampled.a,0.0,1.0));
|
||||||
}
|
}
|
||||||
@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); }
|
|
||||||
fn grading_result(source: vec4<f32>, graded: vec3<f32>, factor: f32) -> vec4<f32> { return vec4(mix(source.rgb, graded, vec3(factor)), source.a); }
|
fn grading_result(source: vec4<f32>, graded: vec3<f32>, factor: f32) -> vec4<f32> { return vec4(mix(source.rgb, graded, vec3(factor)), source.a); }
|
||||||
@fragment fn fs_color_balance(in: VertexOut) -> @location(0) vec4<f32> {
|
@fragment fn fs_color_balance(in: VertexOut) -> @location(0) vec4<f32> {
|
||||||
let c=sample_source(in.uv); var graded: vec3<f32>;
|
let c=sample_source(in.uv); var graded: vec3<f32>;
|
||||||
|
|||||||
+403
-13
@@ -33,6 +33,24 @@ struct FullscreenUniforms {
|
|||||||
values: [[f32; 4]; 8],
|
values: [[f32; 4]; 8],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum FullscreenSamplerChoice {
|
||||||
|
Linear,
|
||||||
|
Nearest,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame_out_sampler_choice(
|
||||||
|
parameters: &crate::render_graph::NormalizedParameters,
|
||||||
|
) -> Option<FullscreenSamplerChoice> {
|
||||||
|
let crate::render_graph::NormalizedParameters::FrameOut { filter, .. } = parameters else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(match filter {
|
||||||
|
crate::render_graph::FrameFilter::Linear => FullscreenSamplerChoice::Linear,
|
||||||
|
crate::render_graph::FrameFilter::Nearest => FullscreenSamplerChoice::Nearest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn pack_fullscreen_uniforms(
|
fn pack_fullscreen_uniforms(
|
||||||
key: &str,
|
key: &str,
|
||||||
parameters: &crate::render_graph::NormalizedParameters,
|
parameters: &crate::render_graph::NormalizedParameters,
|
||||||
@@ -40,11 +58,7 @@ fn pack_fullscreen_uniforms(
|
|||||||
use crate::render_graph::NormalizedParameters;
|
use crate::render_graph::NormalizedParameters;
|
||||||
let mut values = [[0.; 4]; 8];
|
let mut values = [[0.; 4]; 8];
|
||||||
let first = match (key, parameters) {
|
let first = match (key, parameters) {
|
||||||
(
|
("fullscreen_copy", NormalizedParameters::FullscreenCopy) => [0.; 4],
|
||||||
"fullscreen_copy" | "frame_out",
|
|
||||||
NormalizedParameters::FullscreenCopy | NormalizedParameters::FrameOut,
|
|
||||||
) => [0.; 4],
|
|
||||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => [*exposure, 0., 0., 0.],
|
|
||||||
(
|
(
|
||||||
"color_balance",
|
"color_balance",
|
||||||
NormalizedParameters::ColorBalance {
|
NormalizedParameters::ColorBalance {
|
||||||
@@ -134,10 +148,68 @@ fn pack_fullscreen_uniforms(
|
|||||||
Some(FullscreenUniforms { values })
|
Some(FullscreenUniforms { values })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pack_frame_out_uniforms(
|
||||||
|
parameters: &crate::render_graph::NormalizedParameters,
|
||||||
|
surface: &crate::render_graph::RuntimeSurfaceContract,
|
||||||
|
) -> Option<FullscreenUniforms> {
|
||||||
|
use crate::render_graph::*;
|
||||||
|
let NormalizedParameters::FrameOut {
|
||||||
|
dynamic_range,
|
||||||
|
output_transfer,
|
||||||
|
scale_mode,
|
||||||
|
background_color,
|
||||||
|
..
|
||||||
|
} = parameters
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if *output_transfer == OutputTransfer::Linear && surface.format.is_srgb() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (hdr, mapper, exposure) = match dynamic_range {
|
||||||
|
FrameDynamicRange::Sdr => (0., 0., 0.),
|
||||||
|
FrameDynamicRange::Hdr {
|
||||||
|
tone_mapper,
|
||||||
|
exposure_stops,
|
||||||
|
} => (
|
||||||
|
1.,
|
||||||
|
match tone_mapper {
|
||||||
|
ToneMapper::None => 0.,
|
||||||
|
ToneMapper::Reinhard => 1.,
|
||||||
|
ToneMapper::Aces => 2.,
|
||||||
|
},
|
||||||
|
*exposure_stops,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let mut values = [[0.; 4]; 8];
|
||||||
|
values[0] = [
|
||||||
|
hdr,
|
||||||
|
mapper,
|
||||||
|
exposure,
|
||||||
|
if *output_transfer == OutputTransfer::Srgb && !surface.format.is_srgb() {
|
||||||
|
1.
|
||||||
|
} else {
|
||||||
|
0.
|
||||||
|
},
|
||||||
|
];
|
||||||
|
values[1] = [
|
||||||
|
match scale_mode {
|
||||||
|
ScaleMode::Stretch => 0.,
|
||||||
|
ScaleMode::Contain => 1.,
|
||||||
|
ScaleMode::Cover => 2.,
|
||||||
|
},
|
||||||
|
surface.width as f32,
|
||||||
|
surface.height as f32,
|
||||||
|
0.,
|
||||||
|
];
|
||||||
|
values[2] = *background_color;
|
||||||
|
Some(FullscreenUniforms { values })
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_fullscreen_entry(key: &str) -> Option<&'static str> {
|
fn resolve_fullscreen_entry(key: &str) -> Option<&'static str> {
|
||||||
match key {
|
match key {
|
||||||
"fullscreen_copy" | "frame_out" => Some("fs_copy"),
|
"fullscreen_copy" => Some("fs_copy"),
|
||||||
"tone_map" => Some("fs_tone_map"),
|
"frame_out" => Some("fs_frame_out"),
|
||||||
"color_balance" => Some("fs_color_balance"),
|
"color_balance" => Some("fs_color_balance"),
|
||||||
"exposure_contrast" => Some("fs_exposure_contrast"),
|
"exposure_contrast" => Some("fs_exposure_contrast"),
|
||||||
"saturation" => Some("fs_saturation"),
|
"saturation" => Some("fs_saturation"),
|
||||||
@@ -153,7 +225,7 @@ fn resolve_fullscreen_entry(key: &str) -> Option<&'static str> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod fullscreen_tests {
|
mod fullscreen_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::render_graph::{ColorBalanceMode, NormalizedParameters};
|
use crate::render_graph::*;
|
||||||
|
|
||||||
fn assert_packed(key: &str, parameters: NormalizedParameters, expected: &[[f32; 4]]) {
|
fn assert_packed(key: &str, parameters: NormalizedParameters, expected: &[[f32; 4]]) {
|
||||||
let packed = pack_fullscreen_uniforms(key, ¶meters).unwrap();
|
let packed = pack_fullscreen_uniforms(key, ¶meters).unwrap();
|
||||||
@@ -179,15 +251,15 @@ mod fullscreen_tests {
|
|||||||
assert!(packed.values[1..].iter().all(|value| *value == [0.0; 4]));
|
assert!(packed.values[1..].iter().all(|value| *value == [0.0; 4]));
|
||||||
assert_eq!(bytemuck::bytes_of(&packed).len(), 128);
|
assert_eq!(bytemuck::bytes_of(&packed).len(), 128);
|
||||||
assert!(
|
assert!(
|
||||||
pack_fullscreen_uniforms("tone_map", &NormalizedParameters::FullscreenCopy).is_none()
|
pack_fullscreen_uniforms("frame_out", &NormalizedParameters::FullscreenCopy).is_none()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fullscreen_entries_are_explicit() {
|
fn fullscreen_entries_are_explicit() {
|
||||||
assert_eq!(resolve_fullscreen_entry("fullscreen_copy"), Some("fs_copy"));
|
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("frame_out"), Some("fs_frame_out"));
|
||||||
assert_eq!(resolve_fullscreen_entry("tone_map"), Some("fs_tone_map"));
|
assert_eq!(resolve_fullscreen_entry("tone_map"), None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_fullscreen_entry("color_balance"),
|
resolve_fullscreen_entry("color_balance"),
|
||||||
Some("fs_color_balance")
|
Some("fs_color_balance")
|
||||||
@@ -223,6 +295,296 @@ mod fullscreen_tests {
|
|||||||
assert_eq!(resolve_fullscreen_entry("unknown"), None);
|
assert_eq!(resolve_fullscreen_entry("unknown"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn surface(format: wgpu::TextureFormat) -> RuntimeSurfaceContract {
|
||||||
|
RuntimeSurfaceContract {
|
||||||
|
format,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame(
|
||||||
|
dynamic_range: FrameDynamicRange,
|
||||||
|
transfer: OutputTransfer,
|
||||||
|
scale_mode: ScaleMode,
|
||||||
|
filter: FrameFilter,
|
||||||
|
) -> NormalizedParameters {
|
||||||
|
NormalizedParameters::FrameOut {
|
||||||
|
dynamic_range,
|
||||||
|
output_transfer: transfer,
|
||||||
|
scale_mode,
|
||||||
|
filter,
|
||||||
|
background_color: [0.1, 0.2, 0.3, 0.4],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_out_packer_tags_all_lanes_and_sampler_is_gpu_independent() {
|
||||||
|
let expected = |first, scale| {
|
||||||
|
[
|
||||||
|
first,
|
||||||
|
[scale, 1920., 1080., 0.],
|
||||||
|
[0.1, 0.2, 0.3, 0.4],
|
||||||
|
[0.; 4],
|
||||||
|
[0.; 4],
|
||||||
|
[0.; 4],
|
||||||
|
[0.; 4],
|
||||||
|
[0.; 4],
|
||||||
|
]
|
||||||
|
};
|
||||||
|
for (mapper, mapper_tag, scale, scale_tag, filter) in [
|
||||||
|
(
|
||||||
|
ToneMapper::None,
|
||||||
|
0.,
|
||||||
|
ScaleMode::Stretch,
|
||||||
|
0.,
|
||||||
|
FrameFilter::Linear,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ToneMapper::Reinhard,
|
||||||
|
1.,
|
||||||
|
ScaleMode::Contain,
|
||||||
|
1.,
|
||||||
|
FrameFilter::Nearest,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ToneMapper::Aces,
|
||||||
|
2.,
|
||||||
|
ScaleMode::Cover,
|
||||||
|
2.,
|
||||||
|
FrameFilter::Linear,
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let p = frame(
|
||||||
|
FrameDynamicRange::Hdr {
|
||||||
|
tone_mapper: mapper,
|
||||||
|
exposure_stops: -2.,
|
||||||
|
},
|
||||||
|
OutputTransfer::Srgb,
|
||||||
|
scale,
|
||||||
|
filter,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pack_frame_out_uniforms(&p, &surface(wgpu::TextureFormat::Rgba8Unorm))
|
||||||
|
.unwrap()
|
||||||
|
.values,
|
||||||
|
expected([1., mapper_tag, -2., 1.], scale_tag)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
frame_out_sampler_choice(&p),
|
||||||
|
Some(if filter == FrameFilter::Nearest {
|
||||||
|
FullscreenSamplerChoice::Nearest
|
||||||
|
} else {
|
||||||
|
FullscreenSamplerChoice::Linear
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let sdr = frame(
|
||||||
|
FrameDynamicRange::Sdr,
|
||||||
|
OutputTransfer::Srgb,
|
||||||
|
ScaleMode::Stretch,
|
||||||
|
FrameFilter::Linear,
|
||||||
|
);
|
||||||
|
for format in [
|
||||||
|
wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||||
|
wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
pack_frame_out_uniforms(&sdr, &surface(format))
|
||||||
|
.unwrap()
|
||||||
|
.values,
|
||||||
|
expected([0.; 4], 0.)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
pack_frame_out_uniforms(&sdr, &surface(wgpu::TextureFormat::Bgra8Unorm))
|
||||||
|
.unwrap()
|
||||||
|
.values,
|
||||||
|
expected([0., 0., 0., 1.], 0.)
|
||||||
|
);
|
||||||
|
let linear = frame(
|
||||||
|
FrameDynamicRange::Sdr,
|
||||||
|
OutputTransfer::Linear,
|
||||||
|
ScaleMode::Stretch,
|
||||||
|
FrameFilter::Linear,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pack_frame_out_uniforms(&linear, &surface(wgpu::TextureFormat::Bgra8Unorm))
|
||||||
|
.unwrap()
|
||||||
|
.values,
|
||||||
|
expected([0.; 4], 0.)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pack_frame_out_uniforms(&linear, &surface(wgpu::TextureFormat::Bgra8UnormSrgb))
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
frame_out_sampler_choice(&NormalizedParameters::FullscreenCopy),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn coordinates(
|
||||||
|
p: [f32; 2],
|
||||||
|
surface: [f32; 2],
|
||||||
|
source: [f32; 2],
|
||||||
|
mode: ScaleMode,
|
||||||
|
) -> ([f32; 2], bool) {
|
||||||
|
if mode == ScaleMode::Stretch {
|
||||||
|
return ([p[0] / surface[0], p[1] / surface[1]], true);
|
||||||
|
}
|
||||||
|
let (sa, ia) = (surface[0] / surface[1], source[0] / source[1]);
|
||||||
|
let mut size = surface;
|
||||||
|
if (mode == ScaleMode::Contain && ia > sa) || (mode == ScaleMode::Cover && ia < sa) {
|
||||||
|
size[1] = surface[0] / ia;
|
||||||
|
} else {
|
||||||
|
size[0] = surface[1] * ia;
|
||||||
|
}
|
||||||
|
let origin = [(surface[0] - size[0]) * 0.5, (surface[1] - size[1]) * 0.5];
|
||||||
|
(
|
||||||
|
[(p[0] - origin[0]) / size[0], (p[1] - origin[1]) / size[1]],
|
||||||
|
mode == ScaleMode::Cover
|
||||||
|
|| (p[0] >= origin[0]
|
||||||
|
&& p[1] >= origin[1]
|
||||||
|
&& p[0] < origin[0] + size[0]
|
||||||
|
&& p[1] < origin[1] + size[1]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_coordinates_reference_half_open_centered_crop_and_stretch() {
|
||||||
|
let contain = |p| coordinates(p, [4., 4.], [4., 2.], ScaleMode::Contain);
|
||||||
|
assert_eq!(contain([0., 1.]), ([0., 0.], true));
|
||||||
|
assert_eq!(contain([4., 1.]), ([1., 0.], false));
|
||||||
|
assert_eq!(contain([0., 3.]), ([0., 1.], false));
|
||||||
|
assert!(contain([4. - 0.0001, 3. - 0.0001]).1);
|
||||||
|
for point in [[-1., 2.], [5., 2.], [2., 0.], [2., 4.]] {
|
||||||
|
assert!(!contain(point).1, "outside point {point:?}");
|
||||||
|
}
|
||||||
|
let cover = |p| coordinates(p, [4., 4.], [4., 2.], ScaleMode::Cover);
|
||||||
|
assert_eq!(cover([2., 2.]), ([0.5, 0.5], true));
|
||||||
|
assert_eq!(cover([0., 2.]), ([0.25, 0.5], true));
|
||||||
|
assert_eq!(cover([4., 2.]), ([0.75, 0.5], true));
|
||||||
|
assert_eq!(
|
||||||
|
coordinates([2., 2.], [4., 4.], [1., 9.], ScaleMode::Stretch),
|
||||||
|
([0.5, 0.5], true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn srgb(x: f32) -> f32 {
|
||||||
|
let x = x.clamp(0., 1.);
|
||||||
|
if x <= 0.0031308 {
|
||||||
|
12.92 * x
|
||||||
|
} else {
|
||||||
|
1.055 * x.powf(1. / 2.4) - 0.055
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn shade(c: [f32; 4], mapper: Option<ToneMapper>, exposure: f32, transfer: bool) -> [f32; 4] {
|
||||||
|
let mut rgb = [0.; 3];
|
||||||
|
for i in 0..3 {
|
||||||
|
let x = if mapper.is_some() {
|
||||||
|
(c[i] * 2f32.powf(exposure)).max(0.)
|
||||||
|
} else {
|
||||||
|
c[i]
|
||||||
|
};
|
||||||
|
rgb[i] = match mapper {
|
||||||
|
Some(ToneMapper::Aces) => {
|
||||||
|
((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14)).clamp(0., 1.)
|
||||||
|
}
|
||||||
|
Some(ToneMapper::Reinhard) => x / (1. + x),
|
||||||
|
_ => x.clamp(0., 1.),
|
||||||
|
};
|
||||||
|
if transfer {
|
||||||
|
rgb[i] = srgb(rgb[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[rgb[0], rgb[1], rgb[2], c[3].clamp(0., 1.)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shade_background(
|
||||||
|
c: [f32; 4],
|
||||||
|
_hdr_mapper: ToneMapper,
|
||||||
|
_hdr_exposure_stops: f32,
|
||||||
|
output_transfer: OutputTransfer,
|
||||||
|
) -> [f32; 4] {
|
||||||
|
let mut rgb = [c[0].clamp(0., 1.), c[1].clamp(0., 1.), c[2].clamp(0., 1.)];
|
||||||
|
if output_transfer == OutputTransfer::Srgb {
|
||||||
|
rgb.iter_mut().for_each(|value| *value = srgb(*value));
|
||||||
|
}
|
||||||
|
[rgb[0], rgb[1], rgb[2], c[3].clamp(0., 1.)]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_cpu_goldens_process_rgb_with_straight_clamped_alpha_and_background_once() {
|
||||||
|
let close = |actual: f32, expected: f32| {
|
||||||
|
assert!((actual - expected).abs() < 1e-6, "{actual} != {expected}")
|
||||||
|
};
|
||||||
|
for (input, mapper, expected) in [
|
||||||
|
(0., ToneMapper::Aces, 0.),
|
||||||
|
(1., ToneMapper::Aces, 0.8037975),
|
||||||
|
(1., ToneMapper::Reinhard, 0.5),
|
||||||
|
(4., ToneMapper::Reinhard, 0.8),
|
||||||
|
(-1., ToneMapper::None, 0.),
|
||||||
|
(2., ToneMapper::None, 1.),
|
||||||
|
] {
|
||||||
|
close(
|
||||||
|
shade([input, 0., 0., 0.25], Some(mapper), 0., false)[0],
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (input, expected) in [(0., 0.), (0.0031308, 0.0404499), (0.18, 0.461356), (1., 1.)] {
|
||||||
|
close(srgb(input), expected);
|
||||||
|
}
|
||||||
|
let low_alpha = shade([0.18, 0.5, 1., 0.25], Some(ToneMapper::Reinhard), 1., true);
|
||||||
|
let opaque = shade([0.18, 0.5, 1., 1.], Some(ToneMapper::Reinhard), 1., true);
|
||||||
|
assert_eq!(&low_alpha[..3], &opaque[..3]);
|
||||||
|
assert_eq!((low_alpha[3], opaque[3]), (0.25, 1.));
|
||||||
|
assert_eq!(shade([0.18, 0.5, 1., 2.], None, 0., false)[3], 1.);
|
||||||
|
let background = [0.18, 0.5, 1., 0.25];
|
||||||
|
let once = shade_background(background, ToneMapper::Aces, 10., OutputTransfer::Srgb);
|
||||||
|
for (actual, expected) in once.into_iter().zip([0.461356, 0.7353569, 1., 0.25]) {
|
||||||
|
close(actual, expected);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
once,
|
||||||
|
shade_background(background, ToneMapper::Reinhard, -10., OutputTransfer::Srgb)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
shade_background(background, ToneMapper::Aces, 10., OutputTransfer::Linear),
|
||||||
|
background
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
shade_background(
|
||||||
|
[0.18, 0.5, 1., 1.2],
|
||||||
|
ToneMapper::Aces,
|
||||||
|
10.,
|
||||||
|
OutputTransfer::Srgb
|
||||||
|
)[3],
|
||||||
|
1.
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
shade_background(
|
||||||
|
[0.18, 0.5, 1., -0.2],
|
||||||
|
ToneMapper::Aces,
|
||||||
|
10.,
|
||||||
|
OutputTransfer::Srgb
|
||||||
|
)[3],
|
||||||
|
0.
|
||||||
|
);
|
||||||
|
let white = shade_background(
|
||||||
|
[1., 1., 1., 0.25],
|
||||||
|
ToneMapper::Aces,
|
||||||
|
10.,
|
||||||
|
OutputTransfer::Srgb,
|
||||||
|
);
|
||||||
|
white[..3].iter().for_each(|channel| close(*channel, 1.));
|
||||||
|
assert_eq!(white[3], 0.25);
|
||||||
|
let tone_mapped_white = shade([1., 1., 1., 1.], Some(ToneMapper::Aces), 0., false);
|
||||||
|
assert!(tone_mapped_white[0] < 0.81 && white[0] > tone_mapped_white[0]);
|
||||||
|
}
|
||||||
|
|
||||||
fn balance(mode: ColorBalanceMode) -> NormalizedParameters {
|
fn balance(mode: ColorBalanceMode) -> NormalizedParameters {
|
||||||
NormalizedParameters::ColorBalance {
|
NormalizedParameters::ColorBalance {
|
||||||
mode,
|
mode,
|
||||||
@@ -1218,6 +1580,15 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
min_filter: wgpu::FilterMode::Linear,
|
min_filter: wgpu::FilterMode::Linear,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
let nearest_sampler = self
|
||||||
|
.context
|
||||||
|
.device
|
||||||
|
.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
label: Some(" frame nearest clamp"),
|
||||||
|
mag_filter: wgpu::FilterMode::Nearest,
|
||||||
|
min_filter: wgpu::FilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
let mut executions = Vec::new();
|
let mut executions = Vec::new();
|
||||||
for (index, execution) in graph.executions.iter().enumerate() {
|
for (index, execution) in graph.executions.iter().enumerate() {
|
||||||
let contract = crate::render_graph::contract(&execution.executor.key)
|
let contract = crate::render_graph::contract(&execution.executor.key)
|
||||||
@@ -1256,9 +1627,20 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
_ => return Err(fail("fullscreen inputs mismatch")),
|
_ => return Err(fail("fullscreen inputs mismatch")),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let values =
|
let values = if frame_out {
|
||||||
|
pack_frame_out_uniforms(&execution.parameters, &runtime.surface)
|
||||||
|
} else {
|
||||||
pack_fullscreen_uniforms(&execution.executor.key, &execution.parameters)
|
pack_fullscreen_uniforms(&execution.executor.key, &execution.parameters)
|
||||||
|
}
|
||||||
.ok_or_else(|| fail("executor parameters mismatch"))?;
|
.ok_or_else(|| fail("executor parameters mismatch"))?;
|
||||||
|
let sampler_choice = if frame_out {
|
||||||
|
Some(
|
||||||
|
frame_out_sampler_choice(&execution.parameters)
|
||||||
|
.ok_or_else(|| fail("executor parameters mismatch"))?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
use wgpu::util::DeviceExt;
|
use wgpu::util::DeviceExt;
|
||||||
let uniform =
|
let uniform =
|
||||||
self.context
|
self.context
|
||||||
@@ -1347,7 +1729,15 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
},
|
},
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 2,
|
binding: 2,
|
||||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
resource: wgpu::BindingResource::Sampler(
|
||||||
|
if sampler_choice
|
||||||
|
== Some(FullscreenSamplerChoice::Nearest)
|
||||||
|
{
|
||||||
|
&nearest_sampler
|
||||||
|
} else {
|
||||||
|
&sampler
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 3,
|
binding: 3,
|
||||||
|
|||||||
+4
-1
@@ -176,7 +176,10 @@
|
|||||||
<option value="ember">Ember</option>
|
<option value="ember">Ember</option>
|
||||||
<option value="hdr">HDR Fullscreen</option>
|
<option value="hdr">HDR Fullscreen</option>
|
||||||
<option value="culling">GPU frustum culling</option>
|
<option value="culling">GPU frustum culling</option>
|
||||||
<option value="tone">Tone map</option>
|
<option value="tone">ACES display</option>
|
||||||
|
<option value="contain">SDR contain</option>
|
||||||
|
<option value="reinhard">Reinhard cover</option>
|
||||||
|
<option value="linear">Linear clip</option>
|
||||||
<option value="grading">Grading</option>
|
<option value="grading">Grading</option>
|
||||||
<option value="edges">Edges</option>
|
<option value="edges">Edges</option>
|
||||||
<option value="bloom">Bloom</option>
|
<option value="bloom">Bloom</option>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const GRAPH_ID = "authored_gpu_culling";
|
export const GRAPH_ID = "authored_gpu_culling";
|
||||||
export const CATALOG_VERSION = 5;
|
export const CATALOG_VERSION = 6;
|
||||||
const exact = (type) => ({ kind: "exact", types: [type] });
|
const exact = (type) => ({ kind: "exact", types: [type] });
|
||||||
const i = (type, required = true, authoringType) => ({
|
const i = (type, required = true, authoringType) => ({
|
||||||
accepted: typeof type === "string" ? exact(type) : type,
|
accepted: typeof type === "string" ? exact(type) : type,
|
||||||
@@ -120,16 +120,6 @@ export const semanticCatalog = Object.freeze({
|
|||||||
outputs: { color: o("texture") },
|
outputs: { color: o("texture") },
|
||||||
parameters: {},
|
parameters: {},
|
||||||
},
|
},
|
||||||
tone_map: {
|
|
||||||
version: 1,
|
|
||||||
execution: "render",
|
|
||||||
inputs: {
|
|
||||||
source: i("texture"),
|
|
||||||
colorTarget: i("texture"),
|
|
||||||
},
|
|
||||||
outputs: { color: o("texture") },
|
|
||||||
parameters: { exposure: 1 },
|
|
||||||
},
|
|
||||||
color_balance: {
|
color_balance: {
|
||||||
version: 1,
|
version: 1,
|
||||||
execution: "render",
|
execution: "render",
|
||||||
@@ -198,11 +188,11 @@ export const semanticCatalog = Object.freeze({
|
|||||||
parameters: { strength: 2 },
|
parameters: { strength: 2 },
|
||||||
},
|
},
|
||||||
frame_out: {
|
frame_out: {
|
||||||
version: 1,
|
version: 2,
|
||||||
execution: "frame",
|
execution: "frame",
|
||||||
inputs: { color: i("texture") },
|
inputs: { color: i("texture") },
|
||||||
outputs: {},
|
outputs: {},
|
||||||
parameters: {},
|
parameters: { hdrEnabled: true, toneMapper: "aces", exposureStops: 0, outputTransfer: "srgb", scaleMode: "stretch", filter: "linear", backgroundColor: [0, 0, 0, 1] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const socketColors = [
|
const socketColors = [
|
||||||
@@ -383,7 +373,6 @@ const parameterSchemas = {
|
|||||||
clearColor: color([0.015, 0.02, 0.03, 1]),
|
clearColor: color([0.015, 0.02, 0.03, 1]),
|
||||||
},
|
},
|
||||||
fullscreen_copy: {},
|
fullscreen_copy: {},
|
||||||
tone_map: { exposure: number(1, 0, 32) },
|
|
||||||
color_balance: {
|
color_balance: {
|
||||||
mode: enumeration("lift_gamma_gain", ["lift_gamma_gain", "offset_power_slope"]), factor: number(1, 0, 1),
|
mode: enumeration("lift_gamma_gain", ["lift_gamma_gain", "offset_power_slope"]), factor: number(1, 0, 1),
|
||||||
lift: number(0, -1, 1), liftColor: color([1, 1, 1, 1], 0, 4), gamma: number(1, 0.01, 4), gammaColor: color([1, 1, 1, 1], 0, 4), gain: number(1, 0, 4), gainColor: color([1, 1, 1, 1], 0, 4),
|
lift: number(0, -1, 1), liftColor: color([1, 1, 1, 1], 0, 4), gamma: number(1, 0.01, 4), gammaColor: color([1, 1, 1, 1], 0, 4), gain: number(1, 0, 4), gainColor: color([1, 1, 1, 1], 0, 4),
|
||||||
@@ -399,7 +388,15 @@ const parameterSchemas = {
|
|||||||
},
|
},
|
||||||
bloom_composite: { intensity: number(1, 0, 16) },
|
bloom_composite: { intensity: number(1, 0, 16) },
|
||||||
luminance_edge: { strength: number(2, 0, 16) },
|
luminance_edge: { strength: number(2, 0, 16) },
|
||||||
frame_out: {},
|
frame_out: {
|
||||||
|
hdrEnabled: boolean(true),
|
||||||
|
toneMapper: enumeration("aces", ["aces", "reinhard", "none"]),
|
||||||
|
exposureStops: number(0, -10, 10),
|
||||||
|
outputTransfer: enumeration("srgb", ["srgb", "linear"]),
|
||||||
|
scaleMode: enumeration("stretch", ["stretch", "contain", "cover"]),
|
||||||
|
filter: enumeration("linear", ["linear", "nearest"]),
|
||||||
|
backgroundColor: color([0, 0, 0, 1]),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export const nodeDefinitions = Object.fromEntries(
|
export const nodeDefinitions = Object.fromEntries(
|
||||||
Object.entries(semanticCatalog).map(([key, c]) => {
|
Object.entries(semanticCatalog).map(([key, c]) => {
|
||||||
@@ -471,6 +468,17 @@ nodeDefinitions.color_balance.ui = [
|
|||||||
{ kind: "parameter", parameter: "factor" },
|
{ kind: "parameter", parameter: "factor" },
|
||||||
{ kind: "socket", socket: "source" }, { kind: "socket", socket: "colorTarget" }, { kind: "socket", socket: "color" },
|
{ kind: "socket", socket: "source" }, { kind: "socket", socket: "colorTarget" }, { kind: "socket", socket: "color" },
|
||||||
];
|
];
|
||||||
|
nodeDefinitions.frame_out.ui = [
|
||||||
|
{ kind: "text", variant: "section", title: "Display Transform" },
|
||||||
|
{ kind: "parameter", parameter: "hdrEnabled", title: "HDR" },
|
||||||
|
{ kind: "parameter", parameter: "toneMapper", title: "Tone Mapper", visibleWhen: { parameter: "hdrEnabled", equals: true } },
|
||||||
|
{ kind: "parameter", parameter: "exposureStops", title: "Exposure", visibleWhen: { parameter: "hdrEnabled", equals: true } },
|
||||||
|
{ kind: "parameter", parameter: "outputTransfer", title: "Transfer" },
|
||||||
|
{ kind: "parameter", parameter: "scaleMode", title: "Scale" },
|
||||||
|
{ kind: "parameter", parameter: "filter" },
|
||||||
|
{ kind: "parameter", parameter: "backgroundColor", title: "Background", visibleWhen: { parameter: "scaleMode", equals: "contain" } },
|
||||||
|
{ kind: "socket", socket: "color" },
|
||||||
|
];
|
||||||
export const fxNodeComposition = Object.freeze({
|
export const fxNodeComposition = Object.freeze({
|
||||||
schemaVersion: 2,
|
schemaVersion: 2,
|
||||||
id: "yawn.render-graph",
|
id: "yawn.render-graph",
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ const input = (node, socket) => ({ node, socket });
|
|||||||
const node = (id, key, parameters = {}, inputs = {}) => ({
|
const node = (id, key, parameters = {}, inputs = {}) => ({
|
||||||
id, state: "enabled", executor: { key, version: descriptors[key].version }, parameters, inputs,
|
id, state: "enabled", executor: { key, version: descriptors[key].version }, parameters, inputs,
|
||||||
});
|
});
|
||||||
const texture = (format, scale = 1) => ({
|
const texture = (format, scale = 1, heightScale = scale) => ({
|
||||||
texture: {
|
texture: {
|
||||||
dimension: "d2", format,
|
dimension: "d2", format,
|
||||||
extent: { kind: "surface_relative", width: { numerator: 1, denominator: scale }, height: { numerator: 1, denominator: scale }, depthOrArrayLayers: 1 },
|
extent: { kind: "surface_relative", width: { numerator: 1, denominator: scale }, height: { numerator: 1, denominator: heightScale }, depthOrArrayLayers: 1 },
|
||||||
mipLevelCount: 1, sampleCount: 1, viewFormats: [],
|
mipLevelCount: 1, sampleCount: 1, viewFormats: [],
|
||||||
},
|
},
|
||||||
residency: "transient",
|
residency: "transient",
|
||||||
});
|
});
|
||||||
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1]) => [
|
const frameOut = (hdr, options = {}) => ({ hdrEnabled: hdr, toneMapper: "aces", exposureStops: 0, outputTransfer: "srgb", scaleMode: "stretch", filter: "linear", backgroundColor: [0, 0, 0, 1], ...options });
|
||||||
node("hdr", "texture", texture("rgba16_float")),
|
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1) => [
|
||||||
node("depth", "texture", texture("depth32_float")),
|
node("hdr", "texture", texture("rgba16_float", 1, heightScale)),
|
||||||
|
node("depth", "texture", texture("depth32_float", 1, heightScale)),
|
||||||
node("mesh", "mesh"),
|
node("mesh", "mesh"),
|
||||||
node("query", "mesh_query", { visiblePredicate: "required_true", visibleDefault: true, frustumCulledPredicate: "any", frustumCulledDefault: false }, { mesh: input("mesh", "mesh"), isVisible: input("mesh", "isVisible") }),
|
node("query", "mesh_query", { visiblePredicate: "required_true", visibleDefault: true, frustumCulledPredicate: "any", frustumCulledDefault: false }, { mesh: input("mesh", "mesh"), isVisible: input("mesh", "isVisible") }),
|
||||||
node("registry", "pipeline_registry", {}, { pipelineIndices: input("mesh", "pipelineIndices") }),
|
node("registry", "pipeline_registry", {}, { pipelineIndices: input("mesh", "pipelineIndices") }),
|
||||||
@@ -26,13 +27,13 @@ const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 2, graphId, rev
|
|||||||
const direct = (graphId, clearColor) => graph(graphId, [
|
const direct = (graphId, clearColor) => graph(graphId, [
|
||||||
node("ldr", "texture", texture("rgba8_unorm")),
|
node("ldr", "texture", texture("rgba8_unorm")),
|
||||||
...scene("ldr", clearColor).filter((item) => item.id !== "hdr"),
|
...scene("ldr", clearColor).filter((item) => item.id !== "hdr"),
|
||||||
node("frame_out", "frame_out", {}, { color: input("pbr_double", "color") }),
|
node("frame_out", "frame_out", frameOut(false), { color: input("pbr_double", "color") }),
|
||||||
]);
|
]);
|
||||||
export const midnight = direct("preset_midnight", [0.015, 0.06, 0.18, 1]);
|
export const midnight = direct("preset_midnight", [0.015, 0.06, 0.18, 1]);
|
||||||
export const ember = direct("preset_ember", [0.18, 0.035, 0.012, 1]);
|
export const ember = direct("preset_ember", [0.18, 0.035, 0.012, 1]);
|
||||||
export const hdr = graph("preset_hdr_fullscreen", [
|
export const hdr = graph("preset_hdr_fullscreen", [
|
||||||
...scene("hdr"),
|
...scene("hdr"),
|
||||||
node("frame_out", "frame_out", {}, { color: input("pbr_double", "color") }),
|
node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") }),
|
||||||
]);
|
]);
|
||||||
export const culling = graph("preset_gpu_culling", (() => {
|
export const culling = graph("preset_gpu_culling", (() => {
|
||||||
const nodes = structuredClone(hdr.nodes);
|
const nodes = structuredClone(hdr.nodes);
|
||||||
@@ -43,15 +44,15 @@ export const culling = graph("preset_gpu_culling", (() => {
|
|||||||
return nodes;
|
return nodes;
|
||||||
})());
|
})());
|
||||||
const postPreset = (graphId, kind) => {
|
const postPreset = (graphId, kind) => {
|
||||||
const nodes = [node("ldr", "texture", texture("rgba8_unorm")), ...scene("hdr")];
|
const nodes = [...scene("hdr")];
|
||||||
let source = "pbr_double";
|
let source = "pbr_double";
|
||||||
if (kind === "edges") {
|
if (kind === "edges") {
|
||||||
nodes.splice(1, 0, node("edge_hdr", "texture", texture("rgba16_float")));
|
nodes.splice(0, 0, node("edge_hdr", "texture", texture("rgba16_float")));
|
||||||
nodes.push(node("edges", "luminance_edge", { strength: 2 }, { source: input(source, "color"), colorTarget: input("edge_hdr", "texture") }));
|
nodes.push(node("edges", "luminance_edge", { strength: 2 }, { source: input(source, "color"), colorTarget: input("edge_hdr", "texture") }));
|
||||||
source = "edges";
|
source = "edges";
|
||||||
}
|
}
|
||||||
if (kind === "bloom" || kind === "combined") {
|
if (kind === "bloom" || kind === "combined") {
|
||||||
nodes.splice(1, 0,
|
nodes.splice(0, 0,
|
||||||
node("half_a", "texture", texture("rgba16_float", 2)), node("half_b", "texture", texture("rgba16_float", 2)),
|
node("half_a", "texture", texture("rgba16_float", 2)), node("half_b", "texture", texture("rgba16_float", 2)),
|
||||||
node("half_c", "texture", texture("rgba16_float", 2)), node("composite_hdr", "texture", texture("rgba16_float")));
|
node("half_c", "texture", texture("rgba16_float", 2)), node("composite_hdr", "texture", texture("rgba16_float")));
|
||||||
nodes.push(
|
nodes.push(
|
||||||
@@ -62,26 +63,32 @@ const postPreset = (graphId, kind) => {
|
|||||||
);
|
);
|
||||||
source = "composite";
|
source = "composite";
|
||||||
if (kind === "combined") {
|
if (kind === "combined") {
|
||||||
nodes.splice(1, 0, node("edge_hdr", "texture", texture("rgba16_float")));
|
nodes.splice(0, 0, node("edge_hdr", "texture", texture("rgba16_float")));
|
||||||
nodes.push(node("edges", "luminance_edge", { strength: 2 }, { source: input(source, "color"), colorTarget: input("edge_hdr", "texture") }));
|
nodes.push(node("edges", "luminance_edge", { strength: 2 }, { source: input(source, "color"), colorTarget: input("edge_hdr", "texture") }));
|
||||||
source = "edges";
|
source = "edges";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nodes.push(node("tone", "tone_map", { exposure: 1 }, { source: input(source, "color"), colorTarget: input("ldr", "texture") }));
|
nodes.push(node("frame_out", "frame_out", frameOut(true), { color: input(source, "color") }));
|
||||||
nodes.push(node("frame_out", "frame_out", {}, { color: input("tone", "color") }));
|
|
||||||
return graph(graphId, nodes);
|
return graph(graphId, nodes);
|
||||||
};
|
};
|
||||||
export const tone = postPreset("preset_tone", "tone");
|
export const tone = postPreset("preset_tone", "tone");
|
||||||
|
const displayPreset = (id, parameters, heightScale = 1) => graph(id, [
|
||||||
|
...scene("hdr", undefined, heightScale),
|
||||||
|
node("frame_out", "frame_out", parameters, { color: input("pbr_double", "color") }),
|
||||||
|
]);
|
||||||
|
export const contain = displayPreset("preset_contain", frameOut(false, { scaleMode: "contain", filter: "nearest", backgroundColor: [0.18, 0.18, 0.18, 0.25] }), 2);
|
||||||
|
export const reinhard = displayPreset("preset_reinhard", frameOut(true, { toneMapper: "reinhard", exposureStops: 2, scaleMode: "cover", filter: "nearest" }), 2);
|
||||||
|
export const linear = displayPreset("preset_linear", frameOut(true, { toneMapper: "none", exposureStops: 2, outputTransfer: "linear" }));
|
||||||
export const edges = postPreset("preset_edges", "edges");
|
export const edges = postPreset("preset_edges", "edges");
|
||||||
export const bloom = postPreset("preset_bloom", "bloom");
|
export const bloom = postPreset("preset_bloom", "bloom");
|
||||||
export const combined = postPreset("preset_combined", "combined");
|
export const combined = postPreset("preset_combined", "combined");
|
||||||
export const grading = graph("preset_grading", [
|
export const grading = graph("preset_grading", [
|
||||||
node("balance_hdr", "texture", texture("rgba16_float")), node("exposure_hdr", "texture", texture("rgba16_float")), node("saturation_hdr", "texture", texture("rgba16_float")), node("mixer_hdr", "texture", texture("rgba16_float")), node("ldr", "texture", texture("rgba8_unorm")),
|
node("balance_hdr", "texture", texture("rgba16_float")), node("exposure_hdr", "texture", texture("rgba16_float")), node("saturation_hdr", "texture", texture("rgba16_float")), node("mixer_hdr", "texture", texture("rgba16_float")),
|
||||||
...scene("hdr"),
|
...scene("hdr"),
|
||||||
node("balance", "color_balance", { mode: "lift_gamma_gain", factor: 1, lift: 0, liftColor: [1,1,1,1], gamma: 1, gammaColor: [1,1,1,1], gain: 1, gainColor: [1,1,1,1], offset: 0, offsetColor: [1,1,1,1], power: 1, powerColor: [1,1,1,1], slope: 1, slopeColor: [1,1,1,1] }, { source: input("pbr_double", "color"), colorTarget: input("balance_hdr", "texture") }),
|
node("balance", "color_balance", { mode: "lift_gamma_gain", factor: 1, lift: 0, liftColor: [1,1,1,1], gamma: 1, gammaColor: [1,1,1,1], gain: 1, gainColor: [1,1,1,1], offset: 0, offsetColor: [1,1,1,1], power: 1, powerColor: [1,1,1,1], slope: 1, slopeColor: [1,1,1,1] }, { source: input("pbr_double", "color"), colorTarget: input("balance_hdr", "texture") }),
|
||||||
node("exposure", "exposure_contrast", { exposureStops: 0, contrast: 1, pivot: 0.18, factor: 1 }, { source: input("balance", "color"), colorTarget: input("exposure_hdr", "texture") }),
|
node("exposure", "exposure_contrast", { exposureStops: 0, contrast: 1, pivot: 0.18, factor: 1 }, { source: input("balance", "color"), colorTarget: input("exposure_hdr", "texture") }),
|
||||||
node("saturation", "saturation", { saturation: 1, factor: 1 }, { source: input("exposure", "color"), colorTarget: input("saturation_hdr", "texture") }),
|
node("saturation", "saturation", { saturation: 1, factor: 1 }, { source: input("exposure", "color"), colorTarget: input("saturation_hdr", "texture") }),
|
||||||
node("mixer", "channel_mixer", { redOutput: [1,0,0], greenOutput: [0,1,0], blueOutput: [0,0,1], factor: 1 }, { source: input("saturation", "color"), colorTarget: input("mixer_hdr", "texture") }),
|
node("mixer", "channel_mixer", { redOutput: [1,0,0], greenOutput: [0,1,0], blueOutput: [0,0,1], factor: 1 }, { source: input("saturation", "color"), colorTarget: input("mixer_hdr", "texture") }),
|
||||||
node("tone", "tone_map", { exposure: 1 }, { source: input("mixer", "color"), colorTarget: input("ldr", "texture") }), node("frame_out", "frame_out", {}, { color: input("tone", "color") }),
|
node("frame_out", "frame_out", frameOut(true), { color: input("mixer", "color") }),
|
||||||
]);
|
]);
|
||||||
export const renderGraphPresets = Object.freeze({ midnight, ember, hdr, culling, tone, grading, edges, bloom, combined });
|
export const renderGraphPresets = Object.freeze({ midnight, ember, hdr, culling, tone, contain, reinhard, linear, grading, edges, bloom, combined });
|
||||||
|
|||||||
+12
-16
@@ -10,17 +10,13 @@ import {
|
|||||||
spawnRequestedNode,
|
spawnRequestedNode,
|
||||||
} from "../static/render-graph/node-spawn.js";
|
} from "../static/render-graph/node-spawn.js";
|
||||||
|
|
||||||
test("add-node model contains all 17 catalog types in application groups", () => {
|
test("add-node model contains all 16 catalog types in application groups", () => {
|
||||||
assert.equal(addNodeItems.length, 17);
|
assert.equal(addNodeItems.length, 16);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
[...new Set(addNodeItems.map((item) => item.group))],
|
[...new Set(addNodeItems.map((item) => item.group))],
|
||||||
["Source", "Compute", "CPU preparation", "Render / post", "Frame"],
|
["Source", "Compute", "CPU preparation", "Render / post", "Frame"],
|
||||||
);
|
);
|
||||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 17);
|
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 16);
|
||||||
assert.deepEqual(
|
|
||||||
searchAddNodeItems("tone render").map((item) => item.typeId),
|
|
||||||
["tone_map"],
|
|
||||||
);
|
|
||||||
assert.deepEqual(searchAddNodeItems("no such node"), []);
|
assert.deepEqual(searchAddNodeItems("no such node"), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,7 +36,7 @@ test("allocator avoids existing and session-reserved IDs and is bounded", () =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("all 17 types spawn with exact position, current version and generated ID", async () => {
|
test("all 16 types spawn with exact position, current version and generated ID", async () => {
|
||||||
let revision = 5,
|
let revision = 5,
|
||||||
expectedType;
|
expectedType;
|
||||||
const request = { compositionRevision: 5, viewPosition: { x: 12.25, y: -4 } };
|
const request = { compositionRevision: 5, viewPosition: { x: 12.25, y: -4 } };
|
||||||
@@ -91,13 +87,13 @@ test("spawn rechecks composition after getState and propagates add errors", asyn
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await spawnRequestedNode(root, view, request, "tone_map", allocate),
|
await spawnRequestedNode(root, view, request, "fullscreen_copy", allocate),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
revision = 2;
|
revision = 2;
|
||||||
root.getState = async () => ({ version: 3, nodes: [] });
|
root.getState = async () => ({ version: 3, nodes: [] });
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
spawnRequestedNode(root, view, request, "tone_map", allocate),
|
spawnRequestedNode(root, view, request, "fullscreen_copy", allocate),
|
||||||
/must not add/,
|
/must not add/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -124,7 +120,7 @@ test("spawn cancels when a pending getState becomes mutated or dead", async () =
|
|||||||
root,
|
root,
|
||||||
view,
|
view,
|
||||||
request,
|
request,
|
||||||
"tone_map",
|
"fullscreen_copy",
|
||||||
() => "node_a",
|
() => "node_a",
|
||||||
() => alive,
|
() => alive,
|
||||||
);
|
);
|
||||||
@@ -136,7 +132,7 @@ test("spawn cancels when a pending getState becomes mutated or dead", async () =
|
|||||||
root,
|
root,
|
||||||
view,
|
view,
|
||||||
request,
|
request,
|
||||||
"tone_map",
|
"fullscreen_copy",
|
||||||
() => "node_b",
|
() => "node_b",
|
||||||
() => alive,
|
() => alive,
|
||||||
);
|
);
|
||||||
@@ -161,7 +157,7 @@ test("spawn has a final liveness guard after ID allocation", async () => {
|
|||||||
root,
|
root,
|
||||||
view,
|
view,
|
||||||
request,
|
request,
|
||||||
"tone_map",
|
"fullscreen_copy",
|
||||||
() => {
|
() => {
|
||||||
alive = false;
|
alive = false;
|
||||||
return "node_reserved";
|
return "node_reserved";
|
||||||
@@ -190,7 +186,7 @@ test("spawn suppresses teardown RPC rejections but propagates genuine live add e
|
|||||||
root,
|
root,
|
||||||
view,
|
view,
|
||||||
request,
|
request,
|
||||||
"tone_map",
|
"fullscreen_copy",
|
||||||
() => "node_a",
|
() => "node_a",
|
||||||
() => alive,
|
() => alive,
|
||||||
),
|
),
|
||||||
@@ -208,7 +204,7 @@ test("spawn suppresses teardown RPC rejections but propagates genuine live add e
|
|||||||
root,
|
root,
|
||||||
view,
|
view,
|
||||||
request,
|
request,
|
||||||
"tone_map",
|
"fullscreen_copy",
|
||||||
() => "node_b",
|
() => "node_b",
|
||||||
() => alive,
|
() => alive,
|
||||||
),
|
),
|
||||||
@@ -224,7 +220,7 @@ test("spawn suppresses teardown RPC rejections but propagates genuine live add e
|
|||||||
root,
|
root,
|
||||||
view,
|
view,
|
||||||
request,
|
request,
|
||||||
"tone_map",
|
"fullscreen_copy",
|
||||||
() => "node_c",
|
() => "node_c",
|
||||||
() => alive,
|
() => alive,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ test("production render graph composition passes fxnode's public validator", asy
|
|||||||
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
||||||
);
|
);
|
||||||
assert.equal(fxNodeComposition.schemaVersion, 2);
|
assert.equal(fxNodeComposition.schemaVersion, 2);
|
||||||
assert.equal(fxNodeComposition.version, 5);
|
assert.equal(fxNodeComposition.version, 6);
|
||||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 17);
|
assert.equal(Object.keys(fxNodeComposition.nodes).length, 16);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
Object.values(fxNodeComposition.nodes).every(
|
Object.values(fxNodeComposition.nodes).every(
|
||||||
(definition) => definition.migrations.length === 0,
|
(definition) => definition.migrations.length === 0,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function fixture() {
|
|||||||
return {
|
return {
|
||||||
id: n.id,
|
id: n.id,
|
||||||
typeId: n.executor.key,
|
typeId: n.executor.key,
|
||||||
typeVersion: 1,
|
typeVersion: d.version,
|
||||||
known: true,
|
known: true,
|
||||||
muted: n.state !== "enabled",
|
muted: n.state !== "enabled",
|
||||||
position: { x: 10, y: 20 },
|
position: { x: 10, y: 20 },
|
||||||
@@ -96,7 +96,7 @@ function fixture() {
|
|||||||
test("catalog exhaustively mirrors all current contracts", () => {
|
test("catalog exhaustively mirrors all current contracts", () => {
|
||||||
for (const [key, semantic] of Object.entries(semanticCatalog)) {
|
for (const [key, semantic] of Object.entries(semanticCatalog)) {
|
||||||
assert.ok(Object.hasOwn(semantic, "version"));
|
assert.ok(Object.hasOwn(semantic, "version"));
|
||||||
assert.equal(semantic.version, 1);
|
assert.equal(semantic.version, key === "frame_out" ? 2 : 1);
|
||||||
assert.equal(nodeDefinitions[key].version, semantic.version);
|
assert.equal(nodeDefinitions[key].version, semantic.version);
|
||||||
assert.equal(descriptors[key].version, semantic.version);
|
assert.equal(descriptors[key].version, semantic.version);
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,6 @@ test("catalog exhaustively mirrors all current contracts", () => {
|
|||||||
"pipeline_registry",
|
"pipeline_registry",
|
||||||
"pipeline",
|
"pipeline",
|
||||||
"fullscreen_copy",
|
"fullscreen_copy",
|
||||||
"tone_map",
|
|
||||||
"color_balance",
|
"color_balance",
|
||||||
"exposure_contrast",
|
"exposure_contrast",
|
||||||
"saturation",
|
"saturation",
|
||||||
@@ -134,7 +133,7 @@ test("catalog exhaustively mirrors all current contracts", () => {
|
|||||||
Object.keys(contract.parameters).sort(),
|
Object.keys(contract.parameters).sort(),
|
||||||
key,
|
key,
|
||||||
);
|
);
|
||||||
assert.equal(CATALOG_VERSION, 5);
|
assert.equal(CATALOG_VERSION, 6);
|
||||||
assert.deepEqual(nodeDefinitions.pipeline.parameters, {
|
assert.deepEqual(nodeDefinitions.pipeline.parameters, {
|
||||||
pipeline: {
|
pipeline: {
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -238,6 +237,7 @@ test("adapter validates and exactly lowers canonical pipeline controls and blur
|
|||||||
visible: socket.visible,
|
visible: socket.visible,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
blur.typeVersion = descriptors.bloom_blur.version;
|
||||||
x.nodes.push(blur);
|
x.nodes.push(blur);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
adaptFxNodeSnapshot(x).nodes.find((node) => node.id === "blur").parameters
|
adaptFxNodeSnapshot(x).nodes.find((node) => node.id === "blur").parameters
|
||||||
@@ -346,6 +346,51 @@ test("adapter rejects hostile shape, IDs, duplicates, catalog, sockets and type
|
|||||||
link.fromSocketId = "mesh:mesh";
|
link.fromSocketId = "mesh:mesh";
|
||||||
}, "AUTHORING_LINK_TYPE");
|
}, "AUTHORING_LINK_TYPE");
|
||||||
});
|
});
|
||||||
|
test("Frame Out has the exact v2 schema, defaults, UI, and strict authoring validation", () => {
|
||||||
|
const fields = ["hdrEnabled", "toneMapper", "exposureStops", "outputTransfer", "scaleMode", "filter", "backgroundColor"];
|
||||||
|
assert.equal(CATALOG_VERSION, 6);
|
||||||
|
assert.deepEqual(semanticCatalog.frame_out, {
|
||||||
|
version: 2, execution: "frame", inputs: { color: semanticCatalog.frame_out.inputs.color }, outputs: {},
|
||||||
|
parameters: { hdrEnabled: true, toneMapper: "aces", exposureStops: 0, outputTransfer: "srgb", scaleMode: "stretch", filter: "linear", backgroundColor: [0, 0, 0, 1] },
|
||||||
|
});
|
||||||
|
assert.deepEqual(nodeDefinitions.frame_out.parameters, {
|
||||||
|
hdrEnabled: { type: "boolean", default: { kind: "boolean", value: true } },
|
||||||
|
toneMapper: { type: "string", default: { kind: "string", value: "aces" }, enum: ["aces", "reinhard", "none"] },
|
||||||
|
exposureStops: { type: "number", default: { kind: "number", value: 0 }, minimum: -10, maximum: 10 },
|
||||||
|
outputTransfer: { type: "string", default: { kind: "string", value: "srgb" }, enum: ["srgb", "linear"] },
|
||||||
|
scaleMode: { type: "string", default: { kind: "string", value: "stretch" }, enum: ["stretch", "contain", "cover"] },
|
||||||
|
filter: { type: "string", default: { kind: "string", value: "linear" }, enum: ["linear", "nearest"] },
|
||||||
|
backgroundColor: { type: "color", default: { kind: "color", value: [0, 0, 0, 1] }, minimum: 0, maximum: 1 },
|
||||||
|
});
|
||||||
|
assert.deepEqual(nodeDefinitions.frame_out.ui, [
|
||||||
|
{ kind: "text", variant: "section", title: "Display Transform" },
|
||||||
|
{ kind: "parameter", parameter: "hdrEnabled", title: "HDR" },
|
||||||
|
{ kind: "parameter", parameter: "toneMapper", title: "Tone Mapper", visibleWhen: { parameter: "hdrEnabled", equals: true } },
|
||||||
|
{ kind: "parameter", parameter: "exposureStops", title: "Exposure", visibleWhen: { parameter: "hdrEnabled", equals: true } },
|
||||||
|
{ kind: "parameter", parameter: "outputTransfer", title: "Transfer" },
|
||||||
|
{ kind: "parameter", parameter: "scaleMode", title: "Scale" },
|
||||||
|
{ kind: "parameter", parameter: "filter" },
|
||||||
|
{ kind: "parameter", parameter: "backgroundColor", title: "Background", visibleWhen: { parameter: "scaleMode", equals: "contain" } },
|
||||||
|
{ kind: "socket", socket: "color" },
|
||||||
|
]);
|
||||||
|
const reject = (mutate, code, parameter) => {
|
||||||
|
const x = fixture(), n = x.nodes.find((node) => node.typeId === "frame_out");
|
||||||
|
mutate(x, n);
|
||||||
|
assert.throws(() => adaptFxNodeSnapshot(x), (e) => e instanceof AuthoringGraphError && e.code === code && (!parameter || e.details.nodeId === n.id && e.details.parameter === parameter));
|
||||||
|
};
|
||||||
|
reject((x) => x.catalogVersion = 5, "AUTHORING_CATALOG");
|
||||||
|
reject((x, n) => n.typeVersion = 1, "AUTHORING_NODE_INVALID");
|
||||||
|
for (const field of fields) reject((x, n) => delete n.parameters[field], "AUTHORING_PARAMETER_SET");
|
||||||
|
reject((x, n) => n.parameters.extra = { kind: "number", value: 0 }, "AUTHORING_PARAMETER_SET");
|
||||||
|
for (const [field, value] of [
|
||||||
|
["hdrEnabled", 1], ["toneMapper", "bad"], ["outputTransfer", "bad"], ["scaleMode", "bad"], ["filter", "bad"],
|
||||||
|
["exposureStops", NaN], ["exposureStops", -10.01], ["exposureStops", 10.01],
|
||||||
|
["backgroundColor", [0, 0, 0]], ["backgroundColor", [0, 0, Infinity, 1]], ["backgroundColor", [-0.01, 0, 0, 1]], ["backgroundColor", [0, 0, 0, 1.01]],
|
||||||
|
]) reject((x, n) => n.parameters[field].value = value, "AUTHORING_PARAMETER", field);
|
||||||
|
for (const [hidden, value] of [["toneMapper", "bad"], ["exposureStops", Infinity]])
|
||||||
|
reject((x, n) => { n.parameters.hdrEnabled.value = false; n.parameters[hidden].value = value; }, "AUTHORING_PARAMETER", hidden);
|
||||||
|
reject((x, n) => { n.parameters.scaleMode.value = "stretch"; n.parameters.backgroundColor.value = [2, 0, 0, 1]; }, "AUTHORING_PARAMETER", "backgroundColor");
|
||||||
|
});
|
||||||
test("adapter counts only active incoming links and reports socket overflow", () => {
|
test("adapter counts only active incoming links and reports socket overflow", () => {
|
||||||
const x = fixture();
|
const x = fixture();
|
||||||
const active = x.links.find((link) => link.toSocketId === "frame_out:color");
|
const active = x.links.find((link) => link.toSocketId === "frame_out:color");
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ const order = [
|
|||||||
"hdr",
|
"hdr",
|
||||||
"culling",
|
"culling",
|
||||||
"tone",
|
"tone",
|
||||||
|
"contain",
|
||||||
|
"reinhard",
|
||||||
|
"linear",
|
||||||
"grading",
|
"grading",
|
||||||
"edges",
|
"edges",
|
||||||
"bloom",
|
"bloom",
|
||||||
@@ -61,7 +64,6 @@ const sequences = {
|
|||||||
["frame_out", "frame_out"],
|
["frame_out", "frame_out"],
|
||||||
],
|
],
|
||||||
tone: [
|
tone: [
|
||||||
["ldr", "texture"],
|
|
||||||
["hdr", "texture"],
|
["hdr", "texture"],
|
||||||
["depth", "texture"],
|
["depth", "texture"],
|
||||||
["mesh", "mesh"],
|
["mesh", "mesh"],
|
||||||
@@ -70,17 +72,15 @@ const sequences = {
|
|||||||
["ground", "pipeline"],
|
["ground", "pipeline"],
|
||||||
["pbr", "pipeline"],
|
["pbr", "pipeline"],
|
||||||
["pbr_double", "pipeline"],
|
["pbr_double", "pipeline"],
|
||||||
["tone", "tone_map"],
|
|
||||||
["frame_out", "frame_out"],
|
["frame_out", "frame_out"],
|
||||||
],
|
],
|
||||||
grading: [
|
grading: [
|
||||||
["balance_hdr", "texture"], ["exposure_hdr", "texture"], ["saturation_hdr", "texture"], ["mixer_hdr", "texture"], ["ldr", "texture"],
|
["balance_hdr", "texture"], ["exposure_hdr", "texture"], ["saturation_hdr", "texture"], ["mixer_hdr", "texture"],
|
||||||
["hdr", "texture"], ["depth", "texture"], ["mesh", "mesh"], ["query", "mesh_query"], ["registry", "pipeline_registry"],
|
["hdr", "texture"], ["depth", "texture"], ["mesh", "mesh"], ["query", "mesh_query"], ["registry", "pipeline_registry"],
|
||||||
["ground", "pipeline"], ["pbr", "pipeline"], ["pbr_double", "pipeline"], ["balance", "color_balance"], ["exposure", "exposure_contrast"],
|
["ground", "pipeline"], ["pbr", "pipeline"], ["pbr_double", "pipeline"], ["balance", "color_balance"], ["exposure", "exposure_contrast"],
|
||||||
["saturation", "saturation"], ["mixer", "channel_mixer"], ["tone", "tone_map"], ["frame_out", "frame_out"],
|
["saturation", "saturation"], ["mixer", "channel_mixer"], ["frame_out", "frame_out"],
|
||||||
],
|
],
|
||||||
edges: [
|
edges: [
|
||||||
["ldr", "texture"],
|
|
||||||
["edge_hdr", "texture"],
|
["edge_hdr", "texture"],
|
||||||
["hdr", "texture"],
|
["hdr", "texture"],
|
||||||
["depth", "texture"],
|
["depth", "texture"],
|
||||||
@@ -91,11 +91,9 @@ const sequences = {
|
|||||||
["pbr", "pipeline"],
|
["pbr", "pipeline"],
|
||||||
["pbr_double", "pipeline"],
|
["pbr_double", "pipeline"],
|
||||||
["edges", "luminance_edge"],
|
["edges", "luminance_edge"],
|
||||||
["tone", "tone_map"],
|
|
||||||
["frame_out", "frame_out"],
|
["frame_out", "frame_out"],
|
||||||
],
|
],
|
||||||
bloom: [
|
bloom: [
|
||||||
["ldr", "texture"],
|
|
||||||
["half_a", "texture"],
|
["half_a", "texture"],
|
||||||
["half_b", "texture"],
|
["half_b", "texture"],
|
||||||
["half_c", "texture"],
|
["half_c", "texture"],
|
||||||
@@ -112,11 +110,9 @@ const sequences = {
|
|||||||
["blur_h", "bloom_blur"],
|
["blur_h", "bloom_blur"],
|
||||||
["blur_v", "bloom_blur"],
|
["blur_v", "bloom_blur"],
|
||||||
["composite", "bloom_composite"],
|
["composite", "bloom_composite"],
|
||||||
["tone", "tone_map"],
|
|
||||||
["frame_out", "frame_out"],
|
["frame_out", "frame_out"],
|
||||||
],
|
],
|
||||||
combined: [
|
combined: [
|
||||||
["ldr", "texture"],
|
|
||||||
["edge_hdr", "texture"],
|
["edge_hdr", "texture"],
|
||||||
["half_a", "texture"],
|
["half_a", "texture"],
|
||||||
["half_b", "texture"],
|
["half_b", "texture"],
|
||||||
@@ -135,10 +131,11 @@ const sequences = {
|
|||||||
["blur_v", "bloom_blur"],
|
["blur_v", "bloom_blur"],
|
||||||
["composite", "bloom_composite"],
|
["composite", "bloom_composite"],
|
||||||
["edges", "luminance_edge"],
|
["edges", "luminance_edge"],
|
||||||
["tone", "tone_map"],
|
|
||||||
["frame_out", "frame_out"],
|
["frame_out", "frame_out"],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
for (const name of ["contain", "reinhard", "linear"])
|
||||||
|
sequences[name] = sequences.tone;
|
||||||
|
|
||||||
test("presets have the exact canonical pipeline identities, schemas, and node sequences", () => {
|
test("presets have the exact canonical pipeline identities, schemas, and node sequences", () => {
|
||||||
assert.deepEqual(Object.keys(presets.renderGraphPresets), order);
|
assert.deepEqual(Object.keys(presets.renderGraphPresets), order);
|
||||||
@@ -150,6 +147,9 @@ test("presets have the exact canonical pipeline identities, schemas, and node se
|
|||||||
"preset_hdr_fullscreen",
|
"preset_hdr_fullscreen",
|
||||||
"preset_gpu_culling",
|
"preset_gpu_culling",
|
||||||
"preset_tone",
|
"preset_tone",
|
||||||
|
"preset_contain",
|
||||||
|
"preset_reinhard",
|
||||||
|
"preset_linear",
|
||||||
"preset_grading",
|
"preset_grading",
|
||||||
"preset_edges",
|
"preset_edges",
|
||||||
"preset_bloom",
|
"preset_bloom",
|
||||||
@@ -282,16 +282,19 @@ test("presets preserve common mesh, texture, query, pipeline, culling and post w
|
|||||||
node: "cull",
|
node: "cull",
|
||||||
socket: "isFrustumCulled",
|
socket: "isFrustumCulled",
|
||||||
});
|
});
|
||||||
for (const name of ["tone", "grading", "edges", "bloom", "combined"])
|
for (const name of ["tone", "contain", "reinhard", "linear", "grading", "edges", "bloom", "combined"])
|
||||||
assert.ok(!presets[name].nodes.some((node) => node.id === "copy"));
|
assert.ok(!presets[name].nodes.some((node) => node.id === "copy"));
|
||||||
const finalSource = {
|
const finalSource = {
|
||||||
hdr: "pbr_double",
|
hdr: "pbr_double",
|
||||||
culling: "pbr_double",
|
culling: "pbr_double",
|
||||||
tone: "tone",
|
tone: "pbr_double",
|
||||||
grading: "tone",
|
contain: "pbr_double",
|
||||||
edges: "tone",
|
reinhard: "pbr_double",
|
||||||
bloom: "tone",
|
linear: "pbr_double",
|
||||||
combined: "tone",
|
grading: "mixer",
|
||||||
|
edges: "edges",
|
||||||
|
bloom: "composite",
|
||||||
|
combined: "edges",
|
||||||
midnight: "pbr_double",
|
midnight: "pbr_double",
|
||||||
ember: "pbr_double",
|
ember: "pbr_double",
|
||||||
};
|
};
|
||||||
@@ -300,21 +303,4 @@ test("presets preserve common mesh, texture, query, pipeline, culling and post w
|
|||||||
node: finalSource[name],
|
node: finalSource[name],
|
||||||
socket: "color",
|
socket: "color",
|
||||||
});
|
});
|
||||||
assert.equal(
|
|
||||||
presets.tone.nodes.find((node) => node.id === "tone").inputs.source.node,
|
|
||||||
"pbr_double",
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
presets.edges.nodes.find((node) => node.id === "tone").inputs.source.node,
|
|
||||||
"edges",
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
presets.bloom.nodes.find((node) => node.id === "tone").inputs.source.node,
|
|
||||||
"composite",
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
presets.combined.nodes.find((node) => node.id === "tone").inputs.source
|
|
||||||
.node,
|
|
||||||
"edges",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user