Skip to main content

unity_formats/
pipeline.rs

1//! A Universal Render Pipeline asset's lighting and shadow settings, decoded from the bytes of its
2//! `UniversalRenderPipelineAsset` MonoBehaviour (a build has no type tree for a script's fields).
3//!
4//! The field order is the class's declaration order in the Universal RP package (checked against its 17.6.0 source and
5//! against one player: the 452 bytes of its three assets read to the same settings as the keywords of the shader
6//! variants the player stores for them, as [this test of primase](https://github.com/Kalmarv/primase/tree/main/worlds/unity/tests/resolve_shaders.rs)
7//! compares). It holds for `k_AssetVersion` 13 only; another version is refused. A field is a 4-byte value, or a bool padded to 4 bytes; an object reference is 12 bytes (file id and a 64-bit
8//! path id) and an array is its count and elements. The class's later fields (batching, post-processing, the GPU
9//! resident drawer) are not read, and the layout past the first of them is not verified.
10
11use crate::reader::Reader;
12use serde::Serialize;
13
14use crate::{Result, invalid};
15
16/// The lighting and shadow settings of a Universal Render Pipeline asset as stored (`UniversalRenderPipelineAsset`,
17/// asset version 13; this module's docs state the verified layout): the light modes as `LightRenderingMode`
18/// numbers them (0 disabled, 1 per pixel, 2 per vertex), the shadow cascade count (1 to 4), the settings that decide
19/// which of a pass's keywords the pipeline sets.
20#[derive(Clone, Debug, PartialEq, Serialize)]
21pub struct UnityUrpSettings {
22    /// The main light's rendering mode (`m_MainLightRenderingMode`).
23    pub main_light_mode: i64,
24    /// Whether the main light casts shadows.
25    pub main_light_shadows: bool,
26    /// The additional lights' rendering mode (`m_AdditionalLightsRenderingMode`).
27    pub additional_lights_mode: i64,
28    /// Whether additional lights cast shadows.
29    pub additional_light_shadows: bool,
30    /// Whether reflection probes blend.
31    pub reflection_probe_blending: bool,
32    /// Whether reflection probes use box projection.
33    pub reflection_probe_box_projection: bool,
34    /// Whether reflection probes are packed into an atlas.
35    pub reflection_probe_atlas: bool,
36    /// `m_ShadowDistance`.
37    pub shadow_distance: f32,
38    /// `m_ShadowCascadeCount`.
39    pub shadow_cascades: u8,
40    /// Whether soft shadows are supported.
41    pub soft_shadows: bool,
42    /// Whether mixed lighting is supported.
43    pub mixed_lighting: bool,
44    /// Whether light cookies are supported.
45    pub light_cookies: bool,
46    /// Whether light layers are supported.
47    pub light_layers: bool,
48    /// Whether LOD cross-fading is enabled.
49    pub lod_cross_fade: bool,
50    /// `ShEvalMode`: 0 auto, 1 per vertex, 2 mixed, 3 per pixel.
51    pub sh_eval_mode: i64,
52    /// `LightProbeSystem`: 0 legacy light probes, 1 probe volumes.
53    pub light_probe_system: i64,
54}
55
56/// The asset version the layout below is verified for.
57const ASSET_VERSION: i32 = 13;
58
59/// The settings of the asset whose script fields are `bytes` (the bytes after the MonoBehaviour's base fields).
60pub fn read_urp_settings(bytes: &[u8]) -> Result<UnityUrpSettings> {
61    let mut r = Reader::new(bytes, false);
62    let flag = |r: &mut Reader| -> Result<bool> {
63        let b = r.bool()?;
64        r.align(4)?;
65        Ok(b)
66    };
67    let version = r.i32()?;
68    if version != ASSET_VERSION {
69        return Err(invalid(format!(
70            "Universal Render Pipeline asset version {version}: only version {ASSET_VERSION} has a verified layout"
71        )));
72    }
73    r.i32()?; // k_AssetPreviousVersion
74    r.i32()?; // m_RendererType
75    r.take(12)?; // m_RendererData (a reference)
76    let renderers = r.u32()?;
77    let renderers_len = usize::try_from(renderers)
78        .ok()
79        .and_then(|n| n.checked_mul(12))
80        .ok_or_else(|| invalid(format!("renderer list of {renderers} entries too long")))?;
81    r.take(renderers_len)?;
82    r.i32()?; // m_DefaultRendererIndex
83    r.take(8)?; // m_RequireDepthTexture, m_RequireOpaqueTexture
84    r.i32()?; // m_OpaqueDownsampling
85    r.take(8)?; // m_SupportsTerrainHoles, m_SupportsHDR
86    r.i32()?; // m_HDRColorBufferPrecision
87    r.i32()?; // m_MSAA
88    r.f32()?; // m_RenderScale
89    r.i32()?; // m_UpscalingFilter
90    r.take(4)?; // m_FsrOverrideSharpness
91    r.f32()?; // m_FsrSharpness
92    let lod_cross_fade = flag(&mut r)?;
93    r.i32()?; // m_LODCrossFadeDitheringType
94    let sh_eval_mode = r.i32()?;
95    let light_probe_system = r.i32()?;
96    r.i32()?; // m_ProbeVolumeMemoryBudget
97    r.i32()?; // m_ProbeVolumeBlendingMemoryBudget
98    r.take(16)?; // the four probe volume streaming and scenario flags
99    r.i32()?; // m_ProbeVolumeSHBands
100    let main_light_mode = r.i32()?;
101    let main_light_shadows = flag(&mut r)?;
102    r.i32()?; // m_MainLightShadowmapResolution
103    let additional_lights_mode = r.i32()?;
104    r.i32()?; // m_AdditionalLightsPerObjectLimit
105    let additional_light_shadows = flag(&mut r)?;
106    r.take(16)?; // the additional lights' shadowmap resolution and its three tiers
107    let reflection_probe_blending = flag(&mut r)?;
108    let reflection_probe_box_projection = flag(&mut r)?;
109    let reflection_probe_atlas = flag(&mut r)?;
110    let shadow_distance = r.f32()?;
111    let shadow_cascades = r.i32()?;
112    r.take(4 + 8 + 12 + 4 + 4 + 4)?; // the cascade splits and border, the depth and normal bias
113    let soft_shadows = flag(&mut r)?;
114    r.take(4)?; // m_ConservativeEnclosingSphere
115    r.i32()?; // m_NumIterationsEnclosingSphere
116    r.i32()?; // m_SoftShadowQuality
117    r.i32()?; // m_AdditionalLightsCookieResolution
118    r.i32()?; // m_AdditionalLightsCookieFormat
119    r.take(8)?; // m_UseSRPBatcher, m_SupportsDynamicBatching
120    let mixed_lighting = flag(&mut r)?;
121    let light_cookies = flag(&mut r)?;
122    let light_layers = flag(&mut r)?;
123    // a layout that does not hold reads values outside what the fields can be
124    let in_range = |what: &str, ok: bool| -> Result<()> {
125        ok.then_some(()).ok_or_else(|| {
126            invalid(format!(
127                "Universal Render Pipeline asset: {what} out of range (an unverified layout)"
128            ))
129        })
130    };
131    in_range("m_ShadowCascadeCount", (1..=4).contains(&shadow_cascades))?;
132    in_range(
133        "m_ShadowDistance",
134        shadow_distance.is_finite() && shadow_distance >= 0.0,
135    )?;
136    in_range("m_MainLightRenderingMode", (0..=2).contains(&main_light_mode))?;
137    in_range(
138        "m_AdditionalLightsRenderingMode",
139        (0..=2).contains(&additional_lights_mode),
140    )?;
141    in_range("m_ShEvalMode", (0..=3).contains(&sh_eval_mode))?;
142    Ok(UnityUrpSettings {
143        main_light_mode: i64::from(main_light_mode),
144        main_light_shadows,
145        additional_lights_mode: i64::from(additional_lights_mode),
146        additional_light_shadows,
147        reflection_probe_blending,
148        reflection_probe_box_projection,
149        reflection_probe_atlas,
150        shadow_distance,
151        shadow_cascades: u8::try_from(shadow_cascades).map_err(|_| {
152            invalid(format!(
153                "Universal Render Pipeline asset: m_ShadowCascadeCount {shadow_cascades} out of range"
154            ))
155        })?,
156        soft_shadows,
157        mixed_lighting,
158        light_cookies,
159        light_layers,
160        lod_cross_fade,
161        sh_eval_mode: i64::from(sh_eval_mode),
162        light_probe_system: i64::from(light_probe_system),
163    })
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    /// A synthetic asset in the layout: each field at the position the real bytes of a player's asset put it.
171    fn asset(cascades: i32) -> Vec<u8> {
172        let mut b: Vec<u8> = Vec::new();
173        let int = |b: &mut Vec<u8>, v: i32| b.extend(v.to_le_bytes());
174        let float = |b: &mut Vec<u8>, v: f32| b.extend(v.to_le_bytes());
175        let bool4 = |b: &mut Vec<u8>, v: bool| b.extend(i32::from(v).to_le_bytes());
176        int(&mut b, 13); // k_AssetVersion
177        int(&mut b, 13);
178        int(&mut b, 1); // renderer type
179        b.extend([0; 12]); // m_RendererData
180        int(&mut b, 1); // one renderer
181        b.extend([0; 12]);
182        int(&mut b, 0); // default renderer
183        bool4(&mut b, true); // depth texture
184        bool4(&mut b, false); // opaque texture
185        int(&mut b, 1);
186        bool4(&mut b, false); // terrain holes
187        bool4(&mut b, true); // HDR
188        int(&mut b, 0);
189        int(&mut b, 4); // MSAA
190        float(&mut b, 1.0);
191        int(&mut b, 0); // upscaling filter
192        bool4(&mut b, false);
193        float(&mut b, 0.92);
194        bool4(&mut b, true); // LOD cross fade
195        int(&mut b, 1);
196        int(&mut b, 2); // SH evaluation: mixed
197        int(&mut b, 0); // legacy light probes
198        int(&mut b, 1024);
199        int(&mut b, 256);
200        for _ in 0..4 {
201            bool4(&mut b, false);
202        }
203        int(&mut b, 1); // SH bands
204        int(&mut b, 1); // main light: per pixel
205        bool4(&mut b, true); // main light shadows
206        int(&mut b, 4096);
207        int(&mut b, 1); // additional lights: per pixel
208        int(&mut b, 8);
209        bool4(&mut b, true); // additional light shadows
210        for v in [1024, 256, 512, 1024] {
211            int(&mut b, v);
212        }
213        bool4(&mut b, true); // reflection probe blending
214        bool4(&mut b, false); // box projection
215        bool4(&mut b, true); // atlas
216        float(&mut b, 400.0);
217        int(&mut b, cascades);
218        float(&mut b, 0.25);
219        for v in [0.1, 0.3, 0.123, 0.29, 0.54, 0.11, 0.1, 0.5] {
220            float(&mut b, v);
221        }
222        bool4(&mut b, true); // soft shadows
223        bool4(&mut b, true); // conservative sphere
224        int(&mut b, 64);
225        int(&mut b, 3);
226        int(&mut b, 2048);
227        int(&mut b, 3);
228        bool4(&mut b, true);
229        bool4(&mut b, false);
230        bool4(&mut b, true); // mixed lighting
231        bool4(&mut b, false); // light cookies
232        bool4(&mut b, true); // light layers
233        b
234    }
235
236    /// The fields the keywords depend on are read from where the layout puts them, bools padded to 4 bytes and the
237    /// renderer list's length moving what follows it.
238    #[test]
239    fn a_urp_assets_settings_are_read_by_the_verified_layout() {
240        let s = read_urp_settings(&asset(4)).unwrap();
241        assert_eq!((s.shadow_cascades, s.shadow_distance), (4, 400.0));
242        assert!(s.main_light_shadows && s.soft_shadows && s.additional_light_shadows);
243        assert!(
244            s.reflection_probe_blending && !s.reflection_probe_box_projection && s.reflection_probe_atlas
245        );
246        assert!(s.mixed_lighting && !s.light_cookies && s.light_layers && s.lod_cross_fade);
247        assert_eq!(
248            (s.main_light_mode, s.additional_lights_mode, s.sh_eval_mode),
249            (1, 1, 2)
250        );
251        // another version, and a value no field can hold
252        let mut other = asset(4);
253        other[0] = 12;
254        assert!(read_urp_settings(&other).is_err());
255        assert!(read_urp_settings(&asset(7)).is_err());
256        assert!(read_urp_settings(&asset(4)[..100]).is_err());
257    }
258}