1use std::sync::Arc;
2use std::time::Duration;
3
4use web_time::Instant;
5
6use crate::diagnostic::DiagnosticHook;
7use crate::{
8 Capabilities, CapabilityTier, GpuDiagnostic, GpuDiagnosticHandler, GpuError, IntermediateFormat,
9};
10
11#[derive(Clone, Debug)]
24pub struct GpuContext {
25 adapter: wgpu::Adapter,
26 device: wgpu::Device,
27 queue: Arc<LockedQueue>,
28 capabilities: Arc<Capabilities>,
30 diagnostics: Arc<DiagnosticHook>,
31 rate: Arc<crate::rate::GpuRate>,
32}
33
34#[derive(Debug)]
36struct LockedQueue {
37 queue: wgpu::Queue,
39 lock: QueueLockSlot,
40}
41
42#[derive(Debug)]
44struct QueueLockSlot {
45 lock: Option<Arc<dyn ExternalQueueLock>>,
46 held: bool,
47}
48
49impl LockedQueue {
50 #[cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
53 fn shared(queue: wgpu::Queue, lock: Option<Arc<dyn ExternalQueueLock>>) -> Arc<Self> {
54 Arc::new(Self {
55 queue,
56 lock: QueueLockSlot { lock, held: false },
57 })
58 }
59
60 fn locked<T>(&self, call: impl FnOnce(&wgpu::Queue) -> T) -> T {
62 let guard = self.lock.lock.as_ref().map(|lock| {
63 lock.lock();
64 QueueGuard(lock.as_ref())
65 });
66 let result = call(&self.queue);
67 drop(guard);
68 result
69 }
70}
71
72impl Drop for LockedQueue {
73 fn drop(&mut self) {
74 if let Some(lock) = &self.lock.lock {
77 lock.lock();
78 self.lock.held = true;
79 }
80 }
81}
82
83impl Drop for QueueLockSlot {
84 fn drop(&mut self) {
85 if self.held
86 && let Some(lock) = &self.lock
87 {
88 lock.unlock();
89 }
90 }
91}
92
93pub trait ExternalQueueLock: std::fmt::Debug + Send + Sync + 'static {
99 fn lock(&self);
101 fn unlock(&self);
103}
104
105#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
107#[non_exhaustive]
108#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
109pub struct SubmitTimings {
110 pub queue_lock_wait: Duration,
112 pub queue_submit: Duration,
114}
115
116struct QueueGuard<'a>(&'a dyn ExternalQueueLock);
117
118impl Drop for QueueGuard<'_> {
119 fn drop(&mut self) {
120 self.0.unlock();
121 }
122}
123
124impl GpuContext {
125 #[must_use]
130 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
131 pub fn from_parts(adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue) -> Self {
132 Self::from_parts_with_queue_lock(adapter, device, queue, None)
133 }
134
135 #[must_use]
140 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
141 pub fn from_parts_with_queue_lock(
142 adapter: wgpu::Adapter,
143 device: wgpu::Device,
144 queue: wgpu::Queue,
145 queue_lock: Option<Arc<dyn ExternalQueueLock>>,
146 ) -> Self {
147 let capabilities = Arc::new(Capabilities::from_device(&adapter, &device));
148 crate::validation_log::watch_device(&device, &capabilities.summary());
149 let diagnostics = Arc::new(DiagnosticHook::new());
150 install_device_callbacks(&device, &diagnostics);
151 Self {
152 adapter,
153 device,
154 queue: LockedQueue::shared(queue, queue_lock),
155 capabilities,
156 diagnostics,
157 rate: Arc::default(),
158 }
159 }
160
161 #[must_use]
163 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
164 pub const fn device(&self) -> &wgpu::Device {
165 &self.device
166 }
167
168 #[must_use]
171 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
172 pub fn queue(&self) -> &wgpu::Queue {
173 &self.queue.queue
174 }
175
176 pub fn on_submitted_work_done(&self, callback: impl FnOnce() + Send + 'static) {
180 #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
181 self.queue
182 .locked(|queue| queue.on_submitted_work_done(callback));
183 }
184
185 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
188 pub fn map_async(
189 &self,
190 slice: &wgpu::BufferSlice<'_>,
191 mode: wgpu::MapMode,
192 callback: impl FnOnce(Result<(), wgpu::BufferAsyncError>) + wgpu::WasmNotSend + 'static,
193 ) {
194 #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
195 self.queue.locked(|_| slice.map_async(mode, callback));
196 }
197
198 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
200 pub fn present(&self, texture: wgpu::SurfaceTexture) {
201 #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
202 self.queue.locked(|queue| queue.present(texture));
203 }
204
205 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
208 pub fn submit<I>(&self, command_buffers: I) -> wgpu::SubmissionIndex
209 where
210 I: IntoIterator<Item = wgpu::CommandBuffer>,
211 {
212 self.submit_measured(command_buffers, false).0
213 }
214
215 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
220 pub fn submit_timed<I>(&self, command_buffers: I) -> (wgpu::SubmissionIndex, SubmitTimings)
221 where
222 I: IntoIterator<Item = wgpu::CommandBuffer>,
223 {
224 self.submit_measured(command_buffers, true)
225 }
226
227 fn submit_measured<I>(
228 &self,
229 command_buffers: I,
230 measure: bool,
231 ) -> (wgpu::SubmissionIndex, SubmitTimings)
232 where
233 I: IntoIterator<Item = wgpu::CommandBuffer>,
234 {
235 let mut timings = SubmitTimings::default();
236 let started = measure.then(Instant::now);
237 let index = self.queue.locked(|queue| {
238 let acquired = started.map(|started| {
239 let now = Instant::now();
240 timings.queue_lock_wait = now.duration_since(started);
241 now
242 });
243 #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
244 let index = queue.submit(command_buffers);
245 if let Some(acquired) = acquired {
246 timings.queue_submit = acquired.elapsed();
247 }
248 index
249 });
250 (index, timings)
251 }
252
253 pub async fn request() -> Result<Self, GpuError> {
258 let _turn = crate::serialize_acquisition();
259 let instance = crate::new_instance(wgpu::Backends::all());
260 Self::request_compatible(&instance, None).await
261 }
262
263 #[cfg(not(target_arch = "wasm32"))]
268 pub async fn request_product() -> Result<Self, GpuError> {
269 Self::request_product_with(&ProductRequest::default()).await
270 }
271
272 #[cfg(not(target_arch = "wasm32"))]
282 pub async fn request_product_with(request: &ProductRequest) -> Result<Self, GpuError> {
283 let _turn = crate::serialize_acquisition();
284 let mut tried = ProductAttempt::default();
285 for adapter in product_candidates(request).await? {
286 match Self::open_device(adapter.adapter, request.withheld).await {
287 Ok(context) => return Ok(context),
288 Err(error) => tried.refused(adapter.name, error),
289 }
290 }
291 Err(tried.into_error(request))
292 }
293
294 #[cfg(not(target_arch = "wasm32"))]
303 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
304 pub async fn request_product_accepted<T, E>(
305 mut accept: impl FnMut(&Self) -> Result<T, E>,
306 ) -> Result<AcceptedDevice<T, E>, GpuError> {
307 let _turn = crate::serialize_acquisition();
308 let request = ProductRequest::default();
309 let mut tried = ProductAttempt::default();
310 let mut refusals = AdapterRefusals(Vec::new());
311 let mut first: Option<Self> = None;
312 for candidate in product_candidates(&request).await? {
313 if first.is_some() && candidate.cpu && !candidate.pinned {
314 continue;
315 }
316 let context = match Self::open_device(candidate.adapter, request.withheld).await {
317 Ok(context) => context,
318 Err(error) => {
319 tried.refused(candidate.name, error);
320 continue;
321 }
322 };
323 match accept(&context) {
324 Ok(value) => {
325 return Ok(AcceptedDevice {
326 context,
327 accepted: Some(value),
328 refusals,
329 });
330 }
331 Err(error) => {
332 refusals.0.push((candidate.name, error));
333 first.get_or_insert(context);
334 }
335 }
336 }
337 match first {
338 Some(context) => Ok(AcceptedDevice {
339 context,
340 accepted: None,
341 refusals,
342 }),
343 None => Err(tried.into_error(&request)),
344 }
345 }
346
347 #[cfg(not(target_arch = "wasm32"))]
350 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
351 pub async fn adapters_by_preference_on(
352 backends: wgpu::Backends,
353 ) -> Vec<(usize, wgpu::Adapter)> {
354 let _turn = crate::serialize_acquisition();
355 let instance = crate::new_instance(backends);
356 let mut adapters: Vec<Option<wgpu::Adapter>> = instance
357 .enumerate_adapters(wgpu::Backends::all())
358 .await
359 .into_iter()
360 .map(Some)
361 .collect();
362 let kinds: Vec<(usize, wgpu::DeviceType)> = adapters
363 .iter()
364 .enumerate()
365 .map(|(index, adapter)| {
366 let kind = adapter.as_ref().map_or(wgpu::DeviceType::Other, |adapter| {
367 adapter.get_info().device_type
368 });
369 (index, kind)
370 })
371 .collect();
372 crate::adapter_pin::product_order(&kinds)
373 .into_iter()
374 .filter_map(|index| adapters[index].take().map(|adapter| (index, adapter)))
375 .collect()
376 }
377
378 #[cfg(not(target_arch = "wasm32"))]
383 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
384 pub async fn open_adapter(adapter: wgpu::Adapter) -> Result<Self, GpuError> {
385 Self::open_adapter_without(adapter, wgpu::Features::empty()).await
386 }
387
388 #[cfg(not(target_arch = "wasm32"))]
393 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
394 pub async fn open_adapter_without(
395 adapter: wgpu::Adapter,
396 withheld: wgpu::Features,
397 ) -> Result<Self, GpuError> {
398 let _turn = crate::serialize_acquisition();
399 Self::open_device(adapter, withheld).await
400 }
401
402 #[must_use]
404 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
405 pub const fn adapter(&self) -> &wgpu::Adapter {
406 &self.adapter
407 }
408
409 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
417 pub async fn request_compatible(
418 instance: &wgpu::Instance,
419 compatible_surface: Option<&wgpu::Surface<'_>>,
420 ) -> Result<Self, GpuError> {
421 let preference = wgpu::PowerPreference::from_env().unwrap_or_default();
422 Self::request_compatible_with_preference(
423 instance,
424 compatible_surface,
425 preference,
426 wgpu::Features::empty(),
427 )
428 .await
429 }
430
431 async fn request_compatible_with_preference(
436 instance: &wgpu::Instance,
437 compatible_surface: Option<&wgpu::Surface<'_>>,
438 power_preference: wgpu::PowerPreference,
439 withheld: wgpu::Features,
440 ) -> Result<Self, GpuError> {
441 let _turn = crate::serialize_acquisition();
442 let adapter = instance
443 .request_adapter(&wgpu::RequestAdapterOptions {
444 compatible_surface,
445 power_preference,
446 ..Default::default()
447 })
448 .await
449 .map_err(GpuError::adapter)?;
450 #[cfg(not(target_arch = "wasm32"))]
451 let preferred = adapter.get_info();
452 let error = match Self::open_device(adapter, withheld).await {
453 Ok(context) => return Ok(context),
454 Err(error) => error,
455 };
456 #[cfg(not(target_arch = "wasm32"))]
457 {
458 let mut others: Vec<wgpu::Adapter> = instance
459 .enumerate_adapters(wgpu::Backends::all())
460 .await
461 .into_iter()
462 .filter(|other| other.get_info() != preferred)
463 .filter(|other| {
464 compatible_surface.is_none_or(|surface| other.is_surface_supported(surface))
465 })
466 .collect();
467 others
468 .sort_by_key(|other| fallback_rank(other.get_info().device_type, power_preference));
469 for other in others {
470 if let Ok(context) = Self::open_device(other, withheld).await {
471 return Ok(context);
472 }
473 }
474 }
475 Err(error)
476 }
477
478 async fn open_device(
479 adapter: wgpu::Adapter,
480 withheld: wgpu::Features,
481 ) -> Result<Self, GpuError> {
482 let optional_features = wgpu::Features::TIMESTAMP_QUERY
485 | wgpu::Features::TEXTURE_FORMAT_16BIT_NORM
486 | if cfg!(target_os = "windows") {
487 wgpu::Features::TEXTURE_FORMAT_NV12 | wgpu::Features::TEXTURE_FORMAT_P010
488 } else {
489 wgpu::Features::empty()
490 };
491 let required_features = adapter.features() & optional_features & !withheld;
492 let required_limits =
493 wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits());
494 let (device, queue) = adapter
495 .request_device(&wgpu::DeviceDescriptor {
496 required_features,
497 required_limits,
498 ..wgpu::DeviceDescriptor::default()
499 })
500 .await
501 .map_err(GpuError::device)?;
502 let capabilities = Arc::new(Capabilities::from_device(&adapter, &device));
503 crate::validation_log::watch_device(&device, &capabilities.summary());
504 let diagnostics = Arc::new(DiagnosticHook::new());
505 install_device_callbacks(&device, &diagnostics);
506 Ok(Self {
507 adapter,
508 device,
509 queue: LockedQueue::shared(queue, None),
510 capabilities,
511 diagnostics,
512 rate: Arc::default(),
513 })
514 }
515
516 pub fn set_diagnostic_handler(&self, handler: impl Fn(GpuDiagnostic) + Send + Sync + 'static) {
522 let handler: GpuDiagnosticHandler = Arc::new(handler);
523 self.diagnostics.set(Some(handler));
524 }
525
526 pub fn clear_diagnostic_handler(&self) {
528 self.diagnostics.set(None);
529 }
530
531 #[must_use]
533 pub fn capabilities(&self) -> &Capabilities {
534 &self.capabilities
535 }
536
537 #[must_use]
539 pub fn rate(&self) -> &Arc<crate::rate::GpuRate> {
540 &self.rate
541 }
542
543 #[must_use]
546 pub fn with_rgba8_intermediate(&self) -> Self {
547 let mut capabilities = Capabilities::clone(&self.capabilities);
548 capabilities.intermediate_format = IntermediateFormat::Rgba8Unorm;
549 Self {
550 capabilities: Arc::new(capabilities),
551 ..self.clone()
552 }
553 }
554
555 #[must_use]
558 pub fn with_tier(&self, tier: CapabilityTier) -> Self {
559 let mut capabilities = Capabilities::clone(&self.capabilities);
560 capabilities.tier = tier;
561 Self {
562 capabilities: Arc::new(capabilities),
563 ..self.clone()
564 }
565 }
566
567 #[must_use]
572 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
573 pub fn surface_config(
574 &self,
575 surface: &wgpu::Surface<'_>,
576 width: u32,
577 height: u32,
578 ) -> Option<wgpu::SurfaceConfiguration> {
579 let mut config = surface.get_default_config(&self.adapter, width, height)?;
580 if let Some(format) = surface
581 .get_capabilities(&self.adapter)
582 .formats
583 .into_iter()
584 .find(|format| !format.is_srgb())
585 {
586 config.format = format;
587 }
588 Some(config)
589 }
590}
591
592#[derive(Debug)]
594#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
595pub struct AcceptedDevice<T, E> {
596 pub context: GpuContext,
598 pub accepted: Option<T>,
600 pub refusals: AdapterRefusals<E>,
602}
603
604#[derive(Debug)]
606#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
607pub struct AdapterRefusals<E>(pub Vec<(String, E)>);
608
609impl<E: std::fmt::Display> std::fmt::Display for AdapterRefusals<E> {
610 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611 for (index, (adapter, error)) in self.0.iter().enumerate() {
612 if index > 0 {
613 formatter.write_str("; ")?;
614 }
615 write!(formatter, "{adapter}: {error}")?;
616 }
617 Ok(())
618 }
619}
620
621impl<E: std::fmt::Display + std::fmt::Debug> std::error::Error for AdapterRefusals<E> {}
622
623#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
626pub enum SoftwareAdapters {
627 #[default]
629 Allow,
630 Refuse,
632}
633
634#[derive(Clone, Debug, Default)]
636pub struct ProductRequest {
637 pub software: SoftwareAdapters,
639 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
641 pub withheld: wgpu::Features,
642}
643
644#[must_use]
647pub fn product_backends() -> Vec<wgpu::Backends> {
648 if let Some(backends) = wgpu::Backends::from_env() {
649 return vec![backends];
650 }
651 if cfg!(target_arch = "wasm32") {
652 vec![wgpu::Backends::all()]
653 } else if cfg!(target_os = "windows") {
654 vec![wgpu::Backends::DX12]
655 } else if cfg!(any(target_os = "macos", target_os = "ios")) {
656 vec![wgpu::Backends::METAL]
657 } else {
658 vec![wgpu::Backends::VULKAN, wgpu::Backends::GL]
659 }
660}
661
662#[cfg(not(target_arch = "wasm32"))]
664struct Candidate {
665 adapter: wgpu::Adapter,
666 name: String,
667 cpu: bool,
668 pinned: bool,
669}
670
671#[cfg(not(target_arch = "wasm32"))]
673#[derive(Default)]
674struct ProductAttempt {
675 refusals: Vec<(String, String)>,
676 first: Option<GpuError>,
677}
678
679#[cfg(not(target_arch = "wasm32"))]
680impl ProductAttempt {
681 fn refused(&mut self, adapter: String, error: GpuError) {
682 self.refusals.push((adapter, error.to_string()));
683 self.first.get_or_insert(error);
684 }
685
686 fn into_error(self, request: &ProductRequest) -> GpuError {
687 match (request.software, self.first) {
688 (SoftwareAdapters::Allow, Some(first)) => first,
689 _ => GpuError::no_hardware(Vec::new(), self.refusals),
690 }
691 }
692}
693
694#[cfg(not(target_arch = "wasm32"))]
696async fn product_candidates(request: &ProductRequest) -> Result<Vec<Candidate>, GpuError> {
697 let pin = crate::AdapterPolicy::from_env();
698 let mut listed = Vec::new();
699 let mut software = Vec::new();
700 let mut refusals = Vec::new();
701 for group in product_backends() {
702 let instance = crate::new_instance(group);
703 let adapters = instance.enumerate_adapters(group).await;
704 if group == wgpu::Backends::DX12 && adapters.is_empty() {
705 return Err(GpuError::no_dx12_adapter());
706 }
707 let kinds: Vec<(usize, wgpu::DeviceType)> = adapters
708 .iter()
709 .enumerate()
710 .map(|(index, adapter)| (index, adapter.get_info().device_type))
711 .collect();
712 let mut adapters: Vec<Option<wgpu::Adapter>> = adapters.into_iter().map(Some).collect();
713 let mut candidates = Vec::new();
714 for index in crate::adapter_pin::product_order(&kinds) {
715 let Some(adapter) = adapters[index].take() else {
716 continue;
717 };
718 let info = adapter.get_info();
719 listed.push(format!("[{index}] {} ({:?})", info.name, info.backend));
720 let pinned = pin.names(index, &info.name);
721 if pin.is_pin() && !pinned {
722 continue;
723 }
724 let cpu = crate::software_adapter(&info);
725 if cpu && request.software == SoftwareAdapters::Refuse && !pinned {
726 software.push(info.name);
727 continue;
728 }
729 if info.backend == wgpu::Backend::Gl
730 && let Err(reason) = crate::adapter_pin::gl_version_supported(&info.driver_info)
731 {
732 refusals.push((info.name.clone(), reason));
733 continue;
734 }
735 candidates.push(Candidate {
736 adapter,
737 name: info.name,
738 cpu,
739 pinned,
740 });
741 }
742 if !candidates.is_empty() {
743 return Ok(candidates);
744 }
745 }
746 if pin.is_pin() {
747 return Err(GpuError::pin(format!(
748 "{} names no adapter; the adapters are {}",
749 pin.setting(),
750 if listed.is_empty() {
751 "none".to_owned()
752 } else {
753 listed.join(", ")
754 }
755 )));
756 }
757 Err(GpuError::no_hardware(software, refusals))
758}
759
760#[cfg(not(target_arch = "wasm32"))]
762fn fallback_rank(device_type: wgpu::DeviceType, preference: wgpu::PowerPreference) -> u8 {
763 use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
764 let order: &[wgpu::DeviceType] = match preference {
765 wgpu::PowerPreference::HighPerformance => {
766 &[DiscreteGpu, IntegratedGpu, VirtualGpu, Other, Cpu]
767 }
768 wgpu::PowerPreference::LowPower => &[IntegratedGpu, DiscreteGpu, VirtualGpu, Other, Cpu],
769 wgpu::PowerPreference::None => return 0,
770 };
771 order
772 .iter()
773 .position(|kind| *kind == device_type)
774 .and_then(|rank| u8::try_from(rank).ok())
775 .unwrap_or(u8::MAX)
776}
777
778#[cfg(not(target_arch = "wasm32"))]
780fn install_device_callbacks(device: &wgpu::Device, diagnostics: &Arc<DiagnosticHook>) {
781 let uncaptured = Arc::clone(diagnostics);
782 device.on_uncaptured_error(Arc::new(move |error| {
783 uncaptured.emit(GpuDiagnostic::uncaptured(&error));
784 }));
785 let lost = Arc::clone(diagnostics);
786 device.set_device_lost_callback(move |reason, message| {
787 lost.emit(GpuDiagnostic::DeviceLost {
788 reason: format!("{reason:?}"),
789 message,
790 });
791 });
792}
793
794#[cfg(target_arch = "wasm32")]
795fn install_device_callbacks(_device: &wgpu::Device, _diagnostics: &Arc<DiagnosticHook>) {}
796
797#[cfg(all(test, not(target_arch = "wasm32")))]
798mod tests {
799 use super::fallback_rank;
800 use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
801 use wgpu::PowerPreference;
802
803 #[test]
804 fn fallback_adapters_follow_the_preference_and_a_cpu_comes_last() {
805 let ordered = |preference| {
806 let mut kinds = vec![Cpu, Other, VirtualGpu, IntegratedGpu, DiscreteGpu];
807 kinds.sort_by_key(|kind| fallback_rank(*kind, preference));
808 kinds
809 };
810 assert_eq!(
811 ordered(PowerPreference::HighPerformance),
812 [DiscreteGpu, IntegratedGpu, VirtualGpu, Other, Cpu]
813 );
814 assert_eq!(
815 ordered(PowerPreference::LowPower),
816 [IntegratedGpu, DiscreteGpu, VirtualGpu, Other, Cpu]
817 );
818 assert_eq!(
819 ordered(PowerPreference::None),
820 [Cpu, Other, VirtualGpu, IntegratedGpu, DiscreteGpu]
821 );
822 }
823}