Skip to main content

unity_formats/
pipeline.rs

1//! The Universal Render Pipeline's objects, decoded from the bytes of their MonoBehaviours (a build has no type tree for
2//! a script's fields): a pipeline asset's renderers and its lighting, shadow and texture settings
3//! ([`read_urp_settings`]), a renderer's features, layer masks and modes ([`read_urp_renderer`]), a renderer feature's
4//! `m_Active` ([`read_urp_feature_active`]) and the settings of the Decal and Full Screen Pass features
5//! ([`read_urp_decal_feature`], [`read_urp_full_screen_feature`]), and a camera's URP data ([`read_urp_camera`]).
6//!
7//! The field order is each class's declaration order in the Universal RP package. A field is a 4-byte value, or a
8//! bool padded to 4 bytes; an object reference is 12 bytes (a [`PPtr`]: file id and a 64-bit path id) and an array is
9//! its count and elements. A value outside what its field can hold is refused as a sign of an unverified layout.
10//!
11//! - The asset's layout ([`UrpAssetLayout`]) is Universal RP 17.3's field order, read to the asset's last byte, and is
12//!   identified by the Unity version of the player before any field is read: it is verified on the players of Unity
13//!   6000.3.12f1 (Sledding Game) and 6000.3.15f1 (PEAK), each of whose assets reads to exactly its bytes, with the
14//!   renderers and settings an independent reading makes (tests/urp.rs) and the settings the keywords of the shader
15//!   variants the player stores agree with (as
16//!   [this test of primase](https://github.com/Kalmarv/primase/tree/main/worlds/unity/tests/resolve_shaders.rs)
17//!   compares). A player of another Unity version is refused by name: Big Walk's (6000.3.17f1) assets differ after
18//!   `m_ReflectionProbeBlending`. It holds for `k_AssetVersion` 13 only; another version is refused.
19//! - The renderer, feature and camera layouts are those of Universal RP 17.3 (the package of Unity 6000.3), checked
20//!   against PEAK's, Sledding Game's and Big Walk's players: every renderer, every Decal and Full Screen Pass feature
21//!   and every camera of their scenes reads to exactly its bytes, with the fields an independent reading makes
22//!   (tests/urp.rs). A length the layout does not account for is refused, and so is a renderer `m_AssetVersion`, a
23//!   Full Screen Pass `m_Version` or a camera `m_Version` other than the one read.
24
25use crate::player::PPtr;
26use crate::reader::Reader;
27use crate::version::UnityVersion;
28use serde::Serialize;
29
30use crate::{Result, invalid};
31
32/// The settings of a Universal Render Pipeline asset as stored (`UniversalRenderPipelineAsset`, asset version 13; this
33/// module's docs state the verified layout): its renderers and the one cameras use by default, whether it makes the
34/// depth and opaque textures, its MSAA sample count, and the lighting and shadow settings that decide which of a
35/// pass's keywords the pipeline sets: the light modes as `LightRenderingMode` numbers them (0 disabled, 1 per pixel,
36/// 2 per vertex) and the shadow cascade count (1 to 4).
37#[derive(Clone, Debug, PartialEq, Serialize)]
38pub struct UnityUrpSettings {
39    /// The renderers (`m_RendererDataList`), each a `ScriptableRendererData` asset, in the order a camera's renderer
40    /// index counts them.
41    pub renderers: Vec<PPtr>,
42    /// The index in [`UnityUrpSettings::renderers`] of the renderer a camera with renderer index -1 draws with
43    /// (`m_DefaultRendererIndex`).
44    pub default_renderer: i64,
45    /// Whether the pipeline makes the camera depth texture (`m_RequireDepthTexture`), for a camera whose depth texture
46    /// option is the pipeline's setting.
47    pub require_depth_texture: bool,
48    /// Whether the pipeline makes the camera opaque texture (`m_RequireOpaqueTexture`), likewise.
49    pub require_opaque_texture: bool,
50    /// The MSAA sample count (`m_MSAA`: 1, 2, 4 or 8).
51    pub msaa: i64,
52    /// The main light's rendering mode (`m_MainLightRenderingMode`).
53    pub main_light_mode: i64,
54    /// Whether the main light casts shadows.
55    pub main_light_shadows: bool,
56    /// The additional lights' rendering mode (`m_AdditionalLightsRenderingMode`).
57    pub additional_lights_mode: i64,
58    /// Whether additional lights cast shadows.
59    pub additional_light_shadows: bool,
60    /// Whether reflection probes blend.
61    pub reflection_probe_blending: bool,
62    /// Whether reflection probes use box projection.
63    pub reflection_probe_box_projection: bool,
64    /// Whether reflection probes are packed into an atlas.
65    pub reflection_probe_atlas: bool,
66    /// `m_ShadowDistance`.
67    pub shadow_distance: f32,
68    /// `m_ShadowCascadeCount`.
69    pub shadow_cascades: u8,
70    /// Whether soft shadows are supported.
71    pub soft_shadows: bool,
72    /// Whether mixed lighting is supported.
73    pub mixed_lighting: bool,
74    /// Whether light cookies are supported.
75    pub light_cookies: bool,
76    /// Whether light layers are supported.
77    pub light_layers: bool,
78    /// Whether LOD cross-fading is enabled.
79    pub lod_cross_fade: bool,
80    /// `ShEvalMode`: 0 auto, 1 per vertex, 2 mixed, 3 per pixel.
81    pub sh_eval_mode: i64,
82    /// `LightProbeSystem`: 0 legacy light probes, 1 probe volumes.
83    pub light_probe_system: i64,
84}
85
86/// The asset version the layout below is verified for.
87const ASSET_VERSION: i32 = 13;
88
89/// A serialized layout of `UniversalRenderPipelineAsset` whose every field is verified (this module's docs say on
90/// which players).
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92#[non_exhaustive]
93pub enum UrpAssetLayout {
94    /// Universal RP 17.3's, as Unity 6000.3.12f1 and 6000.3.15f1 players store it.
95    Urp17_3,
96}
97
98/// The Unity versions whose players' assets are verified to have [`UrpAssetLayout::Urp17_3`].
99const URP_17_3_PLAYERS: [&str; 2] = ["6000.3.12f1", "6000.3.15f1"];
100
101impl UrpAssetLayout {
102    /// The layout of the assets a player of Unity `unity` stores (its serialized files' version); a version on whose
103    /// players no layout is verified is refused, naming it.
104    pub fn of(unity: UnityVersion) -> Result<Self> {
105        for v in URP_17_3_PLAYERS {
106            if UnityVersion::parse(v)? == unity {
107                return Ok(UrpAssetLayout::Urp17_3);
108            }
109        }
110        Err(invalid(format!(
111            "a Universal Render Pipeline asset of a Unity {unity} player: its layout is verified only on the players \
112             of Unity {} (the package's fields are not recorded in the asset, and other players store others)",
113            URP_17_3_PLAYERS.join(" and ")
114        )))
115    }
116}
117
118/// The settings of the asset whose script fields are `bytes` (the bytes after the MonoBehaviour's base fields), stored
119/// in `layout` ([`UrpAssetLayout::of`] the player's Unity version; [`crate::player::Player::urp_settings`] reads an
120/// asset of a player so). Every field is read, to the last byte.
121pub fn read_urp_settings(bytes: &[u8], layout: UrpAssetLayout) -> Result<UnityUrpSettings> {
122    match layout {
123        UrpAssetLayout::Urp17_3 => {}
124    }
125    let mut r = Reader::new(bytes, false);
126    let version = r.i32()?;
127    if version != ASSET_VERSION {
128        return Err(invalid(format!(
129            "Universal Render Pipeline asset version {version}: only version {ASSET_VERSION} has a verified layout"
130        )));
131    }
132    r.i32()?; // k_AssetPreviousVersion
133    r.i32()?; // m_RendererType
134    r.take(12)?; // m_RendererData (a reference)
135    let renderers = pptrs(&mut r, "m_RendererDataList")?;
136    let default_renderer = r.i32()?;
137    let require_depth_texture = flag(&mut r)?;
138    let require_opaque_texture = flag(&mut r)?;
139    r.i32()?; // m_OpaqueDownsampling
140    r.take(8)?; // m_SupportsTerrainHoles, m_SupportsHDR
141    r.i32()?; // m_HDRColorBufferPrecision
142    let msaa = r.i32()?;
143    r.f32()?; // m_RenderScale
144    r.i32()?; // m_UpscalingFilter
145    r.take(4)?; // m_FsrOverrideSharpness
146    r.f32()?; // m_FsrSharpness
147    let lod_cross_fade = flag(&mut r)?;
148    r.i32()?; // m_LODCrossFadeDitheringType
149    let sh_eval_mode = r.i32()?;
150    let light_probe_system = r.i32()?;
151    r.i32()?; // m_ProbeVolumeMemoryBudget
152    r.i32()?; // m_ProbeVolumeBlendingMemoryBudget
153    r.take(16)?; // the four probe volume streaming and scenario flags
154    r.i32()?; // m_ProbeVolumeSHBands
155    let main_light_mode = r.i32()?;
156    let main_light_shadows = flag(&mut r)?;
157    r.i32()?; // m_MainLightShadowmapResolution
158    let additional_lights_mode = r.i32()?;
159    r.i32()?; // m_AdditionalLightsPerObjectLimit
160    let additional_light_shadows = flag(&mut r)?;
161    r.take(16)?; // the additional lights' shadowmap resolution and its three tiers
162    let reflection_probe_blending = flag(&mut r)?;
163    let reflection_probe_box_projection = flag(&mut r)?;
164    let reflection_probe_atlas = flag(&mut r)?;
165    let shadow_distance = r.f32()?;
166    let shadow_cascades = r.i32()?;
167    r.take(4 + 8 + 12 + 4 + 4 + 4)?; // the cascade splits and border, the depth and normal bias
168    let soft_shadows = flag(&mut r)?;
169    r.take(4)?; // m_ConservativeEnclosingSphere
170    r.i32()?; // m_NumIterationsEnclosingSphere
171    r.i32()?; // m_SoftShadowQuality
172    r.i32()?; // m_AdditionalLightsCookieResolution
173    r.i32()?; // m_AdditionalLightsCookieFormat
174    r.take(8)?; // m_UseSRPBatcher, m_SupportsDynamicBatching
175    let mixed_lighting = flag(&mut r)?;
176    let light_cookies = flag(&mut r)?;
177    let light_layers = flag(&mut r)?;
178    r.i32()?; // m_DebugLevel
179    r.i32()?; // m_StoreActionsOptimization
180    flag(&mut r)?; // m_UseAdaptivePerformance
181    r.i32()?; // m_ColorGradingMode
182    r.i32()?; // m_ColorGradingLutSize
183    for _ in 0..4 {
184        // m_AllowPostProcessAlphaOutput, m_UseFastSRGBLinearConversion and the two lens flare flags
185        flag(&mut r)?;
186    }
187    r.i32()?; // m_GPUResidentDrawerMode
188    r.f32()?; // m_SmallMeshScreenPercentage
189    flag(&mut r)?; // m_GPUResidentDrawerEnableOcclusionCullingInCameras
190    r.i32()?; // m_ShadowType
191    flag(&mut r)?; // m_LocalShadowsSupported
192    r.take(12)?; // m_LocalShadowsAtlasResolution, m_MaxPixelLights, m_ShadowAtlasResolution
193    r.i32()?; // m_VolumeFrameworkUpdateMode
194    pptr(&mut r)?; // m_VolumeProfile
195    // apvScenesData: two SerializedDictionary, each its keys' list and its values' list (scene GUIDs to bounds, and
196    // to whether the scene has probe volumes)
197    strings(&mut r, "apvScenesData.obsoleteSceneBounds.m_Keys")?;
198    let n = list_len(&mut r, "apvScenesData.obsoleteSceneBounds.m_Values", 24)?;
199    for _ in 0..n {
200        r.take(24)?; // a Bounds: its centre and extents
201    }
202    strings(&mut r, "apvScenesData.obsoleteHasProbeVolumes.m_Keys")?;
203    let n = list_len(&mut r, "apvScenesData.obsoleteHasProbeVolumes.m_Values", 1)?;
204    for _ in 0..n {
205        r.bool()?;
206    }
207    r.align(4)?;
208    r.i32()?; // m_ShaderVariantLogLevel
209    r.i32()?; // m_ShadowCascades
210    r.take(24)?; // m_Textures: its two textures
211    used_up(&r, "UniversalRenderPipelineAsset")?;
212    // a layout that does not hold reads values outside what the fields can be
213    let in_range = |what: &str, ok: bool| -> Result<()> {
214        ok.then_some(()).ok_or_else(|| {
215            invalid(format!(
216                "Universal Render Pipeline asset: {what} out of range (an unverified layout)"
217            ))
218        })
219    };
220    in_range("m_ShadowCascadeCount", (1..=4).contains(&shadow_cascades))?;
221    in_range(
222        "m_ShadowDistance",
223        shadow_distance.is_finite() && shadow_distance >= 0.0,
224    )?;
225    in_range("m_MainLightRenderingMode", (0..=2).contains(&main_light_mode))?;
226    in_range(
227        "m_AdditionalLightsRenderingMode",
228        (0..=2).contains(&additional_lights_mode),
229    )?;
230    in_range("m_ShEvalMode", (0..=3).contains(&sh_eval_mode))?;
231    in_range("m_MSAA", [1, 2, 4, 8].contains(&msaa))?;
232    in_range(
233        "m_DefaultRendererIndex",
234        usize::try_from(default_renderer).is_ok_and(|i| i < renderers.len()),
235    )?;
236    Ok(UnityUrpSettings {
237        renderers,
238        default_renderer: i64::from(default_renderer),
239        require_depth_texture,
240        require_opaque_texture,
241        msaa: i64::from(msaa),
242        main_light_mode: i64::from(main_light_mode),
243        main_light_shadows,
244        additional_lights_mode: i64::from(additional_lights_mode),
245        additional_light_shadows,
246        reflection_probe_blending,
247        reflection_probe_box_projection,
248        reflection_probe_atlas,
249        shadow_distance,
250        shadow_cascades: u8::try_from(shadow_cascades).map_err(|_| {
251            invalid(format!(
252                "Universal Render Pipeline asset: m_ShadowCascadeCount {shadow_cascades} out of range"
253            ))
254        })?,
255        soft_shadows,
256        mixed_lighting,
257        light_cookies,
258        light_layers,
259        lod_cross_fade,
260        sh_eval_mode: i64::from(sh_eval_mode),
261        light_probe_system: i64::from(light_probe_system),
262    })
263}
264
265/// A bool, padded to 4 bytes.
266fn flag(r: &mut Reader) -> Result<bool> {
267    let b = r.bool()?;
268    r.align(4)?;
269    Ok(b)
270}
271
272/// A list of strings, each its byte count and its UTF-8 bytes padded to 4 bytes.
273fn strings(r: &mut Reader, what: &str) -> Result<Vec<String>> {
274    let n = list_len(r, what, 4)?;
275    (0..n)
276        .map(|_| {
277            let len = list_len(r, what, 1)?;
278            let s = std::str::from_utf8(r.take(len)?)
279                .map_err(|_| invalid(format!("{what}: a string that is not UTF-8")))?
280                .to_owned();
281            r.align(4)?;
282            Ok(s)
283        })
284        .collect()
285}
286
287/// An object reference: its file id and path id.
288fn pptr(r: &mut Reader) -> Result<PPtr> {
289    Ok(PPtr {
290        file_id: r.i32()?,
291        path_id: r.i64()?,
292    })
293}
294
295/// A list's count, refused when its `size`-byte elements would run past the bytes left (`what` names the list).
296fn list_len(r: &mut Reader, what: &str, size: usize) -> Result<usize> {
297    let n = r.u32()?;
298    usize::try_from(n)
299        .ok()
300        .filter(|&n| n.checked_mul(size).is_some_and(|len| len <= r.remaining()))
301        .ok_or_else(|| {
302            invalid(format!(
303                "{what}: {n} entries of {size} bytes, {} bytes left",
304                r.remaining()
305            ))
306        })
307}
308
309/// A list of object references.
310fn pptrs(r: &mut Reader, what: &str) -> Result<Vec<PPtr>> {
311    let n = list_len(r, what, 12)?;
312    (0..n).map(|_| pptr(r)).collect()
313}
314
315/// A 4-byte enum value, refused unless it is one of `values` (`what` names the class and field).
316fn choice(r: &mut Reader, what: &str, values: &[i32]) -> Result<i64> {
317    let v = r.i32()?;
318    if values.contains(&v) {
319        Ok(i64::from(v))
320    } else {
321        Err(invalid(format!(
322            "{what} {v} (the values: {values:?}; an unverified layout)"
323        )))
324    }
325}
326
327/// The values `0..n` of an enum.
328fn first(n: i32) -> Vec<i32> {
329    (0..n).collect()
330}
331
332/// Refused unless the bytes were used up: a stored object longer than the layout is another layout.
333fn used_up(r: &Reader, what: &str) -> Result<()> {
334    match r.remaining() {
335        0 => Ok(()),
336        n => Err(invalid(format!(
337            "{what}: {n} bytes past the verified layout's {} (another layout)",
338            r.pos
339        ))),
340    }
341}
342
343/// The `m_AssetVersion` of `UniversalRendererData` the renderer layout is verified for.
344const RENDERER_ASSET_VERSION: i32 = 3;
345
346/// The bytes a `UniversalRendererData` stores after `m_UseNativeRenderPass` in the verified layout: `xrSystemData`,
347/// `postProcessData`, the asset version, the three layer masks, the stencil state and the eight modes. A Universal RP
348/// compiled without XR support (`ENABLE_VR && ENABLE_XR_MODULE` unset) stores no `xrSystemData` (84 bytes); no player
349/// read so far is one, so that layout is refused.
350const RENDERER_TAIL: usize = 96;
351
352/// The renderer's default stencil state (`m_DefaultStencilState`, a `StencilStateData`): Unity's `CompareFunction`
353/// (0 disabled, 1 never, 2 less, 3 equal, 4 less or equal, 5 greater, 6 not equal, 7 greater or equal, 8 always) and
354/// `StencilOp` (0 keep, 1 zero, 2 replace, 3 increment saturate, 4 decrement saturate, 5 invert, 6 increment wrap,
355/// 7 decrement wrap) numbers.
356#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
357pub struct UnityStencilState {
358    /// Whether the renderer's opaque and transparent draws use this state (`overrideStencilState`).
359    pub override_stencil: bool,
360    /// `stencilReference`.
361    pub reference: i64,
362    /// `stencilCompareFunction`.
363    pub compare: i64,
364    /// `passOperation`.
365    pub pass: i64,
366    /// `failOperation`.
367    pub fail: i64,
368    /// `zFailOperation`.
369    pub z_fail: i64,
370}
371
372/// A Universal Render Pipeline renderer as stored (`UniversalRendererData`, asset version 3, Universal RP 17.3; this
373/// module's docs say how the layout is verified). Its modes are numbered as the package's enums are:
374///
375/// - `RenderingMode`: 0 forward, 1 deferred, 2 Forward+, 3 Deferred+.
376/// - `DepthPrimingMode`: 0 disabled, 1 auto, 2 forced.
377/// - `CopyDepthMode`: 0 after opaques, 1 after transparents, 2 force prepass.
378/// - `DepthFormat`: 0 default, 90 16-bit, 91 24-bit, 93 32-bit, 151, 92 and 94 the same with 8 stencil bits.
379/// - `IntermediateTextureMode`: 0 auto, 1 always.
380#[derive(Clone, Debug, PartialEq, Serialize)]
381pub struct UnityUrpRenderer {
382    /// The renderer features (`m_RendererFeatures`), each a `ScriptableRendererFeature` asset, in the order the
383    /// renderer asks them for passes.
384    pub features: Vec<PPtr>,
385    /// `m_RendererFeatureMap` as stored: the editor's ids for the features. A player does not read it, and a stored
386    /// map need not match the features (Big Walk's second renderer has no features and one entry).
387    pub feature_map: Vec<i64>,
388    /// `m_UseNativeRenderPass`.
389    pub native_render_pass: bool,
390    /// `xrSystemData`.
391    pub xr_system_data: PPtr,
392    /// `postProcessData`.
393    pub post_process_data: PPtr,
394    /// The layers a depth prepass draws (`m_PrepassLayerMask`, one bit per layer).
395    pub prepass_layer_mask: u32,
396    /// The layers the opaque draws draw (`m_OpaqueLayerMask`).
397    pub opaque_layer_mask: u32,
398    /// The layers the transparent draws draw (`m_TransparentLayerMask`).
399    pub transparent_layer_mask: u32,
400    /// `m_DefaultStencilState`.
401    pub stencil: UnityStencilState,
402    /// `m_ShadowTransparentReceive`.
403    pub shadow_transparent_receive: bool,
404    /// `m_RenderingMode`.
405    pub rendering_mode: i64,
406    /// `m_DepthPrimingMode`.
407    pub depth_priming_mode: i64,
408    /// `m_CopyDepthMode`.
409    pub copy_depth_mode: i64,
410    /// `m_DepthAttachmentFormat`.
411    pub depth_attachment_format: i64,
412    /// `m_DepthTextureFormat`.
413    pub depth_texture_format: i64,
414    /// `m_AccurateGbufferNormals`.
415    pub accurate_gbuffer_normals: bool,
416    /// `m_IntermediateTextureMode`.
417    pub intermediate_texture_mode: i64,
418}
419
420/// The renderer whose script fields are `bytes` (the bytes after the MonoBehaviour's base fields).
421pub fn read_urp_renderer(bytes: &[u8]) -> Result<UnityUrpRenderer> {
422    const WHAT: &str = "Universal Render Pipeline renderer";
423    let mut r = Reader::new(bytes, false);
424    // `debugShaders` (two shaders) and `probeVolumeResources` (seven objects)
425    r.take(12 * 9)?;
426    let features = pptrs(&mut r, "m_RendererFeatures")?;
427    let n = list_len(&mut r, "m_RendererFeatureMap", 8)?;
428    let feature_map = (0..n).map(|_| r.i64()).collect::<Result<Vec<_>>>()?;
429    let native_render_pass = flag(&mut r)?;
430    if r.remaining() != RENDERER_TAIL {
431        return Err(invalid(format!(
432            "{WHAT}: {} bytes after m_UseNativeRenderPass, the verified layout stores {RENDERER_TAIL} (84 is the layout \
433             without xrSystemData, which is not verified)",
434            r.remaining()
435        )));
436    }
437    let xr_system_data = pptr(&mut r)?;
438    let post_process_data = pptr(&mut r)?;
439    let version = r.i32()?;
440    if version != RENDERER_ASSET_VERSION {
441        return Err(invalid(format!(
442            "{WHAT} asset version {version}: only version {RENDERER_ASSET_VERSION} has a verified layout"
443        )));
444    }
445    let prepass_layer_mask = r.u32()?;
446    let opaque_layer_mask = r.u32()?;
447    let transparent_layer_mask = r.u32()?;
448    let stencil = UnityStencilState {
449        override_stencil: flag(&mut r)?,
450        reference: i64::from(r.i32()?),
451        compare: choice(&mut r, "stencilCompareFunction", &first(9))?,
452        pass: choice(&mut r, "passOperation", &first(8))?,
453        fail: choice(&mut r, "failOperation", &first(8))?,
454        z_fail: choice(&mut r, "zFailOperation", &first(8))?,
455    };
456    let depth_formats = [0, 90, 91, 92, 93, 94, 151];
457    let renderer = UnityUrpRenderer {
458        features,
459        feature_map,
460        native_render_pass,
461        xr_system_data,
462        post_process_data,
463        prepass_layer_mask,
464        opaque_layer_mask,
465        transparent_layer_mask,
466        stencil,
467        shadow_transparent_receive: flag(&mut r)?,
468        rendering_mode: choice(&mut r, "m_RenderingMode", &first(4))?,
469        depth_priming_mode: choice(&mut r, "m_DepthPrimingMode", &first(3))?,
470        copy_depth_mode: choice(&mut r, "m_CopyDepthMode", &first(3))?,
471        depth_attachment_format: choice(&mut r, "m_DepthAttachmentFormat", &depth_formats)?,
472        depth_texture_format: choice(&mut r, "m_DepthTextureFormat", &depth_formats)?,
473        accurate_gbuffer_normals: flag(&mut r)?,
474        intermediate_texture_mode: choice(&mut r, "m_IntermediateTextureMode", &first(2))?,
475    };
476    used_up(&r, WHAT)?;
477    Ok(renderer)
478}
479
480/// Whether the renderer feature whose script fields are `bytes` is on (`ScriptableRendererFeature.m_Active`, the
481/// first field of every feature class). A renderer asks only its active features for passes.
482pub fn read_urp_feature_active(bytes: &[u8]) -> Result<bool> {
483    flag(&mut Reader::new(bytes, false))
484}
485
486/// The script class of Universal RP's Decal renderer feature.
487pub const URP_DECAL_FEATURE: &str = "UnityEngine.Rendering.Universal.DecalRendererFeature";
488
489/// The script class of Universal RP's Full Screen Pass renderer feature.
490pub const URP_FULL_SCREEN_FEATURE: &str = "UnityEngine.Rendering.Universal.FullScreenPassRendererFeature";
491
492/// A Decal renderer feature as stored ([`URP_DECAL_FEATURE`]: `m_Active` and its `DecalSettings`, Universal RP
493/// 17.3).
494#[derive(Clone, Debug, PartialEq, Serialize)]
495pub struct UnityUrpDecalFeature {
496    /// `m_Active`.
497    pub active: bool,
498    /// `technique`, a `DecalTechniqueOption`: 0 automatic, 1 DBuffer, 2 screen space.
499    pub technique: i64,
500    /// `maxDrawDistance`.
501    pub max_draw_distance: f32,
502    /// `decalLayers`.
503    pub decal_layers: bool,
504    /// `dBufferSettings.surfaceData`, a `DecalSurfaceData`: 0 albedo, 1 albedo and normal, 2 albedo, normal,
505    /// metallic, occlusion and smoothness.
506    pub dbuffer_surface_data: i64,
507    /// `screenSpaceSettings.normalBlend`, a `DecalNormalBlend`: 0 low, 1 medium, 2 high.
508    pub screen_space_normal_blend: i64,
509}
510
511/// The Decal feature whose script fields are `bytes`.
512pub fn read_urp_decal_feature(bytes: &[u8]) -> Result<UnityUrpDecalFeature> {
513    let mut r = Reader::new(bytes, false);
514    let feature = UnityUrpDecalFeature {
515        active: flag(&mut r)?,
516        technique: choice(&mut r, "DecalSettings.technique", &first(3))?,
517        max_draw_distance: r.f32()?,
518        decal_layers: flag(&mut r)?,
519        dbuffer_surface_data: choice(&mut r, "DBufferSettings.surfaceData", &first(3))?,
520        screen_space_normal_blend: choice(&mut r, "DecalScreenSpaceSettings.normalBlend", &first(3))?,
521    };
522    used_up(&r, "Decal renderer feature")?;
523    Ok(feature)
524}
525
526/// The `m_Version` of a Full Screen Pass feature the layout is verified for (`AddFetchColorBufferCheckbox`).
527const FULL_SCREEN_VERSION: i32 = 1;
528
529/// A Full Screen Pass renderer feature as stored ([`URP_FULL_SCREEN_FEATURE`], `m_Version` 1, Universal RP 17.3).
530#[derive(Clone, Debug, PartialEq, Serialize)]
531pub struct UnityUrpFullScreenFeature {
532    /// `m_Active`.
533    pub active: bool,
534    /// When its pass runs (`injectionPoint`, a `RenderPassEvent`): 450 before transparents, 550 before
535    /// post-processing, 600 after post-processing.
536    pub injection_point: i64,
537    /// Whether the pass copies the camera colour for its material to read (`fetchColorBuffer`).
538    pub fetch_color_buffer: bool,
539    /// The textures the pass asks the renderer for (`requirements`, `ScriptableRenderPassInput` flags: 1 depth,
540    /// 2 normals, 4 colour, 8 motion vectors; the editor's "everything" stores -1, every bit set).
541    pub requirements: i64,
542    /// `passMaterial`.
543    pub pass_material: PPtr,
544    /// `passIndex`.
545    pub pass_index: i64,
546    /// `bindDepthStencilAttachment`.
547    pub bind_depth_stencil_attachment: bool,
548}
549
550/// The Full Screen Pass feature whose script fields are `bytes`.
551pub fn read_urp_full_screen_feature(bytes: &[u8]) -> Result<UnityUrpFullScreenFeature> {
552    const WHAT: &str = "Full Screen Pass renderer feature";
553    let mut r = Reader::new(bytes, false);
554    let active = flag(&mut r)?;
555    let injection_point = choice(&mut r, "injectionPoint", &[450, 550, 600])?;
556    let fetch_color_buffer = flag(&mut r)?;
557    let requirements = i64::from(r.i32()?);
558    let pass_material = pptr(&mut r)?;
559    let pass_index = i64::from(r.i32()?);
560    let bind_depth_stencil_attachment = flag(&mut r)?;
561    let version = r.i32()?;
562    if version != FULL_SCREEN_VERSION {
563        return Err(invalid(format!(
564            "{WHAT} m_Version {version}: only version {FULL_SCREEN_VERSION} is verified"
565        )));
566    }
567    used_up(&r, WHAT)?;
568    Ok(UnityUrpFullScreenFeature {
569        active,
570        injection_point,
571        fetch_color_buffer,
572        requirements,
573        pass_material,
574        pass_index,
575        bind_depth_stencil_attachment,
576    })
577}
578
579/// The script class of the URP data Universal RP keeps beside a Camera.
580pub const URP_CAMERA_DATA: &str = "UnityEngine.Rendering.Universal.UniversalAdditionalCameraData";
581
582/// The `m_Version` of a camera's URP data the layout is verified for (`DepthAndOpaqueTextureOptions`).
583const CAMERA_VERSION: i32 = 2;
584
585/// A camera's temporal anti-aliasing settings (`TemporalAA.Settings`).
586#[derive(Clone, Debug, PartialEq, Serialize)]
587pub struct UnityTaaSettings {
588    /// `m_Quality`, a `TemporalAAQuality`: 0 very low to 4 very high.
589    pub quality: i64,
590    /// `m_FrameInfluence`.
591    pub frame_influence: f32,
592    /// `m_JitterScale`.
593    pub jitter_scale: f32,
594    /// `m_MipBias`.
595    pub mip_bias: f32,
596    /// `m_VarianceClampScale`.
597    pub variance_clamp_scale: f32,
598    /// `m_ContrastAdaptiveSharpening`.
599    pub contrast_adaptive_sharpening: f32,
600}
601
602/// A camera's URP data as stored ([`URP_CAMERA_DATA`], `m_Version` 2, Universal RP 17.3). The options are
603/// `CameraOverrideOption`s: 0 off, 1 on, 2 the pipeline asset's setting.
604#[derive(Clone, Debug, PartialEq, Serialize)]
605pub struct UnityUrpCamera {
606    /// `m_RenderShadows`.
607    pub render_shadows: bool,
608    /// Whether the camera makes the depth texture (`m_RequiresDepthTextureOption`).
609    pub depth_texture_option: i64,
610    /// Whether the camera makes the opaque texture (`m_RequiresOpaqueTextureOption`).
611    pub opaque_texture_option: i64,
612    /// `m_CameraType`, a `CameraRenderType`: 0 base, 1 overlay.
613    pub camera_type: i64,
614    /// The overlay cameras stacked on a base camera (`m_Cameras`).
615    pub stack: Vec<PPtr>,
616    /// The index of the pipeline asset's renderer the camera draws with (`m_RendererIndex`); -1 for the asset's
617    /// default renderer.
618    pub renderer_index: i64,
619    /// `m_VolumeLayerMask`.
620    pub volume_layer_mask: u32,
621    /// `m_VolumeTrigger`.
622    pub volume_trigger: PPtr,
623    /// `m_VolumeFrameworkUpdateModeOption`: 0 every frame, 1 by script, 2 the pipeline asset's setting.
624    pub volume_update_mode: i64,
625    /// `m_RenderPostProcessing`.
626    pub render_post_processing: bool,
627    /// `m_Antialiasing`, an `AntialiasingMode`: 0 none, 1 FXAA, 2 SMAA, 3 temporal.
628    pub antialiasing: i64,
629    /// `m_AntialiasingQuality`: 0 low, 1 medium, 2 high.
630    pub antialiasing_quality: i64,
631    /// `m_StopNaN`.
632    pub stop_nan: bool,
633    /// `m_Dithering`.
634    pub dithering: bool,
635    /// `m_ClearDepth`.
636    pub clear_depth: bool,
637    /// `m_AllowXRRendering`.
638    pub allow_xr_rendering: bool,
639    /// `m_AllowHDROutput`.
640    pub allow_hdr_output: bool,
641    /// `m_UseScreenCoordOverride`.
642    pub use_screen_coord_override: bool,
643    /// `m_ScreenSizeOverride`.
644    pub screen_size_override: [f32; 4],
645    /// `m_ScreenCoordScaleBias`.
646    pub screen_coord_scale_bias: [f32; 4],
647    /// `m_RequiresDepthTexture`, the field the depth texture option replaced.
648    pub requires_depth_texture: bool,
649    /// `m_RequiresColorTexture`, the field the opaque texture option replaced.
650    pub requires_color_texture: bool,
651    /// `m_TaaSettings`.
652    pub taa: UnityTaaSettings,
653}
654
655/// The camera URP data whose script fields are `bytes`.
656pub fn read_urp_camera(bytes: &[u8]) -> Result<UnityUrpCamera> {
657    const WHAT: &str = "Universal Render Pipeline camera data";
658    let mut r = Reader::new(bytes, false);
659    let vec4 = |r: &mut Reader| -> Result<[f32; 4]> { Ok([r.f32()?, r.f32()?, r.f32()?, r.f32()?]) };
660    let camera = UnityUrpCamera {
661        render_shadows: flag(&mut r)?,
662        depth_texture_option: choice(&mut r, "m_RequiresDepthTextureOption", &first(3))?,
663        opaque_texture_option: choice(&mut r, "m_RequiresOpaqueTextureOption", &first(3))?,
664        camera_type: choice(&mut r, "m_CameraType", &first(2))?,
665        stack: pptrs(&mut r, "m_Cameras")?,
666        renderer_index: i64::from(r.i32()?),
667        volume_layer_mask: r.u32()?,
668        volume_trigger: pptr(&mut r)?,
669        volume_update_mode: choice(&mut r, "m_VolumeFrameworkUpdateModeOption", &first(3))?,
670        render_post_processing: flag(&mut r)?,
671        antialiasing: choice(&mut r, "m_Antialiasing", &first(4))?,
672        antialiasing_quality: choice(&mut r, "m_AntialiasingQuality", &first(3))?,
673        stop_nan: flag(&mut r)?,
674        dithering: flag(&mut r)?,
675        clear_depth: flag(&mut r)?,
676        allow_xr_rendering: flag(&mut r)?,
677        allow_hdr_output: flag(&mut r)?,
678        use_screen_coord_override: flag(&mut r)?,
679        screen_size_override: vec4(&mut r)?,
680        screen_coord_scale_bias: vec4(&mut r)?,
681        requires_depth_texture: flag(&mut r)?,
682        requires_color_texture: flag(&mut r)?,
683        taa: UnityTaaSettings {
684            quality: choice(&mut r, "TemporalAA.Settings.m_Quality", &first(5))?,
685            frame_influence: r.f32()?,
686            jitter_scale: r.f32()?,
687            mip_bias: r.f32()?,
688            variance_clamp_scale: r.f32()?,
689            contrast_adaptive_sharpening: r.f32()?,
690        },
691    };
692    let version = r.i32()?;
693    if version != CAMERA_VERSION {
694        return Err(invalid(format!(
695            "{WHAT} m_Version {version}: only version {CAMERA_VERSION} is verified"
696        )));
697    }
698    used_up(&r, WHAT)?;
699    Ok(camera)
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    /// A synthetic asset in the layout: each field at the position the real bytes of a player's asset put it.
707    fn asset(cascades: i32, default_renderer: i32) -> Vec<u8> {
708        let mut b: Vec<u8> = Vec::new();
709        let int = |b: &mut Vec<u8>, v: i32| b.extend(v.to_le_bytes());
710        let float = |b: &mut Vec<u8>, v: f32| b.extend(v.to_le_bytes());
711        let bool4 = |b: &mut Vec<u8>, v: bool| b.extend(i32::from(v).to_le_bytes());
712        int(&mut b, 13); // k_AssetVersion
713        int(&mut b, 13);
714        int(&mut b, 1); // renderer type
715        b.extend([0; 12]); // m_RendererData
716        int(&mut b, 1); // one renderer
717        int(&mut b, 0);
718        b.extend(6168i64.to_le_bytes());
719        int(&mut b, default_renderer);
720        bool4(&mut b, true); // depth texture
721        bool4(&mut b, false); // opaque texture
722        int(&mut b, 1);
723        bool4(&mut b, false); // terrain holes
724        bool4(&mut b, true); // HDR
725        int(&mut b, 0);
726        int(&mut b, 4); // MSAA
727        float(&mut b, 1.0);
728        int(&mut b, 0); // upscaling filter
729        bool4(&mut b, false);
730        float(&mut b, 0.92);
731        bool4(&mut b, true); // LOD cross fade
732        int(&mut b, 1);
733        int(&mut b, 2); // SH evaluation: mixed
734        int(&mut b, 0); // legacy light probes
735        int(&mut b, 1024);
736        int(&mut b, 256);
737        for _ in 0..4 {
738            bool4(&mut b, false);
739        }
740        int(&mut b, 1); // SH bands
741        int(&mut b, 1); // main light: per pixel
742        bool4(&mut b, true); // main light shadows
743        int(&mut b, 4096);
744        int(&mut b, 1); // additional lights: per pixel
745        int(&mut b, 8);
746        bool4(&mut b, true); // additional light shadows
747        for v in [1024, 256, 512, 1024] {
748            int(&mut b, v);
749        }
750        bool4(&mut b, true); // reflection probe blending
751        bool4(&mut b, false); // box projection
752        bool4(&mut b, true); // atlas
753        float(&mut b, 400.0);
754        int(&mut b, cascades);
755        float(&mut b, 0.25);
756        for v in [0.1, 0.3, 0.123, 0.29, 0.54, 0.11, 0.1, 0.5] {
757            float(&mut b, v);
758        }
759        bool4(&mut b, true); // soft shadows
760        bool4(&mut b, true); // conservative sphere
761        int(&mut b, 64);
762        int(&mut b, 3);
763        int(&mut b, 2048);
764        int(&mut b, 3);
765        bool4(&mut b, true);
766        bool4(&mut b, false);
767        bool4(&mut b, true); // mixed lighting
768        bool4(&mut b, false); // light cookies
769        bool4(&mut b, true); // light layers
770        // the fields after them, which no setting is read from: debug level to the volume framework's update mode
771        int(&mut b, 0);
772        int(&mut b, 0);
773        bool4(&mut b, true);
774        int(&mut b, 0);
775        int(&mut b, 32);
776        for _ in 0..4 {
777            bool4(&mut b, true);
778        }
779        int(&mut b, 0);
780        float(&mut b, 0.0);
781        bool4(&mut b, false);
782        int(&mut b, 1);
783        bool4(&mut b, false);
784        for v in [256, 0, 256, 2] {
785            int(&mut b, v);
786        }
787        b.extend([0; 12]); // m_VolumeProfile
788        // apvScenesData: one scene's bounds, one scene's flag (a 5-byte key, padded)
789        int(&mut b, 1);
790        int(&mut b, 5);
791        b.extend(b"scene\0\0\0");
792        int(&mut b, 1);
793        b.extend([0; 24]);
794        int(&mut b, 1);
795        int(&mut b, 5);
796        b.extend(b"scene\0\0\0");
797        int(&mut b, 1);
798        b.extend([1, 0, 0, 0]);
799        int(&mut b, 0); // m_ShaderVariantLogLevel
800        int(&mut b, 1); // m_ShadowCascades
801        b.extend([0; 24]); // m_Textures
802        b
803    }
804
805    /// The fields the keywords depend on are read from where the layout puts them, bools padded to 4 bytes and the
806    /// renderer list's length moving what follows it.
807    #[test]
808    fn a_urp_assets_settings_are_read_by_the_verified_layout() {
809        let read = |b: &[u8]| read_urp_settings(b, UrpAssetLayout::Urp17_3);
810        let s = read(&asset(4, 0)).unwrap();
811        let renderer = PPtr {
812            file_id: 0,
813            path_id: 6168,
814        };
815        assert_eq!((s.renderers.as_slice(), s.default_renderer), (&[renderer][..], 0));
816        assert!(s.require_depth_texture && !s.require_opaque_texture);
817        assert_eq!(s.msaa, 4);
818        assert_eq!((s.shadow_cascades, s.shadow_distance), (4, 400.0));
819        assert!(s.main_light_shadows && s.soft_shadows && s.additional_light_shadows);
820        assert!(
821            s.reflection_probe_blending && !s.reflection_probe_box_projection && s.reflection_probe_atlas
822        );
823        assert!(s.mixed_lighting && !s.light_cookies && s.light_layers && s.lod_cross_fade);
824        assert_eq!(
825            (s.main_light_mode, s.additional_lights_mode, s.sh_eval_mode),
826            (1, 1, 2)
827        );
828        // another version, and values no field can hold: a fifth cascade, a default renderer past the list
829        let mut other = asset(4, 0);
830        other[0] = 12;
831        assert!(read(&other).is_err());
832        assert!(read(&asset(7, 0)).is_err());
833        let past = read(&asset(4, 1)).unwrap_err().to_string();
834        assert!(past.contains("m_DefaultRendererIndex"), "{past}");
835        // the asset is read to its last byte: one short, or one more, is another layout
836        let whole = asset(4, 0);
837        assert!(read(&whole[..whole.len() - 4]).is_err());
838        let mut longer = whole.clone();
839        longer.extend([0; 4]);
840        let longer = read(&longer).unwrap_err().to_string();
841        assert!(longer.contains("past the verified layout"), "{longer}");
842    }
843
844    /// The layout is the player's Unity version's, known before any field is read: the versions it is verified on,
845    /// and no other.
846    #[test]
847    fn a_urp_assets_layout_is_identified_by_the_players_unity_version() {
848        let of = |v: &str| UrpAssetLayout::of(UnityVersion::parse(v).unwrap());
849        assert_eq!(of("6000.3.12f1").unwrap(), UrpAssetLayout::Urp17_3);
850        assert_eq!(of("6000.3.15f1").unwrap(), UrpAssetLayout::Urp17_3);
851        let refused = of("6000.3.17f1").unwrap_err().to_string();
852        assert!(refused.contains("Unity 6000.3.17f1 player"), "{refused}");
853        assert!(of("6000.6.4f1").is_err());
854    }
855
856    fn int(b: &mut Vec<u8>, v: i32) {
857        b.extend(v.to_le_bytes());
858    }
859
860    fn reference(b: &mut Vec<u8>, path_id: i64) {
861        int(b, 0);
862        b.extend(path_id.to_le_bytes());
863    }
864
865    /// A synthetic renderer in the layout: two features, the masks, the stencil state and the modes after them.
866    fn renderer(version: i32, mode: i32) -> Vec<u8> {
867        let mut b = vec![0; 12 * 9]; // debugShaders, probeVolumeResources
868        int(&mut b, 2);
869        reference(&mut b, 6001);
870        reference(&mut b, 6002);
871        int(&mut b, 2);
872        b.extend(7i64.to_le_bytes());
873        b.extend((-8i64).to_le_bytes());
874        int(&mut b, 1); // native render pass
875        reference(&mut b, 0); // xrSystemData
876        reference(&mut b, 6170); // postProcessData
877        int(&mut b, version);
878        for mask in [0x8a30_9437u32, 0x8a31_d437, u32::MAX] {
879            b.extend(mask.to_le_bytes());
880        }
881        for v in [1, 3, 8, 2, 0, 0] {
882            int(&mut b, v); // stencil: overridden, reference 3, always, replace
883        }
884        int(&mut b, 1); // shadow transparent receive
885        int(&mut b, mode);
886        int(&mut b, 0); // depth priming disabled
887        int(&mut b, 1); // copy depth after transparents
888        int(&mut b, 92);
889        int(&mut b, 0);
890        int(&mut b, 0);
891        int(&mut b, 1); // intermediate texture always
892        b
893    }
894
895    /// A renderer's fields are read from where the layout puts them, the feature list's length moving what follows;
896    /// a feature map is read as stored, whatever its length; another asset version, a mode no field can hold and a
897    /// length the layout does not account for (extra bytes, or the layout without `xrSystemData`) are refused.
898    #[test]
899    fn a_urp_renderer_is_read_by_the_verified_layout() {
900        let r = read_urp_renderer(&renderer(3, 2)).unwrap();
901        let ids: Vec<i64> = r.features.iter().map(|f| f.path_id).collect();
902        assert_eq!((ids, r.feature_map.clone()), (vec![6001, 6002], vec![7, -8]));
903        assert!(r.native_render_pass && r.shadow_transparent_receive && !r.accurate_gbuffer_normals);
904        assert_eq!((r.xr_system_data.path_id, r.post_process_data.path_id), (0, 6170));
905        assert_eq!(
906            (
907                r.prepass_layer_mask,
908                r.opaque_layer_mask,
909                r.transparent_layer_mask
910            ),
911            (0x8a30_9437, 0x8a31_d437, u32::MAX)
912        );
913        assert_eq!(
914            r.stencil,
915            UnityStencilState {
916                override_stencil: true,
917                reference: 3,
918                compare: 8,
919                pass: 2,
920                fail: 0,
921                z_fail: 0
922            }
923        );
924        assert_eq!(
925            (r.rendering_mode, r.depth_priming_mode, r.copy_depth_mode),
926            (2, 0, 1)
927        );
928        assert_eq!(
929            (
930                r.depth_attachment_format,
931                r.depth_texture_format,
932                r.intermediate_texture_mode
933            ),
934            (92, 0, 1)
935        );
936        let refusal = |b: &[u8]| read_urp_renderer(b).unwrap_err().to_string();
937        assert!(refusal(&renderer(2, 2)).contains("asset version 2"));
938        assert!(refusal(&renderer(3, 4)).contains("m_RenderingMode 4"));
939        let mut longer = renderer(3, 2);
940        longer.extend([0; 4]);
941        assert!(refusal(&longer).contains("100 bytes after m_UseNativeRenderPass"));
942        // the layout without xrSystemData: the same renderer 12 bytes shorter
943        let mut no_xr = renderer(3, 2);
944        no_xr.drain(160..172);
945        assert!(refusal(&no_xr).contains("84 bytes after"), "{}", refusal(&no_xr));
946        // a map of one entry for the two features: the bytes after it move back by one entry, as they are read
947        let mut map = renderer(3, 2);
948        map[136] = 1; // the map's count: one entry
949        map.drain(148..156);
950        let short = read_urp_renderer(&map).unwrap();
951        assert_eq!((short.features.len(), short.feature_map), (2, vec![7]));
952        assert_eq!(short.post_process_data.path_id, 6170);
953        assert!(read_urp_renderer(&renderer(3, 2)[..150]).is_err());
954    }
955
956    /// A feature's `m_Active` is its first field; the Decal and Full Screen Pass features' settings follow it, and a
957    /// value outside a field's enum, another version or a length past the layout is refused.
958    #[test]
959    fn urp_features_are_read_by_the_verified_layout() {
960        assert!(read_urp_feature_active(&[1, 0, 0, 0, 9, 9]).unwrap());
961        assert!(!read_urp_feature_active(&[0, 0, 0, 0]).unwrap());
962        assert!(read_urp_feature_active(&[2, 0, 0, 0]).is_err());
963        let decal = |technique: i32| {
964            let mut b = Vec::new();
965            int(&mut b, 1);
966            int(&mut b, technique);
967            b.extend(500f32.to_le_bytes());
968            int(&mut b, 0);
969            int(&mut b, 2);
970            int(&mut b, 0);
971            b
972        };
973        assert_eq!(
974            read_urp_decal_feature(&decal(1)).unwrap(),
975            UnityUrpDecalFeature {
976                active: true,
977                technique: 1,
978                max_draw_distance: 500.0,
979                decal_layers: false,
980                dbuffer_surface_data: 2,
981                screen_space_normal_blend: 0
982            }
983        );
984        assert!(read_urp_decal_feature(&decal(3)).is_err());
985        let mut longer = decal(1);
986        longer.extend([0; 4]);
987        assert!(read_urp_decal_feature(&longer).is_err());
988        let full_screen = |injection: i32, version: i32| {
989            let mut b = Vec::new();
990            int(&mut b, 1);
991            int(&mut b, injection);
992            int(&mut b, 1);
993            int(&mut b, -1); // every input
994            reference(&mut b, 7);
995            int(&mut b, 0);
996            int(&mut b, 0);
997            int(&mut b, version);
998            b
999        };
1000        let f = read_urp_full_screen_feature(&full_screen(600, 1)).unwrap();
1001        assert!(f.active && f.fetch_color_buffer && !f.bind_depth_stencil_attachment);
1002        assert_eq!(
1003            (
1004                f.injection_point,
1005                f.requirements,
1006                f.pass_material.path_id,
1007                f.pass_index
1008            ),
1009            (600, -1, 7, 0)
1010        );
1011        let refusal = |b: &[u8]| read_urp_full_screen_feature(b).unwrap_err().to_string();
1012        assert!(refusal(&full_screen(300, 1)).contains("injectionPoint 300"));
1013        assert!(refusal(&full_screen(600, 0)).contains("m_Version 0"));
1014    }
1015
1016    /// A synthetic camera's URP data in the layout, with `stack` overlay cameras.
1017    fn camera(stack: i32, version: i32) -> Vec<u8> {
1018        let mut b = Vec::new();
1019        int(&mut b, 1); // shadows
1020        int(&mut b, 2);
1021        int(&mut b, 2);
1022        int(&mut b, 0); // base
1023        int(&mut b, stack);
1024        for i in 0..stack {
1025            reference(&mut b, i64::from(i) + 100);
1026        }
1027        int(&mut b, -1); // the default renderer
1028        b.extend(0x8000_0000u32.to_le_bytes());
1029        reference(&mut b, 0);
1030        int(&mut b, 2);
1031        int(&mut b, 1); // post-processing
1032        int(&mut b, 3); // temporal anti-aliasing
1033        int(&mut b, 2);
1034        for v in [0, 1, 1, 1, 1, 0] {
1035            int(&mut b, v);
1036        }
1037        for v in [0f32, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0] {
1038            b.extend(v.to_le_bytes());
1039        }
1040        int(&mut b, 0);
1041        int(&mut b, 0);
1042        int(&mut b, 3); // TAA quality high
1043        for v in [0.1f32, 1.0, 0.0, 0.9, 0.0] {
1044            b.extend(v.to_le_bytes());
1045        }
1046        int(&mut b, version);
1047        b
1048    }
1049
1050    /// A camera's fields are read from where the layout puts them, the stack's length moving what follows; another
1051    /// version and a length past the layout are refused.
1052    #[test]
1053    fn a_urp_camera_is_read_by_the_verified_layout() {
1054        assert_eq!(camera(0, 2).len(), 148);
1055        let c = read_urp_camera(&camera(2, 2)).unwrap();
1056        let stack: Vec<i64> = c.stack.iter().map(|p| p.path_id).collect();
1057        assert_eq!(stack, vec![100, 101]);
1058        assert_eq!(
1059            (
1060                c.renderer_index,
1061                c.depth_texture_option,
1062                c.antialiasing,
1063                c.taa.quality
1064            ),
1065            (-1, 2, 3, 3)
1066        );
1067        assert!(c.render_shadows && c.render_post_processing && c.clear_depth && !c.stop_nan && c.dithering);
1068        assert_eq!(
1069            (c.volume_layer_mask, c.screen_coord_scale_bias),
1070            (0x8000_0000, [1.0, 1.0, 0.0, 0.0])
1071        );
1072        assert_eq!(c.taa.variance_clamp_scale, 0.9);
1073        assert!(
1074            read_urp_camera(&camera(0, 1))
1075                .unwrap_err()
1076                .to_string()
1077                .contains("m_Version 1")
1078        );
1079        let mut longer = camera(0, 2);
1080        longer.extend([0; 4]);
1081        assert!(read_urp_camera(&longer).is_err());
1082    }
1083}