Skip to main content

librashader_runtime/
binding.rs

1use crate::parameters::RuntimeParameters;
2use crate::uniforms::{BindUniform, NoUniformBinder, UniformStorage};
3use librashader_common::map::{FastHashMap, ShortString};
4use librashader_common::Size;
5use librashader_preprocess::ShaderParameter;
6use librashader_reflect::reflect::semantics::{
7    BindingMeta, MemberOffset, Semantic, TextureBinding, TextureSemantics, UniformBinding,
8    UniformMeta, UniqueSemantics,
9};
10use num_traits::Zero;
11use std::ops::{Deref, DerefMut};
12
13/// Trait for input textures used during uniform binding,
14pub trait TextureInput {
15    /// Gets the size of this input texture.
16    fn size(&self) -> Size<u32>;
17}
18
19/// A uniform member offset with context that needs to be resolved.
20pub trait ContextOffset<H, C, D = ()>
21where
22    H: BindUniform<C, f32, D>,
23    H: BindUniform<C, u32, D>,
24    H: BindUniform<C, i32, D>,
25    H: for<'a> BindUniform<C, &'a [f32; 3], D>,
26    H: for<'a> BindUniform<C, &'a [f32; 4], D>,
27    H: for<'a> BindUniform<C, &'a [f32; 16], D>,
28{
29    /// Gets the `MemberOffset` part of the offset.
30    fn offset(&self) -> MemberOffset;
31
32    /// Gets the context part of the offset.
33    fn context(&self) -> C;
34}
35
36impl<D, H> ContextOffset<H, Option<()>, D> for MemberOffset
37where
38    H: BindUniform<Option<()>, f32, D>,
39    H: BindUniform<Option<()>, u32, D>,
40    H: BindUniform<Option<()>, i32, D>,
41    H: for<'a> BindUniform<Option<()>, &'a [f32; 3], D>,
42    H: for<'a> BindUniform<Option<()>, &'a [f32; 4], D>,
43    H: for<'a> BindUniform<Option<()>, &'a [f32; 16], D>,
44{
45    fn offset(&self) -> MemberOffset {
46        *self
47    }
48
49    fn context(&self) -> Option<()> {
50        None
51    }
52}
53
54/// Inputs to binding semantics
55pub struct UniformInputs<'a> {
56    /// MVP
57    pub mvp: &'a [f32; 16],
58    /// FrameCount
59    pub frame_count: u32,
60    /// Rotation
61    pub rotation: u32,
62    /// TotalSubFrames
63    pub total_subframes: u32,
64    /// CurrentSubFrame
65    pub current_subframe: u32,
66    /// FrameDirection
67    pub frame_direction: i32,
68    /// OriginalAspectRatio (need to normalize)
69    pub aspect_ratio: f32,
70    /// OriginalFPS
71    pub frames_per_second: f32,
72    /// FrameTimeDelta
73    pub frametime_delta: u32,
74    /// OutputSize
75    pub framebuffer_size: Size<u32>,
76    /// FinalViewportSize
77    pub viewport_size: Size<u32>,
78    /// Grouped HDR inputs.
79    pub hdr_inputs: HdrUniformInputs,
80    /// Grouped sensor (gyroscope/accelerometer) inputs.
81    pub sensor_inputs: SensorUniformInputs,
82}
83
84/// Three-axis sensor readings bound to the shader as `vec3` uniforms.
85pub struct SensorUniformInputs {
86    /// Gyroscope (x, y, z)
87    pub gyroscope: [f32; 3],
88    /// Accelerometer (x, y, z)
89    pub accelerometer: [f32; 3],
90    /// AccelerometerRest (Device position at launch / resume) (x, y, z)
91    pub accelerometer_rest: [f32; 3],
92}
93
94pub struct HdrUniformInputs {
95    /// HDRMode
96    pub color_space: librashader_common::ColorSpace,
97    /// BrightnessNits
98    pub brightness_nits: f32,
99    /// ExpandGamut
100    pub expand_gamut: u32,
101}
102
103/// Trait that abstracts binding of semantics to shader uniforms.
104pub trait BindSemantics<H = NoUniformBinder, C = Option<()>, U = Box<[u8]>, P = Box<[u8]>>
105where
106    C: Copy,
107    U: Deref<Target = [u8]> + DerefMut,
108    P: Deref<Target = [u8]> + DerefMut,
109    H: BindUniform<C, f32, Self::DeviceContext>,
110    H: BindUniform<C, u32, Self::DeviceContext>,
111    H: BindUniform<C, i32, Self::DeviceContext>,
112    H: for<'b> BindUniform<C, &'b [f32; 3], Self::DeviceContext>,
113    H: for<'b> BindUniform<C, &'b [f32; 4], Self::DeviceContext>,
114    H: for<'b> BindUniform<C, &'b [f32; 16], Self::DeviceContext>,
115{
116    /// The type of the input texture used for semantic binding.
117    type InputTexture: TextureInput;
118
119    /// The set of texture samplers available.
120    type SamplerSet;
121
122    /// The descriptor set or object that holds sampler and texture bindings.
123    type DescriptorSet<'a>;
124
125    /// The device context containing the state of the graphics processor.
126    type DeviceContext;
127
128    /// The type of uniform offsets to use.
129    type UniformOffset: ContextOffset<H, C, Self::DeviceContext>;
130
131    /// Bind a texture to the input descriptor set
132    fn bind_texture<'a>(
133        descriptors: &mut Self::DescriptorSet<'a>,
134        samplers: &Self::SamplerSet,
135        binding: &TextureBinding,
136        texture: &Self::InputTexture,
137        device: &Self::DeviceContext,
138    );
139
140    #[allow(clippy::too_many_arguments)]
141    /// Write uniform and texture semantics to the provided storages.
142    fn bind_semantics<'a>(
143        device: &Self::DeviceContext,
144        sampler_set: &Self::SamplerSet,
145        uniform_storage: &mut UniformStorage<H, C, U, P, Self::DeviceContext>,
146        descriptor_set: &mut Self::DescriptorSet<'a>,
147        uniform_inputs: UniformInputs<'_>,
148        original: &Self::InputTexture,
149        source: &Self::InputTexture,
150        uniform_bindings: &FastHashMap<UniformBinding, Self::UniformOffset>,
151        texture_meta: &FastHashMap<Semantic<TextureSemantics>, TextureBinding>,
152        pass_outputs: impl Iterator<Item = Option<impl AsRef<Self::InputTexture>>>,
153        pass_feedback: impl Iterator<Item = Option<impl AsRef<Self::InputTexture>>>,
154        original_history: impl Iterator<Item = Option<impl AsRef<Self::InputTexture>>>,
155        lookup_textures: impl Iterator<Item = (usize, impl AsRef<Self::InputTexture>)>,
156        parameter_defaults: &FastHashMap<ShortString, ShaderParameter>,
157        runtime_parameters: &RuntimeParameters,
158    ) {
159        let runtime_parameters = runtime_parameters.parameters.load();
160        // Bind MVP
161        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::MVP.into()) {
162            uniform_storage.bind_mat4(
163                offset.offset(),
164                uniform_inputs.mvp,
165                offset.context(),
166                device,
167            );
168        }
169
170        // Bind OutputSize
171        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::Output.into()) {
172            uniform_storage.bind_vec4(
173                offset.offset(),
174                uniform_inputs.framebuffer_size,
175                offset.context(),
176                device,
177            );
178        }
179
180        // bind FinalViewportSize
181        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::FinalViewport.into()) {
182            uniform_storage.bind_vec4(
183                offset.offset(),
184                uniform_inputs.viewport_size,
185                offset.context(),
186                device,
187            );
188        }
189
190        // bind FrameCount
191        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::FrameCount.into()) {
192            uniform_storage.bind_scalar(
193                offset.offset(),
194                uniform_inputs.frame_count,
195                offset.context(),
196                device,
197            );
198        }
199
200        // bind FrameDirection
201        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::FrameDirection.into()) {
202            uniform_storage.bind_scalar(
203                offset.offset(),
204                uniform_inputs.frame_direction,
205                offset.context(),
206                device,
207            );
208        }
209
210        // bind Rotation
211        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::Rotation.into()) {
212            uniform_storage.bind_scalar(
213                offset.offset(),
214                uniform_inputs.rotation,
215                offset.context(),
216                device,
217            );
218        }
219
220        // bind TotalSubFrames
221        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::TotalSubFrames.into()) {
222            uniform_storage.bind_scalar(
223                offset.offset(),
224                uniform_inputs.total_subframes,
225                offset.context(),
226                device,
227            );
228        }
229
230        // bind CurrentSubFrames
231        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::CurrentSubFrame.into()) {
232            uniform_storage.bind_scalar(
233                offset.offset(),
234                uniform_inputs.current_subframe,
235                offset.context(),
236                device,
237            );
238        }
239
240        // bind OriginalFPS
241        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::OriginalFPS.into()) {
242            uniform_storage.bind_scalar(
243                offset.offset(),
244                uniform_inputs.frames_per_second,
245                offset.context(),
246                device,
247            );
248        }
249
250        // bind FrameTimeDelta
251        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::FrameTimeDelta.into()) {
252            uniform_storage.bind_scalar(
253                offset.offset(),
254                uniform_inputs.frametime_delta,
255                offset.context(),
256                device,
257            );
258        }
259
260        let mut aspect_ratio = uniform_inputs.aspect_ratio;
261        if aspect_ratio.is_zero() {
262            aspect_ratio = original.size().aspect_ratio();
263        }
264
265        // bind OriginalAspect
266        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::OriginalAspect.into()) {
267            uniform_storage.bind_scalar(offset.offset(), aspect_ratio, offset.context(), device);
268        }
269
270        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::OriginalAspectRotated.into()) {
271            let rotated_aspect = if uniform_inputs.rotation == 1 || uniform_inputs.rotation == 3 {
272                1.0f32 / aspect_ratio
273            } else {
274                aspect_ratio
275            };
276
277            uniform_storage.bind_scalar(offset.offset(), rotated_aspect, offset.context(), device);
278        }
279
280        let hdr = uniform_inputs.hdr_inputs;
281        // bind HDRMode
282        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::HDRMode.into()) {
283            uniform_storage.bind_scalar(
284                offset.offset(),
285                hdr.color_space as u32,
286                offset.context(),
287                device,
288            );
289        }
290
291        // bind BrightnessNits
292        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::BrightnessNits.into()) {
293            uniform_storage.bind_scalar(
294                offset.offset(),
295                hdr.brightness_nits,
296                offset.context(),
297                device,
298            );
299        }
300
301        // bind ExpandGamut
302        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::ExpandGamut.into()) {
303            uniform_storage.bind_scalar(
304                offset.offset(),
305                hdr.expand_gamut,
306                offset.context(),
307                device,
308            );
309        }
310
311        // Scanlines, InverseTonemap, HDR10, and SubpixelLayout are RA-internal
312        // hdr.frag uniforms that librashader does not expose for user
313        // configuration (RA drives them from its own swapchain mastering
314        // pipeline). Bind 0 whenever a shader declares them so the slot is
315        // deterministic.
316        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::Scanlines.into()) {
317            uniform_storage.bind_scalar(offset.offset(), 0.0f32, offset.context(), device);
318        }
319        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::InverseTonemap.into()) {
320            uniform_storage.bind_scalar(offset.offset(), 0.0f32, offset.context(), device);
321        }
322        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::HDR10.into()) {
323            uniform_storage.bind_scalar(offset.offset(), 0.0f32, offset.context(), device);
324        }
325        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::SubpixelLayout.into()) {
326            uniform_storage.bind_scalar(offset.offset(), 0u32, offset.context(), device);
327        }
328
329        let sensors = uniform_inputs.sensor_inputs;
330        // bind Gyroscope
331        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::Gyroscope.into()) {
332            uniform_storage.bind_vec3(offset.offset(), sensors.gyroscope, offset.context(), device);
333        }
334
335        // bind Accelerometer
336        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::Accelerometer.into()) {
337            uniform_storage.bind_vec3(
338                offset.offset(),
339                sensors.accelerometer,
340                offset.context(),
341                device,
342            );
343        }
344
345        // bind AccelerometerRest
346        if let Some(offset) = uniform_bindings.get(&UniqueSemantics::AccelerometerRest.into()) {
347            uniform_storage.bind_vec3(
348                offset.offset(),
349                sensors.accelerometer_rest,
350                offset.context(),
351                device,
352            );
353        }
354
355        // bind Original sampler
356        if let Some(binding) = texture_meta.get(&TextureSemantics::Original.semantics(0)) {
357            Self::bind_texture(descriptor_set, sampler_set, binding, original, device);
358        }
359
360        // bind OriginalSize
361        if let Some(offset) = uniform_bindings.get(&TextureSemantics::Original.semantics(0).into())
362        {
363            uniform_storage.bind_vec4(offset.offset(), original.size(), offset.context(), device);
364        }
365
366        // bind Source sampler
367        if let Some(binding) = texture_meta.get(&TextureSemantics::Source.semantics(0)) {
368            Self::bind_texture(descriptor_set, sampler_set, binding, source, device);
369        }
370
371        // bind SourceSize
372        if let Some(offset) = uniform_bindings.get(&TextureSemantics::Source.semantics(0).into()) {
373            uniform_storage.bind_vec4(offset.offset(), source.size(), offset.context(), device);
374        }
375
376        // OriginalHistory0 aliases OriginalHistory
377
378        // bind OriginalHistory0 sampler
379        if let Some(binding) = texture_meta.get(&TextureSemantics::OriginalHistory.semantics(0)) {
380            Self::bind_texture(descriptor_set, sampler_set, binding, original, device);
381        }
382
383        // bind OriginalHistory0Size
384        if let Some(offset) =
385            uniform_bindings.get(&TextureSemantics::OriginalHistory.semantics(0).into())
386        {
387            uniform_storage.bind_vec4(offset.offset(), original.size(), offset.context(), device);
388        }
389
390        // bind OriginalHistory1-..
391        for (index, history) in original_history.enumerate() {
392            let Some(history) = history else {
393                continue;
394            };
395
396            let history = history.as_ref();
397
398            if let Some(binding) =
399                texture_meta.get(&TextureSemantics::OriginalHistory.semantics(index + 1))
400            {
401                Self::bind_texture(descriptor_set, sampler_set, binding, history, device);
402            }
403
404            if let Some(offset) = uniform_bindings.get(
405                &TextureSemantics::OriginalHistory
406                    .semantics(index + 1)
407                    .into(),
408            ) {
409                uniform_storage.bind_vec4(
410                    offset.offset(),
411                    history.size(),
412                    offset.context(),
413                    device,
414                );
415            }
416        }
417
418        // bind PassOutput0..
419        // The caller should be responsible for limiting this up to
420        // pass_index
421        for (index, output) in pass_outputs.enumerate() {
422            let Some(output) = output else {
423                continue;
424            };
425
426            let output = output.as_ref();
427
428            if let Some(binding) = texture_meta.get(&TextureSemantics::PassOutput.semantics(index))
429            {
430                Self::bind_texture(descriptor_set, sampler_set, binding, output, device);
431            }
432
433            if let Some(offset) =
434                uniform_bindings.get(&TextureSemantics::PassOutput.semantics(index).into())
435            {
436                uniform_storage.bind_vec4(offset.offset(), output.size(), offset.context(), device);
437            }
438        }
439
440        // bind PassFeedback0..
441        for (index, feedback) in pass_feedback.enumerate() {
442            let Some(output) = feedback else {
443                continue;
444            };
445
446            let feedback = output.as_ref();
447
448            if let Some(binding) =
449                texture_meta.get(&TextureSemantics::PassFeedback.semantics(index))
450            {
451                Self::bind_texture(descriptor_set, sampler_set, binding, feedback, device);
452            }
453
454            if let Some(offset) =
455                uniform_bindings.get(&TextureSemantics::PassFeedback.semantics(index).into())
456            {
457                uniform_storage.bind_vec4(
458                    offset.offset(),
459                    feedback.size(),
460                    offset.context(),
461                    device,
462                );
463            }
464        }
465
466        // bind User parameters
467        for (id, offset) in uniform_bindings
468            .iter()
469            .filter_map(|(binding, value)| match binding {
470                UniformBinding::Parameter(id) => Some((id, value)),
471                _ => None,
472            })
473        {
474            let default = parameter_defaults.get(id).map_or(0f32, |f| f.initial);
475
476            let value = *runtime_parameters.get(id).unwrap_or(&default);
477
478            uniform_storage.bind_scalar(offset.offset(), value, offset.context(), device);
479        }
480
481        // bind luts
482        for (index, lut) in lookup_textures {
483            let lut = lut.as_ref();
484            if let Some(binding) = texture_meta.get(&TextureSemantics::User.semantics(index)) {
485                Self::bind_texture(descriptor_set, sampler_set, binding, lut, device);
486            }
487
488            if let Some(offset) =
489                uniform_bindings.get(&TextureSemantics::User.semantics(index).into())
490            {
491                uniform_storage.bind_vec4(offset.offset(), lut.size(), offset.context(), device);
492            }
493        }
494    }
495}
496
497#[derive(Debug)]
498pub struct BindingRequirements {
499    pub(crate) required_history: usize,
500    pub(crate) uses_final_pass_as_feedback: bool,
501}
502
503/// Trait for objects that can be used to create a binding map.
504pub trait BindingUtil {
505    /// Create the uniform binding map with the given reflection information.
506    fn create_binding_map<T>(
507        &self,
508        f: impl Fn(&dyn UniformMeta) -> T,
509    ) -> FastHashMap<UniformBinding, T>;
510
511    /// Calculate the number of required images for history.
512    fn calculate_requirements<'a>(pass_meta: impl Iterator<Item = &'a Self>) -> BindingRequirements
513    where
514        Self: 'a;
515}
516
517impl BindingUtil for BindingMeta {
518    fn create_binding_map<T>(
519        &self,
520        f: impl Fn(&dyn UniformMeta) -> T,
521    ) -> FastHashMap<UniformBinding, T> {
522        let mut uniform_bindings = FastHashMap::default();
523        for param in self.parameter_meta.values() {
524            uniform_bindings.insert(UniformBinding::Parameter(param.id.clone()), f(param));
525        }
526
527        for (semantics, param) in &self.unique_meta {
528            uniform_bindings.insert(UniformBinding::SemanticVariable(*semantics), f(param));
529        }
530
531        for (semantics, param) in &self.texture_size_meta {
532            uniform_bindings.insert(UniformBinding::TextureSize(*semantics), f(param));
533        }
534
535        uniform_bindings
536    }
537
538    fn calculate_requirements<'a>(pass_meta: impl Iterator<Item = &'a Self>) -> BindingRequirements
539    where
540        Self: 'a,
541    {
542        let mut required_images = 0;
543
544        let mut len: i64 = 0;
545        let mut latest_feedback_pass: i64 = -1;
546
547        for pass in pass_meta {
548            len += 1;
549
550            // If a shader uses history size, but not history, we still need to keep the texture.
551            let history_texture_max_index = pass
552                .texture_meta
553                .iter()
554                .filter(|(semantics, _)| semantics.semantics == TextureSemantics::OriginalHistory)
555                .map(|(semantic, _)| semantic.index)
556                .fold(0, std::cmp::max);
557            let history_texture_size_max_index = pass
558                .texture_size_meta
559                .iter()
560                .filter(|(semantics, _)| semantics.semantics == TextureSemantics::OriginalHistory)
561                .map(|(semantic, _)| semantic.index)
562                .fold(0, std::cmp::max);
563
564            let feedback_max_index = pass
565                .texture_meta
566                .iter()
567                .filter(|(semantics, _)| semantics.semantics == TextureSemantics::PassFeedback)
568                .map(|(semantic, _)| semantic.index as i64)
569                .fold(-1, std::cmp::max);
570            let feedback_max_size_index = pass
571                .texture_size_meta
572                .iter()
573                .filter(|(semantics, _)| semantics.semantics == TextureSemantics::PassFeedback)
574                .map(|(semantic, _)| semantic.index as i64)
575                .fold(-1, std::cmp::max);
576
577            latest_feedback_pass = std::cmp::max(latest_feedback_pass, feedback_max_index);
578            latest_feedback_pass = std::cmp::max(latest_feedback_pass, feedback_max_size_index);
579
580            required_images = std::cmp::max(required_images, history_texture_max_index);
581            required_images = std::cmp::max(required_images, history_texture_size_max_index);
582        }
583
584        let uses_feedback = if latest_feedback_pass.is_negative() {
585            false
586        } else {
587            // Technically = but we can be permissive here
588
589            // account for off by 1
590            latest_feedback_pass + 1 >= len
591        };
592
593        BindingRequirements {
594            required_history: required_images,
595            uses_final_pass_as_feedback: uses_feedback,
596        }
597    }
598}
599
600#[macro_export]
601macro_rules! impl_default_frame_options {
602    ($ty:ident) => {
603        /// Options for each frame.
604        #[repr(C)]
605        #[derive(Debug, Clone)]
606        pub struct $ty {
607            /// Whether or not to clear the history buffers.
608            pub clear_history: bool,
609            /// The direction of rendering.
610            /// -1 indicates that the frames are played in reverse order.
611            pub frame_direction: i32,
612            /// The rotation of the output. 0 = 0deg, 1 = 90deg, 2 = 180deg, 3 = 270deg.
613            pub rotation: u32,
614            /// The total number of subframes ran. Default is 1.
615            pub total_subframes: u32,
616            /// The current sub frame. Default is 1.
617            pub current_subframe: u32,
618            /// The expected aspect ratio of the source image.
619            ///
620            /// This can differ from the actual aspect ratio of the source
621            /// image.
622            ///
623            /// The default is 0 which will automatically infer the ratio from the source image.
624            pub aspect_ratio: f32,
625            /// The original frames per second of the source. Default is 1.
626            pub frames_per_second: f32,
627            /// Time in milliseconds between the current and previous frame. Default is 0.
628            pub frametime_delta: u32,
629            /// Target color space bound to the shader `HDRMode` uniform. Must
630            /// match the host swapchain color space. Default is
631            /// `ColorSpace::Sdr`.
632            pub color_space: $crate::__ColorSpace,
633            /// HDR SDR reference white in nits, bound to the shader `BrightnessNits` uniform.
634            /// Default is 200.0. Only meaningful when the chain's HDR mode is non-zero.
635            pub brightness_nits: f32,
636            /// Gamut expansion mode bound to the shader `ExpandGamut` uniform. Default is 0.
637            pub expand_gamut: u32,
638            /// Bound to the shader `Gyroscope` (vec3) uniform. Default is [0, 0, 0].
639            pub gyroscope: [f32; 3],
640            /// Bound to the shader `Accelerometer` (vec3) uniform. Default is [0, 0, 0].
641            pub accelerometer: [f32; 3],
642            /// Bound to the shader `AccelerometerRest` (vec3) uniform. Default is [0, 0, 0].
643            pub accelerometer_rest: [f32; 3],
644        }
645
646        impl Default for $ty {
647            fn default() -> Self {
648                Self {
649                    clear_history: false,
650                    frame_direction: 1,
651                    rotation: 0,
652                    total_subframes: 1,
653                    current_subframe: 1,
654                    aspect_ratio: 0.0,
655                    frametime_delta: 0,
656                    frames_per_second: 1.0,
657                    color_space: $crate::__ColorSpace::Sdr,
658                    brightness_nits: 200.0,
659                    expand_gamut: 0,
660                    gyroscope: [0.0, 0.0, 0.0],
661                    accelerometer: [0.0, 0.0, 0.0],
662                    accelerometer_rest: [0.0, 0.0, 0.0],
663                }
664            }
665        }
666    };
667}