1use self::spec::IO_APIC_VERSION;
10use self::spec::IOAPIC_DEVICE_MMIO_REGION_SIZE;
11use self::spec::IndexRegister;
12use self::spec::IoApicId;
13use self::spec::IoApicVersion;
14use self::spec::REDIRECTION_WRITE_MASK;
15use self::spec::RedirectionEntry;
16use crate::ioapic::spec::IOAPIC_DEVICE_MMIO_REGION_MASK;
17use crate::ioapic::spec::Register;
18use chipset_device::ChipsetDevice;
19use chipset_device::interrupt::HandleEoi;
20use chipset_device::interrupt::LineInterruptTarget;
21use chipset_device::io::IoError;
22use chipset_device::io::IoResult;
23use chipset_device::mmio::MmioIntercept;
24use chipset_resources::ioapic::IoApicRouting;
25use inspect::Inspect;
26use inspect::InspectMut;
27use inspect_counters::Counter;
28use std::fmt::Debug;
29use std::ops::RangeInclusive;
30use vmcore::device_state::ChangeDeviceState;
31use x86defs::apic::DeliveryMode;
32use x86defs::msi::MsiAddress;
33use x86defs::msi::MsiData;
34
35pub mod resolver;
36
37pub const IOAPIC_DEVICE_MMIO_REGION_BASE_ADDRESS: u64 =
38 acpi_spec::hyperv::IOAPIC_BASE_ADDRESS as u64;
39
40mod spec {
41 use bitfield_struct::bitfield;
42 use inspect::Inspect;
43 use open_enum::open_enum;
44
45 pub const IO_APIC_VERSION: u8 = 0x11;
47
48 open_enum! {
49 pub enum Register: u64 {
50 INDEX = 0,
51 DATA = 0x10,
52 }
53 }
54
55 open_enum! {
56 #[derive(Inspect)]
57 #[inspect(debug)]
58 pub enum IndexRegister: u8 {
59 ID = 0,
60 VERSION = 1,
61 ARBITRATION_ID = 2,
62 REDIRECTION_TABLE_START = 0x10,
63 }
64 }
65
66 pub const IOAPIC_DEVICE_MMIO_REGION_SIZE: u64 = 0x20;
67 pub const IOAPIC_DEVICE_MMIO_REGION_MASK: u64 = IOAPIC_DEVICE_MMIO_REGION_SIZE - 1;
68
69 #[derive(Inspect)]
70 #[bitfield(u64)]
71 #[rustfmt::skip]
72 pub struct RedirectionEntry {
73 #[bits(8)] pub vector: u8,
74 #[bits(3)] pub delivery_mode: u8,
75 #[bits(1)] pub destination_mode_logical: bool,
76 #[bits(1)] pub delivery_status: bool,
77 #[bits(1)] pub active_low: bool,
78 #[bits(1)] pub remote_irr: bool,
79 #[bits(1)] pub trigger_mode_level: bool,
80 #[bits(1)] pub masked: bool,
81 #[bits(15)] _unused: u32,
82 #[bits(16)] _unused2: u32,
83 #[bits(8)] pub extended_destination: u8,
84 #[bits(8)] pub destination: u8,
85 }
86
87 pub const REDIRECTION_WRITE_MASK: u64 = RedirectionEntry::new()
89 .with_vector(0xff)
90 .with_delivery_mode(0x7)
91 .with_destination_mode_logical(true)
92 .with_active_low(true)
93 .with_trigger_mode_level(true)
94 .with_masked(true)
95 .with_destination(0xff)
96 .with_extended_destination(0xff)
97 .0;
98
99 #[bitfield(u32)]
100 pub struct IoApicId {
101 #[bits(24)]
102 _reserved: u32,
103 #[bits(4)]
104 pub id: u8,
105 #[bits(4)]
106 _reserved2: u32,
107 }
108
109 #[bitfield(u32)]
110 pub struct IoApicVersion {
111 pub version: u8,
112 _reserved: u8,
113 pub max_entry: u8,
114 _reserved2: u8,
115 }
116}
117
118impl RedirectionEntry {
119 fn init() -> Self {
120 Self::new()
121 .with_destination_mode_logical(true)
122 .with_masked(true)
123 }
124
125 fn as_msi(&self) -> Option<(u64, u32)> {
127 if self.masked() {
128 return None;
129 }
130
131 let address = MsiAddress::new()
134 .with_address(x86defs::msi::MSI_ADDRESS)
135 .with_destination(self.destination())
136 .with_extended_destination(self.extended_destination())
137 .with_destination_mode_logical(self.destination_mode_logical())
138 .with_redirection_hint(self.delivery_mode() == DeliveryMode::LOWEST_PRIORITY.0);
139
140 let data = MsiData::new()
141 .with_assert(true)
142 .with_destination_mode_logical(self.destination_mode_logical())
143 .with_delivery_mode(self.delivery_mode())
144 .with_trigger_mode_level(self.trigger_mode_level())
145 .with_vector(self.vector());
146
147 Some((u32::from(address).into(), data.into()))
148 }
149}
150
151#[derive(Debug, Inspect)]
152struct IrqEntry {
153 #[inspect(flatten)]
154 redirection: RedirectionEntry,
155 line_level: bool,
156 #[inspect(skip)]
157 registered_request: Option<(u64, u32)>,
158}
159
160impl IrqEntry {
161 fn assert(&mut self, routing: &dyn IoApicRouting, stats: &mut IoApicStats, n: u8) {
162 let old_level = std::mem::replace(&mut self.line_level, true);
163 self.evaluate(routing, stats, n, !old_level);
164 }
165
166 fn deassert(&mut self) {
167 self.line_level = false;
168 }
171
172 fn eoi(&mut self, vector: u32, routing: &dyn IoApicRouting, stats: &mut IoApicStats, n: u8) {
173 if self.redirection.vector() as u32 == vector {
174 self.redirection.set_remote_irr(false);
177 self.evaluate(routing, stats, n, false);
178 }
179 }
180
181 fn evaluate(
182 &mut self,
183 routing: &dyn IoApicRouting,
184 stats: &mut IoApicStats,
185 n: u8,
186 edge: bool,
187 ) {
188 let is_level = self.redirection.trigger_mode_level()
190 && matches!(
191 DeliveryMode(self.redirection.delivery_mode()),
192 DeliveryMode::FIXED | DeliveryMode::LOWEST_PRIORITY
193 );
194
195 if self.redirection.masked()
198 || (is_level && (!self.line_level || self.redirection.remote_irr()))
199 || (!is_level && !edge)
200 {
201 return;
202 }
203
204 self.redirection.set_remote_irr(is_level);
206 stats.interrupts.increment();
207 stats.interrupts_per_irq[n as usize].increment();
208 routing.assert(n);
209 }
210}
211
212#[derive(Debug, InspectMut)]
213pub struct IoApicDevice {
214 #[inspect(skip)]
216 valid_lines: [RangeInclusive<u32>; 1],
217
218 #[inspect(skip)]
220 routing: Box<dyn IoApicRouting>,
221
222 stats: IoApicStats,
224
225 #[inspect(with = r#"|x| inspect::iter_by_index(x.iter())"#)]
227 irqs: Box<[IrqEntry]>,
228 id: u8,
229 index: IndexRegister,
230}
231
232#[derive(Debug, Inspect)]
233struct IoApicStats {
234 #[inspect(iter_by_index)]
235 interrupts_per_irq: Vec<Counter>,
236 interrupts: Counter,
237}
238
239impl IoApicDevice {
240 pub fn new(num_entries: u8, routing: Box<dyn IoApicRouting>) -> Self {
241 let irqs = (0..num_entries)
242 .map(|_| IrqEntry {
243 redirection: RedirectionEntry::init(),
244 line_level: false,
245 registered_request: None,
246 })
247 .collect();
248
249 IoApicDevice {
250 valid_lines: [0..=num_entries as u32 - 1],
251 routing,
252
253 id: 0,
254 irqs,
255 index: IndexRegister::ID,
256 stats: IoApicStats {
257 interrupts_per_irq: (0..num_entries).map(|_| Counter::new()).collect(),
258 interrupts: Counter::new(),
259 },
260 }
261 }
262
263 fn read_redirection_register(&self, index: u8) -> u32 {
264 let mut value = self
265 .irqs
266 .get(index as usize / 2)
267 .map_or(0, |irq| irq.redirection.into());
268 if index & 1 == 1 {
269 value >>= 32;
270 }
271 value as u32
272 }
273
274 fn write_redirection_register(&mut self, index: u8, val: u32) {
275 let n = index as usize / 2;
276 if let Some(irq) = self.irqs.get_mut(n) {
277 let mut val = val as u64;
278 let mut redirection = u64::from(irq.redirection);
279 if index & 1 == 1 {
280 redirection &= 0xffffffff;
281 val <<= 32;
282 } else {
283 redirection &= !0xffffffff;
284 }
285 redirection |= val & REDIRECTION_WRITE_MASK;
286 irq.redirection = redirection.into();
287
288 tracing::trace!(n, entry = ?irq.redirection, "new redirection entry");
289
290 let request = irq.redirection.as_msi();
291 if request != irq.registered_request {
292 self.routing.set_route(n as u8, request);
293 irq.registered_request = request;
294 }
295
296 irq.evaluate(self.routing.as_ref(), &mut self.stats, n as u8, false);
298 }
299 }
300
301 fn read_register(&self, index: IndexRegister) -> u32 {
302 match index {
303 IndexRegister::ID => IoApicId::new().with_id(self.id).into(),
304 IndexRegister::VERSION => IoApicVersion::new()
305 .with_version(IO_APIC_VERSION)
306 .with_max_entry((self.irqs.len() - 1) as u8)
307 .into(),
308 IndexRegister::ARBITRATION_ID => 0,
309 _ if self.index >= IndexRegister::REDIRECTION_TABLE_START => {
310 self.read_redirection_register(index.0 - IndexRegister::REDIRECTION_TABLE_START.0)
311 }
312 _ => {
313 tracelimit::warn_ratelimited!(?index, "unsupported register index read");
314 !0
315 }
316 }
317 }
318
319 fn write_register(&mut self, index: IndexRegister, val: u32) {
320 match index {
321 IndexRegister::ID => self.id = IoApicId::from(val).id(),
322 IndexRegister::VERSION | IndexRegister::ARBITRATION_ID => {
323 tracing::debug!(?index, val, "ignoring write to read-only register");
324 }
325 _ if self.index >= IndexRegister::REDIRECTION_TABLE_START => {
326 self.write_redirection_register(
327 index.0 - IndexRegister::REDIRECTION_TABLE_START.0,
328 val,
329 );
330 }
331 _ => {
332 tracelimit::warn_ratelimited!(?index, "unsupported register index write");
333 }
334 }
335 }
336}
337
338impl LineInterruptTarget for IoApicDevice {
339 fn set_irq(&mut self, n: u32, high: bool) {
340 if let Some(irq) = self.irqs.get_mut(n as usize) {
341 if high {
342 irq.assert(self.routing.as_ref(), &mut self.stats, n as u8);
343 } else {
344 irq.deassert();
345 }
346 }
347 }
348
349 fn valid_lines(&self) -> &[RangeInclusive<u32>] {
350 &self.valid_lines
351 }
352}
353
354impl HandleEoi for IoApicDevice {
355 fn handle_eoi(&mut self, irq_to_end: u32) {
357 for (index, irq) in self.irqs.iter_mut().enumerate() {
358 irq.eoi(
359 irq_to_end,
360 self.routing.as_ref(),
361 &mut self.stats,
362 index as u8,
363 );
364 }
365 }
366}
367
368impl ChangeDeviceState for IoApicDevice {
369 fn start(&mut self) {}
370
371 async fn stop(&mut self) {}
372
373 async fn reset(&mut self) {
374 let Self {
375 valid_lines: _,
376 routing: _,
377 irqs,
378 id,
379 index,
380 stats: _,
381 } = self;
382 *id = 0;
383 *index = IndexRegister::ID;
384 for (n, irq) in irqs.iter_mut().enumerate() {
385 irq.redirection = RedirectionEntry::init();
386 self.routing.set_route(n as u8, None);
387 irq.registered_request = None;
388 }
389 }
390}
391
392impl ChipsetDevice for IoApicDevice {
393 fn supports_mmio(&mut self) -> Option<&mut dyn MmioIntercept> {
394 Some(self)
395 }
396
397 fn supports_line_interrupt_target(&mut self) -> Option<&mut dyn LineInterruptTarget> {
398 Some(self)
399 }
400
401 fn supports_handle_eoi(&mut self) -> Option<&mut dyn HandleEoi> {
402 Some(self)
403 }
404}
405
406mod save_restore {
407 use super::IoApicDevice;
408 use super::spec::IndexRegister;
409 use super::spec::RedirectionEntry;
410 use thiserror::Error;
411 use vmcore::save_restore::RestoreError;
412 use vmcore::save_restore::SaveError;
413 use vmcore::save_restore::SaveRestore;
414
415 mod state {
416 use mesh::payload::Protobuf;
417 use vmcore::save_restore::SavedStateRoot;
418
419 #[derive(Clone, Debug, Default, Protobuf, SavedStateRoot)]
420 #[mesh(package = "chipset.ioapic")]
421 pub struct SavedState {
422 #[mesh(1)]
423 pub(super) id: u8,
424 #[mesh(2)]
425 pub(super) index: u8,
426 #[mesh(3)]
427 pub(super) redirection_entries: Vec<u64>,
428 #[mesh(4)]
430 pub(super) line_levels: Vec<bool>,
431 }
432 }
433
434 #[derive(Error, Debug)]
435 #[error("wrong number of redirection entries")]
436 struct WrongNumberOfRedirectionEntries;
437
438 #[derive(Error, Debug)]
439 #[error("wrong number of line levels")]
440 struct WrongNumberOfLineLevels;
441
442 impl SaveRestore for IoApicDevice {
443 type SavedState = state::SavedState;
444
445 fn save(&mut self) -> Result<state::SavedState, SaveError> {
446 let Self {
447 valid_lines: _,
448 routing: _,
449 irqs,
450 id,
451 index,
452 stats: _,
453 } = &self;
454
455 Ok(state::SavedState {
456 redirection_entries: irqs.iter().map(|irq| irq.redirection.into()).collect(),
457 line_levels: irqs.iter().map(|irq| irq.line_level).collect(),
458 index: index.0,
459 id: *id,
460 })
461 }
462
463 fn restore(&mut self, state: state::SavedState) -> Result<(), RestoreError> {
464 let state::SavedState {
465 redirection_entries,
466 line_levels,
467 id,
468 index,
469 } = state;
470 if redirection_entries.len() != self.irqs.len() {
471 return Err(RestoreError::Other(WrongNumberOfRedirectionEntries.into()));
472 }
473 let line_levels = if line_levels.is_empty() {
474 vec![false; self.irqs.len()]
475 } else if line_levels.len() == self.irqs.len() {
476 line_levels
477 } else {
478 return Err(RestoreError::Other(WrongNumberOfLineLevels.into()));
479 };
480 for (n, ((state, line_level), irq)) in redirection_entries
481 .into_iter()
482 .zip(line_levels)
483 .zip(self.irqs.iter_mut())
484 .enumerate()
485 {
486 irq.redirection = RedirectionEntry::from(state);
487 irq.line_level = line_level;
488 let request = irq.redirection.as_msi();
489 self.routing.set_route(n as u8, request);
490 irq.registered_request = request;
491 }
492 self.id = id;
493 self.index = IndexRegister(index);
494 Ok(())
495 }
496 }
497
498 #[cfg(test)]
499 mod tests {
500 use super::*;
501 use chipset_device::interrupt::HandleEoi;
502 use chipset_device::interrupt::LineInterruptTarget;
503 use chipset_resources::ioapic::IoApicRouting;
504 use parking_lot::Mutex;
505 use std::sync::Arc;
506 use test_with_tracing::test;
507
508 const ENTRY_COUNT: u8 = 2;
509 const IRQ: u8 = 1;
510 const VECTOR: u8 = 0x30;
511
512 #[derive(Clone, Default)]
513 struct RecordingRouting {
514 assertions: Arc<Mutex<Vec<u8>>>,
515 }
516
517 impl IoApicRouting for RecordingRouting {
518 fn assert(&self, irq: u8) {
519 self.assertions.lock().push(irq);
520 }
521
522 fn set_route(&self, _irq: u8, _request: Option<(u64, u32)>) {}
523 }
524
525 fn device() -> (IoApicDevice, RecordingRouting) {
526 let routing = RecordingRouting::default();
527 (
528 IoApicDevice::new(ENTRY_COUNT, Box::new(routing.clone())),
529 routing,
530 )
531 }
532
533 fn set_entry(device: &mut IoApicDevice, level: bool, masked: bool) {
534 let entry = RedirectionEntry::new()
535 .with_vector(VECTOR)
536 .with_trigger_mode_level(level)
537 .with_masked(masked);
538 device.write_redirection_register(IRQ * 2, u64::from(entry) as u32);
539 }
540
541 #[test]
542 fn restore_preserves_edge_history() {
543 let (mut source, _) = device();
544 set_entry(&mut source, false, false);
545 source.set_irq(IRQ.into(), true);
546 let state = source.save().expect("save succeeds");
547
548 let (mut restored, routing) = device();
549 restored.restore(state).expect("restore succeeds");
550 restored.set_irq(IRQ.into(), true);
551 assert!(routing.assertions.lock().is_empty());
552
553 restored.set_irq(IRQ.into(), false);
554 restored.set_irq(IRQ.into(), true);
555 assert_eq!(*routing.assertions.lock(), [IRQ]);
556 }
557
558 #[test]
559 fn restore_allows_new_rising_edge() {
560 let (mut source, _) = device();
561 set_entry(&mut source, false, false);
562 let state = source.save().expect("save succeeds");
563
564 let (mut restored, routing) = device();
565 set_entry(&mut restored, false, false);
566 restored.set_irq(IRQ.into(), true);
567 routing.assertions.lock().clear();
568
569 restored.restore(state).expect("restore succeeds");
570 assert!(routing.assertions.lock().is_empty());
571 restored.set_irq(IRQ.into(), true);
572 assert_eq!(*routing.assertions.lock(), [IRQ]);
573 }
574
575 #[test]
576 fn restore_preserves_asserted_level_until_unmasked() {
577 let (mut source, _) = device();
578 set_entry(&mut source, true, true);
579 source.set_irq(IRQ.into(), true);
580 let state = source.save().expect("save succeeds");
581
582 let (mut restored, routing) = device();
583 restored.restore(state).expect("restore succeeds");
584 assert!(routing.assertions.lock().is_empty());
585 set_entry(&mut restored, true, false);
586
587 assert_eq!(*routing.assertions.lock(), [IRQ]);
588 }
589
590 #[test]
591 fn restore_preserves_remote_irr_until_eoi() {
592 let (mut source, _) = device();
593 set_entry(&mut source, true, false);
594 source.set_irq(IRQ.into(), true);
595 let state = source.save().expect("save succeeds");
596
597 let (mut restored, routing) = device();
598 restored.restore(state).expect("restore succeeds");
599 restored.set_irq(IRQ.into(), true);
600 assert!(routing.assertions.lock().is_empty());
601
602 restored.handle_eoi(VECTOR.into());
603 assert_eq!(*routing.assertions.lock(), [IRQ]);
604 restored.set_irq(IRQ.into(), false);
605 restored.handle_eoi(VECTOR.into());
606 assert_eq!(*routing.assertions.lock(), [IRQ]);
607 }
608
609 #[test]
610 fn restore_does_not_evaluate_level_interrupts() {
611 let (mut source, _) = device();
612 set_entry(&mut source, true, true);
613 source.set_irq(IRQ.into(), true);
614 let mut state = source.save().expect("save succeeds");
615 state.redirection_entries[IRQ as usize] =
616 RedirectionEntry::from(state.redirection_entries[IRQ as usize])
617 .with_masked(false)
618 .into();
619
620 let (mut restored, routing) = device();
621 restored.restore(state).expect("restore succeeds");
622 assert!(routing.assertions.lock().is_empty());
623
624 restored.set_irq(IRQ.into(), true);
625 assert_eq!(*routing.assertions.lock(), [IRQ]);
626 }
627
628 #[test]
629 fn restore_defaults_absent_line_levels_to_deasserted() {
630 let (mut source, _) = device();
631 set_entry(&mut source, true, true);
632 source.set_irq(IRQ.into(), true);
633 let mut state = source.save().expect("save succeeds");
634 state.line_levels.clear();
635
636 let (mut restored, routing) = device();
637 restored.restore(state).expect("restore succeeds");
638 set_entry(&mut restored, true, false);
639
640 assert!(routing.assertions.lock().is_empty());
641 }
642
643 #[test]
644 fn restore_rejects_wrong_number_of_line_levels() {
645 let (mut source, _) = device();
646 let mut state = source.save().expect("save succeeds");
647 state.line_levels.pop();
648
649 let (mut restored, routing) = device();
650 set_entry(&mut restored, false, false);
651 let before = restored.save().expect("save succeeds");
652 let error = restored.restore(state).expect_err("restore must fail");
653
654 assert_eq!(error.to_string(), "wrong number of line levels");
655 let after = restored.save().expect("save succeeds");
656 assert_eq!(after.redirection_entries, before.redirection_entries);
657 assert_eq!(after.line_levels, before.line_levels);
658 assert!(routing.assertions.lock().is_empty());
659 }
660 }
661}
662
663impl MmioIntercept for IoApicDevice {
664 fn mmio_read(&mut self, address: u64, data: &mut [u8]) -> IoResult {
665 assert_eq!(
666 address & !IOAPIC_DEVICE_MMIO_REGION_MASK,
667 IOAPIC_DEVICE_MMIO_REGION_BASE_ADDRESS
668 );
669
670 let v = match Register(address & IOAPIC_DEVICE_MMIO_REGION_MASK) {
671 Register::INDEX => self.index.0.into(),
672 Register::DATA => self.read_register(self.index),
673 _ => return IoResult::Err(IoError::InvalidRegister),
674 };
675
676 let n = data.len().min(size_of_val(&v));
678 data.copy_from_slice(&v.to_ne_bytes()[..n]);
679 IoResult::Ok
680 }
681
682 fn mmio_write(&mut self, address: u64, data: &[u8]) -> IoResult {
683 assert_eq!(
684 address & !IOAPIC_DEVICE_MMIO_REGION_MASK,
685 IOAPIC_DEVICE_MMIO_REGION_BASE_ADDRESS
686 );
687
688 match Register(address & IOAPIC_DEVICE_MMIO_REGION_MASK) {
689 Register::INDEX => self.index = IndexRegister(data[0]),
690 Register::DATA => {
691 let Ok(data) = data.try_into() else {
693 return IoResult::Err(IoError::InvalidAccessSize);
694 };
695 let data = u32::from_ne_bytes(data);
696 self.write_register(self.index, data);
697 }
698 _ => return IoResult::Err(IoError::InvalidRegister),
699 }
700
701 IoResult::Ok
702 }
703
704 fn get_static_regions(&mut self) -> &[(&str, RangeInclusive<u64>)] {
705 &[(
706 "mmio",
707 IOAPIC_DEVICE_MMIO_REGION_BASE_ADDRESS
708 ..=IOAPIC_DEVICE_MMIO_REGION_BASE_ADDRESS + IOAPIC_DEVICE_MMIO_REGION_SIZE - 1,
709 )]
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 #[test]
723 fn remappable_rte_emits_remappable_msi() {
724 let index: u16 = 0x9234;
728 let vector = 0x30u8;
729 let bits = u64::from(vector)
730 | (1 << 48)
731 | ((u64::from(index) & 0x7fff) << 49)
732 | (((u64::from(index) >> 15) & 1) << 11);
733 let rte = RedirectionEntry::from(bits);
734
735 let (address, data) = rte.as_msi().expect("unmasked entry yields an MSI");
736
737 assert_eq!((address >> 4) & 1, 1, "remappable format bit set");
740 assert_eq!((address >> 3) & 1, 0, "SHV clear (no subhandle)");
741 let handle = (((address >> 2) & 1) << 15) | ((address >> 5) & 0x7fff);
742 assert_eq!(handle as u16, index, "handle carries the IRTE index");
743 assert_eq!(data as u8, vector, "vector preserved for EOI matching");
744 }
745
746 #[test]
749 fn compat_rte_emits_compat_msi() {
750 let rte = RedirectionEntry::new()
751 .with_vector(0x30)
752 .with_destination(0x04);
753 let (address, _data) = rte.as_msi().expect("unmasked entry yields an MSI");
754 assert_eq!(
755 (address >> 4) & 1,
756 0,
757 "compatibility format (not remappable)"
758 );
759 }
760
761 #[test]
763 fn masked_rte_emits_nothing() {
764 let rte = RedirectionEntry::new().with_masked(true).with_vector(0x30);
765 assert_eq!(rte.as_msi(), None);
766 }
767}