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)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ToneMapParameters {
|
||||
exposure: f32,
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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)]
|
||||
#[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 }
|
||||
}
|
||||
"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" => {
|
||||
let p: ColorBalanceParameters =
|
||||
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"))?,
|
||||
}
|
||||
}
|
||||
"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" => {
|
||||
let p: TextureParameters =
|
||||
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
|
||||
})
|
||||
}
|
||||
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::HdrSameExtent) => authored_target_ok && target_matches_source,
|
||||
Some(FullscreenPolicy::BloomComposite) => {
|
||||
@@ -1418,10 +1441,19 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
};
|
||||
let TextureFamilySource::AuthoredTexture { descriptor, .. } =
|
||||
&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(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"frame output requires a filterable single-view d2 color texture",
|
||||
message,
|
||||
format!("nodes[{i}].inputs.color"),
|
||||
));
|
||||
}
|
||||
@@ -1903,17 +1935,20 @@ pub(super) fn is_single_view_d2(descriptor: &NormalizedTextureDescriptor) -> boo
|
||||
&& descriptor.sample_count == 1
|
||||
&& descriptor.mip_level_count == 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)
|
||||
&& matches!(
|
||||
descriptor.format,
|
||||
TextureFormat::Rgba8Unorm
|
||||
| TextureFormat::Rgba8UnormSrgb
|
||||
| TextureFormat::Bgra8Unorm
|
||||
| TextureFormat::Bgra8UnormSrgb
|
||||
| TextureFormat::Rgba16Float
|
||||
)
|
||||
&& match dynamic_range {
|
||||
FrameDynamicRange::Hdr { .. } => descriptor.format == TextureFormat::Rgba16Float,
|
||||
FrameDynamicRange::Sdr => matches!(
|
||||
descriptor.format,
|
||||
TextureFormat::Rgba8Unorm | TextureFormat::Bgra8Unorm | TextureFormat::Rgba16Float
|
||||
),
|
||||
}
|
||||
}
|
||||
pub(super) fn texture_usage(
|
||||
f: &TextureFamily,
|
||||
|
||||
@@ -25,7 +25,6 @@ pub enum ExecutionClass {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FullscreenPolicy {
|
||||
Copy,
|
||||
ToneMap,
|
||||
HdrSameExtent,
|
||||
BloomExtract,
|
||||
BloomComposite,
|
||||
@@ -335,15 +334,6 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inherently_observable: false,
|
||||
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 {
|
||||
key: "color_balance",
|
||||
version: 1,
|
||||
@@ -418,7 +408,7 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
},
|
||||
Contract {
|
||||
key: "frame_out",
|
||||
version: 1,
|
||||
version: 2,
|
||||
execution: ExecutionClass::Frame,
|
||||
inputs: FRAME_OUT_IN,
|
||||
outputs: NONE_OUT,
|
||||
|
||||
@@ -209,9 +209,6 @@ pub enum NormalizedParameters {
|
||||
clear_color: [f64; 4],
|
||||
},
|
||||
FullscreenCopy,
|
||||
ToneMap {
|
||||
exposure: f32,
|
||||
},
|
||||
ColorBalance {
|
||||
mode: ColorBalanceMode,
|
||||
factor: f32,
|
||||
@@ -258,7 +255,50 @@ pub enum NormalizedParameters {
|
||||
LuminanceEdge {
|
||||
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)]
|
||||
|
||||
@@ -330,9 +330,6 @@ fn validate_fullscreen_execution(
|
||||
let vector = |v: &[f32], min, max| v.iter().all(|x| scalar(x, min, max));
|
||||
let valid_parameters = match (key, &execution.parameters) {
|
||||
("fullscreen_copy", NormalizedParameters::FullscreenCopy) => true,
|
||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => {
|
||||
exposure.is_finite() && (0.0..=32.0).contains(exposure)
|
||||
}
|
||||
(
|
||||
"color_balance",
|
||||
NormalizedParameters::ColorBalance {
|
||||
@@ -566,14 +563,6 @@ fn validate_fullscreen_execution(
|
||||
&& target_d.format != TextureFormat::Depth32Float
|
||||
&& 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::HdrSameExtent => hdr(target_d) && target_d.extent == source.extent,
|
||||
FullscreenPolicy::BloomComposite => {
|
||||
@@ -1477,11 +1466,40 @@ pub fn prepare_runtime_plan(
|
||||
}
|
||||
|
||||
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(
|
||||
"frame_out parameters mismatch",
|
||||
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 {
|
||||
return Err(invalid(
|
||||
@@ -1546,7 +1564,7 @@ pub fn prepare_runtime_plan(
|
||||
)
|
||||
})?;
|
||||
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(
|
||||
"frame_out texture descriptor is incompatible",
|
||||
format!("textureFamilies[{}].source.descriptor", family.id),
|
||||
|
||||
@@ -6,8 +6,11 @@ use serde_json::{json, Value};
|
||||
fn input(node: &str, socket: &str) -> Value {
|
||||
json!({"node":node,"socket":socket})
|
||||
}
|
||||
fn node(id: &str, key: &str, parameters: Value, inputs: Value) -> Value {
|
||||
json!({"id":id,"state":"enabled","executor":{"key":key,"version":1},"parameters":parameters,"inputs":inputs})
|
||||
fn node(id: &str, key: &str, mut parameters: Value, inputs: Value) -> Value {
|
||||
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 {
|
||||
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");
|
||||
g["nodes"][copy]["parameters"] = json!({"obsolete":true});
|
||||
assert_eq!(compile_error(g).code, "GRAPH_PARAMETERS_INVALID");
|
||||
assert_eq!(CONTRACTS.len(), 17);
|
||||
assert_eq!(CONTRACTS.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -839,7 +842,6 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
||||
("pipeline_registry", 1),
|
||||
("pipeline", 1),
|
||||
("fullscreen_copy", 1),
|
||||
("tone_map", 1),
|
||||
("color_balance", 1),
|
||||
("exposure_contrast", 1),
|
||||
("saturation", 1),
|
||||
@@ -848,7 +850,7 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
||||
("bloom_blur", 1),
|
||||
("bloom_composite", 1),
|
||||
("luminance_edge", 1),
|
||||
("frame_out", 1),
|
||||
("frame_out", 2),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -864,7 +866,6 @@ fn exact_phase_four_contract_catalog_and_mesh_metadata() {
|
||||
("pipeline_registry", None),
|
||||
("pipeline", None),
|
||||
("fullscreen_copy", Some(FullscreenPolicy::Copy)),
|
||||
("tone_map", Some(FullscreenPolicy::ToneMap)),
|
||||
("color_balance", Some(FullscreenPolicy::HdrSameExtent)),
|
||||
("exposure_contrast", 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());
|
||||
}
|
||||
|
||||
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]
|
||||
fn runtime_rejects_coordinated_contract_and_texture_target_mutations() {
|
||||
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")
|
||||
.unwrap();
|
||||
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 {
|
||||
color_attachments, ..
|
||||
} = &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"));
|
||||
}
|
||||
|
||||
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]
|
||||
fn runtime_rejects_mesh_query_predicate_shape_mutations() {
|
||||
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")
|
||||
.unwrap();
|
||||
let invalid_parameters = [
|
||||
(
|
||||
"tone_map",
|
||||
NormalizedParameters::ToneMap {
|
||||
exposure: f32::INFINITY,
|
||||
},
|
||||
),
|
||||
(
|
||||
"bloom_extract",
|
||||
NormalizedParameters::BloomExtract {
|
||||
@@ -3130,55 +3387,23 @@ fn runtime_rechecks_pipeline_attachment_descriptors() {
|
||||
.unwrap()
|
||||
.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 color = attachment(&graph, "color");
|
||||
mutate_descriptor(&mut graph, color, |descriptor| {
|
||||
mutate_texture_descriptor(&mut graph, color, |descriptor| {
|
||||
descriptor.format = TextureFormat::Depth32Float;
|
||||
});
|
||||
assert_runtime_path(&graph, format!("executions[{pipeline}].inputs"));
|
||||
|
||||
let mut graph = baseline.clone();
|
||||
let depth = attachment(&graph, "depth");
|
||||
mutate_descriptor(&mut graph, depth, |descriptor| {
|
||||
mutate_texture_descriptor(&mut graph, depth, |descriptor| {
|
||||
descriptor.format = TextureFormat::Rgba16Float;
|
||||
});
|
||||
assert_runtime_path(&graph, format!("executions[{pipeline}].inputs"));
|
||||
|
||||
let mut graph = baseline;
|
||||
let color = attachment(&graph, "color");
|
||||
mutate_descriptor(&mut graph, color, |descriptor| {
|
||||
mutate_texture_descriptor(&mut graph, color, |descriptor| {
|
||||
descriptor.extent = NormalizedTextureExtent::Absolute {
|
||||
width: 4,
|
||||
height: 4,
|
||||
|
||||
@@ -102,7 +102,7 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
(
|
||||
surface,
|
||||
wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
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));
|
||||
}
|
||||
fn linear_to_srgb(x: vec3<f32>) -> vec3<f32> {
|
||||
let low = x * 12.92; let high = 1.055 * pow(x, vec3(1.0 / 2.4)) - vec3(0.055);
|
||||
return select(high, low, x <= vec3(0.0031308));
|
||||
let safe = clamp(x, vec3(0.0), vec3(1.0));
|
||||
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); }
|
||||
@fragment fn fs_color_balance(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
let c=sample_source(in.uv); var graded: vec3<f32>;
|
||||
|
||||
+404
-14
@@ -33,6 +33,24 @@ struct FullscreenUniforms {
|
||||
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(
|
||||
key: &str,
|
||||
parameters: &crate::render_graph::NormalizedParameters,
|
||||
@@ -40,11 +58,7 @@ fn pack_fullscreen_uniforms(
|
||||
use crate::render_graph::NormalizedParameters;
|
||||
let mut values = [[0.; 4]; 8];
|
||||
let first = match (key, parameters) {
|
||||
(
|
||||
"fullscreen_copy" | "frame_out",
|
||||
NormalizedParameters::FullscreenCopy | NormalizedParameters::FrameOut,
|
||||
) => [0.; 4],
|
||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => [*exposure, 0., 0., 0.],
|
||||
("fullscreen_copy", NormalizedParameters::FullscreenCopy) => [0.; 4],
|
||||
(
|
||||
"color_balance",
|
||||
NormalizedParameters::ColorBalance {
|
||||
@@ -134,10 +148,68 @@ fn pack_fullscreen_uniforms(
|
||||
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> {
|
||||
match key {
|
||||
"fullscreen_copy" | "frame_out" => Some("fs_copy"),
|
||||
"tone_map" => Some("fs_tone_map"),
|
||||
"fullscreen_copy" => Some("fs_copy"),
|
||||
"frame_out" => Some("fs_frame_out"),
|
||||
"color_balance" => Some("fs_color_balance"),
|
||||
"exposure_contrast" => Some("fs_exposure_contrast"),
|
||||
"saturation" => Some("fs_saturation"),
|
||||
@@ -153,7 +225,7 @@ fn resolve_fullscreen_entry(key: &str) -> Option<&'static str> {
|
||||
#[cfg(test)]
|
||||
mod fullscreen_tests {
|
||||
use super::*;
|
||||
use crate::render_graph::{ColorBalanceMode, NormalizedParameters};
|
||||
use crate::render_graph::*;
|
||||
|
||||
fn assert_packed(key: &str, parameters: NormalizedParameters, expected: &[[f32; 4]]) {
|
||||
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_eq!(bytemuck::bytes_of(&packed).len(), 128);
|
||||
assert!(
|
||||
pack_fullscreen_uniforms("tone_map", &NormalizedParameters::FullscreenCopy).is_none()
|
||||
pack_fullscreen_uniforms("frame_out", &NormalizedParameters::FullscreenCopy).is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_entries_are_explicit() {
|
||||
assert_eq!(resolve_fullscreen_entry("fullscreen_copy"), Some("fs_copy"));
|
||||
assert_eq!(resolve_fullscreen_entry("frame_out"), Some("fs_copy"));
|
||||
assert_eq!(resolve_fullscreen_entry("tone_map"), Some("fs_tone_map"));
|
||||
assert_eq!(resolve_fullscreen_entry("frame_out"), Some("fs_frame_out"));
|
||||
assert_eq!(resolve_fullscreen_entry("tone_map"), None);
|
||||
assert_eq!(
|
||||
resolve_fullscreen_entry("color_balance"),
|
||||
Some("fs_color_balance")
|
||||
@@ -223,6 +295,296 @@ mod fullscreen_tests {
|
||||
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 {
|
||||
NormalizedParameters::ColorBalance {
|
||||
mode,
|
||||
@@ -1218,6 +1580,15 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
..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();
|
||||
for (index, execution) in graph.executions.iter().enumerate() {
|
||||
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")),
|
||||
}
|
||||
};
|
||||
let values =
|
||||
let values = if frame_out {
|
||||
pack_frame_out_uniforms(&execution.parameters, &runtime.surface)
|
||||
} else {
|
||||
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;
|
||||
let uniform =
|
||||
self.context
|
||||
@@ -1347,7 +1729,15 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
resource: wgpu::BindingResource::Sampler(
|
||||
if sampler_choice
|
||||
== Some(FullscreenSamplerChoice::Nearest)
|
||||
{
|
||||
&nearest_sampler
|
||||
} else {
|
||||
&sampler
|
||||
},
|
||||
),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
|
||||
Reference in New Issue
Block a user