// Copyright (c) 2020 Huawei Technologies Co.,Ltd. All rights reserved. // // StratoVirt is licensed under Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan // PSL v2. // You may obtain a copy of Mulan PSL v2 at: // http://license.coscl.org.cn/MulanPSL2 // THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY // KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. //! # Cpu //! //! This mod is to initialize vcpus to assigned state and drive them to run. //! //! ## Design //! //! This module offers support for: //! 1. Create vcpu. //! 2. According configuration, initialize vcpu registers and run. //! 3. Handle vcpu VmIn/VmOut events. //! 4. Handle vcpu lifecycle. //! //! ## Platform Support //! //! - `x86_64` //! - `aarch64` pub mod error; #[allow(clippy::upper_case_acronyms)] #[cfg(target_arch = "aarch64")] mod aarch64; #[cfg(target_arch = "x86_64")] mod x86_64; #[cfg(target_arch = "aarch64")] pub use aarch64::ArmCPUBootConfig as CPUBootConfig; #[cfg(target_arch = "aarch64")] pub use aarch64::ArmCPUFeatures as CPUFeatures; #[cfg(target_arch = "aarch64")] pub use aarch64::ArmCPUState as ArchCPU; #[cfg(target_arch = "aarch64")] pub use aarch64::ArmCPUTopology as CPUTopology; #[cfg(target_arch = "aarch64")] pub use aarch64::ArmRegsIndex as RegsIndex; #[cfg(target_arch = "aarch64")] pub use aarch64::CpregListEntry; #[cfg(target_arch = "aarch64")] pub use aarch64::PMU_INTR; #[cfg(target_arch = "aarch64")] pub use aarch64::PPI_BASE; pub use error::CpuError; #[cfg(target_arch = "x86_64")] pub use x86_64::X86CPUBootConfig as CPUBootConfig; #[cfg(target_arch = "x86_64")] pub use x86_64::X86CPUState as ArchCPU; #[cfg(target_arch = "x86_64")] pub use x86_64::X86CPUTopology as CPUTopology; #[cfg(target_arch = "x86_64")] pub use x86_64::X86RegsIndex as RegsIndex; use std::cell::RefCell; use std::sync::atomic::{fence, AtomicBool, Ordering}; use std::sync::{Arc, Barrier, Condvar, Mutex, Weak}; use std::thread; use anyhow::{anyhow, Context, Result}; use log::{error, info, warn}; use nix::unistd::gettid; use machine_manager::config::ShutdownAction::{ShutdownActionPause, ShutdownActionPoweroff}; use machine_manager::event; use machine_manager::machine::{HypervisorType, MachineInterface}; use machine_manager::qmp::{qmp_channel::QmpChannel, qmp_schema}; // SIGRTMIN = 34 (GNU, in MUSL is 35) and SIGRTMAX = 64 in linux, VCPU signal // number should be assigned to SIGRTMIN + n, (n = 0...30). #[cfg(target_env = "gnu")] pub const VCPU_TASK_SIGNAL: i32 = 34; #[cfg(target_env = "musl")] pub const VCPU_TASK_SIGNAL: i32 = 35; #[cfg(target_env = "ohos")] pub const VCPU_TASK_SIGNAL: i32 = 40; /// Watch `0x3ff` IO port to record the magic value trapped from guest kernel. #[cfg(all(target_arch = "x86_64", feature = "boot_time"))] const MAGIC_SIGNAL_GUEST_BOOT: u64 = 0x3ff; /// Watch Uart MMIO region to record the magic value trapped from guest kernel. #[cfg(all(target_arch = "aarch64", feature = "boot_time"))] const MAGIC_SIGNAL_GUEST_BOOT: u64 = 0x9000f00; /// The boot start value can be verified before kernel start. #[cfg(feature = "boot_time")] const MAGIC_VALUE_SIGNAL_GUEST_BOOT_START: u8 = 0x01; /// The boot complete value can be verified before init guest userspace. #[cfg(feature = "boot_time")] const MAGIC_VALUE_SIGNAL_GUEST_BOOT_COMPLETE: u8 = 0x02; /// State for `CPU` lifecycle. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum CpuLifecycleState { /// `CPU` structure's property is set with configuration. Created = 1, /// `CPU` start to be running. Running = 2, /// `CPU` thread is sleeping. Paused = 3, /// `CPU` structure is going to destroy. Stopping = 4, /// `CPU` structure destroyed, will be dropped soon. Stopped = 5, } /// Trait to handle `CPU` lifetime. #[allow(clippy::upper_case_acronyms)] pub trait CPUInterface { /// Realize `CPU` structure, set registers value for `CPU`. fn realize( &self, boot: &CPUBootConfig, topology: &CPUTopology, #[cfg(target_arch = "aarch64")] features: &CPUFeatures, ) -> Result<()>; /// Start `CPU` thread and run virtual CPU in hypervisor. /// /// # Arguments /// /// * `cpu` - The cpu instance shared in thread. /// * `thread_barrier` - The cpu thread barrier. /// * `paused` - After started, paused vcpu or not. fn start(cpu: Arc<Self>, thread_barrier: Arc<Barrier>, paused: bool) -> Result<()> where Self: std::marker::Sized; /// Make `CPU` lifecycle from `Running` to `Paused`. fn pause(&self) -> Result<()>; /// Make `CPU` lifecycle from `Paused` to `Running`. fn resume(&self) -> Result<()>; /// Make `CPU` lifecycle to `Stopping`, then `Stopped`. fn destroy(&self) -> Result<()>; /// Make `CPU` destroy because of guest inner shutdown. fn guest_shutdown(&self) -> Result<()>; /// Make `CPU` destroy because of guest inner reset. fn guest_reset(&self) -> Result<()>; } pub trait CPUHypervisorOps: Send + Sync { fn get_hypervisor_type(&self) -> HypervisorType; fn init_pmu(&self) -> Result<()>; fn vcpu_init(&self) -> Result<()>; fn set_boot_config( &self, arch_cpu: Arc<Mutex<ArchCPU>>, boot_config: &CPUBootConfig, #[cfg(target_arch = "aarch64")] vcpu_config: &CPUFeatures, ) -> Result<()>; fn get_one_reg(&self, reg_id: u64) -> Result<u128>; fn get_regs(&self, arch_cpu: Arc<Mutex<ArchCPU>>, regs_index: RegsIndex) -> Result<()>; fn set_regs(&self, arch_cpu: Arc<Mutex<ArchCPU>>, regs_index: RegsIndex) -> Result<()>; fn put_register(&self, cpu: Arc<CPU>) -> Result<()>; fn reset_vcpu(&self, cpu: Arc<CPU>) -> Result<()>; fn vcpu_exec( &self, cpu_thread_worker: CPUThreadWorker, thread_barrier: Arc<Barrier>, ) -> Result<()>; fn set_hypervisor_exit(&self) -> Result<()>; fn pause( &self, task: Arc<Mutex<Option<thread::JoinHandle<()>>>>, state: Arc<(Mutex<CpuLifecycleState>, Condvar)>, pause_signal: Arc<AtomicBool>, ) -> Result<()>; fn resume( &self, state: Arc<(Mutex<CpuLifecycleState>, Condvar)>, pause_signal: Arc<AtomicBool>, ) -> Result<()>; fn destroy( &self, task: Arc<Mutex<Option<thread::JoinHandle<()>>>>, state: Arc<(Mutex<CpuLifecycleState>, Condvar)>, ) -> Result<()>; } /// `CPU` is a wrapper around creating and using a hypervisor-based VCPU. #[allow(clippy::upper_case_acronyms)] pub struct CPU { /// ID of this virtual CPU, `0` means this cpu is primary `CPU`. pub id: u8, /// Architecture special CPU property. pub arch_cpu: Arc<Mutex<ArchCPU>>, /// LifeCycle state of hypervisor-based VCPU. pub state: Arc<(Mutex<CpuLifecycleState>, Condvar)>, /// The thread handler of this virtual CPU. task: Arc<Mutex<Option<thread::JoinHandle<()>>>>, /// The thread tid of this VCPU. tid: Arc<Mutex<Option<u64>>>, /// The VM combined by this VCPU. vm: Weak<Mutex<dyn MachineInterface + Send + Sync>>, /// The state backup of architecture CPU right before boot. boot_state: Arc<Mutex<ArchCPU>>, /// Sync the pause state of vCPU in hypervisor and userspace. pause_signal: Arc<AtomicBool>, /// Interact between the vCPU and hypervisor. pub hypervisor_cpu: Arc<dyn CPUHypervisorOps>, } impl CPU { /// Allocates a new `CPU` for `vm` /// /// # Arguments /// /// * `vcpu_fd` - The file descriptor of this `CPU`. /// * `id` - ID of this `CPU`. /// * `arch_cpu` - Architecture special `CPU` property. /// * `vm` - The virtual machine this `CPU` gets attached to. pub fn new( hypervisor_cpu: Arc<dyn CPUHypervisorOps>, id: u8, arch_cpu: Arc<Mutex<ArchCPU>>, vm: Arc<Mutex<dyn MachineInterface + Send + Sync>>, ) -> Self { CPU { id, arch_cpu, state: Arc::new((Mutex::new(CpuLifecycleState::Created), Condvar::new())), task: Arc::new(Mutex::new(None)), tid: Arc::new(Mutex::new(None)), vm: Arc::downgrade(&vm), boot_state: Arc::new(Mutex::new(ArchCPU::default())), pause_signal: Arc::new(AtomicBool::new(false)), hypervisor_cpu, } } /// Get this `CPU`'s ID. pub fn id(&self) -> u8 { self.id } /// Get this `CPU`'s state. pub fn state(&self) -> &(Mutex<CpuLifecycleState>, Condvar) { self.state.as_ref() } /// Get this `CPU`'s architecture-special property. pub fn arch(&self) -> &Arc<Mutex<ArchCPU>> { &self.arch_cpu } /// Set task the `CPU` to handle. fn set_task(&self, task: Option<thread::JoinHandle<()>>) { let mut data = self.task.lock().unwrap(); (*data).take().map(thread::JoinHandle::join); *data = task; } /// Get this `CPU`'s thread id. pub fn tid(&self) -> u64 { (*self.tid.lock().unwrap()).unwrap_or(0) } /// Set thread id for `CPU`. pub fn set_tid(&self, tid: Option<u64>) { if tid.is_none() { *self.tid.lock().unwrap() = Some(gettid().as_raw() as u64); } else { *self.tid.lock().unwrap() = tid; } } /// Get the hypervisor of this `CPU`. pub fn hypervisor_cpu(&self) -> &Arc<dyn CPUHypervisorOps> { &self.hypervisor_cpu } pub fn vm(&self) -> Weak<Mutex<dyn MachineInterface + Send + Sync>> { self.vm.clone() } pub fn boot_state(&self) -> Arc<Mutex<ArchCPU>> { self.boot_state.clone() } pub fn pause_signal(&self) -> Arc<AtomicBool> { self.pause_signal.clone() } } impl CPUInterface for CPU { fn realize( &self, boot: &CPUBootConfig, topology: &CPUTopology, #[cfg(target_arch = "aarch64")] config: &CPUFeatures, ) -> Result<()> { trace::cpu_boot_config(boot); let (cpu_state, _) = &*self.state; if *cpu_state.lock().unwrap() != CpuLifecycleState::Created { return Err(anyhow!(CpuError::RealizeVcpu(format!( "VCPU{} may has realized.", self.id() )))); } self.hypervisor_cpu .set_boot_config( self.arch_cpu.clone(), boot, #[cfg(target_arch = "aarch64")] config, ) .with_context(|| "Failed to realize arch cpu")?; self.arch_cpu .lock() .unwrap() .set_cpu_topology(topology) .with_context(|| "Failed to realize arch cpu")?; self.boot_state.lock().unwrap().set(&self.arch_cpu); Ok(()) } fn resume(&self) -> Result<()> { #[cfg(target_arch = "aarch64")] self.hypervisor_cpu .set_regs(self.arch_cpu.clone(), RegsIndex::VtimerCount)?; self.hypervisor_cpu .resume(self.state.clone(), self.pause_signal.clone()) } fn start(cpu: Arc<CPU>, thread_barrier: Arc<Barrier>, paused: bool) -> Result<()> { let (cpu_state, _) = &*cpu.state; if *cpu_state.lock().unwrap() == CpuLifecycleState::Running { return Err(anyhow!(CpuError::StartVcpu( "Cpu is already running".to_string() ))); } if paused { *cpu_state.lock().unwrap() = CpuLifecycleState::Paused; } else { *cpu_state.lock().unwrap() = CpuLifecycleState::Running; } let local_cpu = cpu.clone(); let cpu_id = cpu.id(); let hypervisor_cpu = cpu.hypervisor_cpu().clone(); let hyp_type = hypervisor_cpu.get_hypervisor_type(); let cpu_thread_worker = CPUThreadWorker::new(cpu); let handle = thread::Builder::new() .name(format!("CPU {}/{:?}", local_cpu.id, hyp_type)) .spawn(move || { if let Err(e) = hypervisor_cpu.vcpu_exec(cpu_thread_worker, thread_barrier) { error!("Some error occurred in cpu{} thread: {:?}", cpu_id, e); } }) .with_context(|| { format!("Failed to create thread for CPU {}/{:?}", cpu_id, hyp_type) })?; local_cpu.set_task(Some(handle)); Ok(()) } fn pause(&self) -> Result<()> { self.hypervisor_cpu.pause( self.task.clone(), self.state.clone(), self.pause_signal.clone(), )?; #[cfg(target_arch = "aarch64")] self.hypervisor_cpu .get_regs(self.arch_cpu.clone(), RegsIndex::VtimerCount)?; Ok(()) } fn destroy(&self) -> Result<()> { self.hypervisor_cpu .destroy(self.task.clone(), self.state.clone()) } fn guest_shutdown(&self) -> Result<()> { if let Some(vm) = self.vm.upgrade() { let shutdown_act = vm.lock().unwrap().get_shutdown_action(); match shutdown_act { ShutdownActionPoweroff => { let (cpu_state, _) = &*self.state; *cpu_state.lock().unwrap() = CpuLifecycleState::Stopped; vm.lock().unwrap().destroy(); } ShutdownActionPause => { vm.lock().unwrap().pause(); } } } else { return Err(anyhow!(CpuError::NoMachineInterface)); } if QmpChannel::is_connected() { let shutdown_msg = qmp_schema::Shutdown { guest: true, reason: "guest-shutdown".to_string(), }; event!(Shutdown; shutdown_msg); } Ok(()) } fn guest_reset(&self) -> Result<()> { if let Some(vm) = self.vm.upgrade() { let (cpu_state, _) = &*self.state; *cpu_state.lock().unwrap() = CpuLifecycleState::Paused; vm.lock().unwrap().reset(); } else { return Err(anyhow!(CpuError::NoMachineInterface)); } Ok(()) } } /// The struct to handle events in cpu thread. #[allow(clippy::upper_case_acronyms)] pub struct CPUThreadWorker { pub thread_cpu: Arc<CPU>, } impl CPUThreadWorker { thread_local!(static LOCAL_THREAD_VCPU: RefCell<Option<CPUThreadWorker>> = RefCell::new(None)); /// Allocates a new `CPUThreadWorker`. fn new(thread_cpu: Arc<CPU>) -> Self { CPUThreadWorker { thread_cpu } } /// Init vcpu thread static variable. pub fn init_local_thread_vcpu(&self) { Self::LOCAL_THREAD_VCPU.with(|thread_vcpu| { *thread_vcpu.borrow_mut() = Some(CPUThreadWorker { thread_cpu: self.thread_cpu.clone(), }); }) } pub fn run_on_local_thread_vcpu<F>(func: F) -> Result<()> where F: FnOnce(Arc<CPU>), { Self::LOCAL_THREAD_VCPU.with(|thread_vcpu| { if let Some(local_thread_vcpu) = thread_vcpu.borrow().as_ref() { func(local_thread_vcpu.thread_cpu.clone()); Ok(()) } else { Err(anyhow!(CpuError::VcpuLocalThreadNotPresent)) } }) } /// Judge whether the hypervisor vcpu is ready to emulate. pub fn ready_for_running(&self) -> Result<bool> { let mut flag = 0_u32; let (cpu_state_locked, cvar) = &*self.thread_cpu.state; let mut cpu_state = cpu_state_locked.lock().unwrap(); loop { match *cpu_state { CpuLifecycleState::Paused => { if flag == 0 { info!("Vcpu{} paused", self.thread_cpu.id); flag = 1; } // Setting pause_signal to be `true` if kvm changes vCPU to pause state. self.thread_cpu.pause_signal().store(true, Ordering::SeqCst); fence(Ordering::Release); cpu_state = cvar.wait(cpu_state).unwrap(); } CpuLifecycleState::Running => { return Ok(true); } CpuLifecycleState::Stopping | CpuLifecycleState::Stopped => { info!("Vcpu{} shutdown", self.thread_cpu.id); return Ok(false); } _ => { warn!("Unknown Vmstate"); return Ok(true); } } } } } /// The wrapper for topology for VCPU. #[derive(Clone)] pub struct CpuTopology { /// Number of vcpus in VM. pub nrcpus: u8, /// Number of sockets in VM. pub sockets: u8, /// Number of dies in one socket. pub dies: u8, /// Number of clusters in one die. pub clusters: u8, /// Number of cores in one cluster. pub cores: u8, /// Number of threads in one core. pub threads: u8, /// Number of online vcpus in VM. pub max_cpus: u8, /// Online mask number of all vcpus. pub online_mask: Arc<Mutex<Vec<u8>>>, } impl CpuTopology { /// * `nr_cpus`: Number of vcpus in one VM. /// * `nr_sockets`: Number of sockets in one VM. /// * `nr_dies`: Number of dies in one socket. /// * `nr_clusters`: Number of clusters in one die. /// * `nr_cores`: Number of cores in one cluster. /// * `nr_threads`: Number of threads in one core. /// * `max_cpus`: Number of online vcpus in VM. pub fn new( nr_cpus: u8, nr_sockets: u8, nr_dies: u8, nr_clusters: u8, nr_cores: u8, nr_threads: u8, max_cpus: u8, ) -> Self { let mut mask: Vec<u8> = vec![0; max_cpus as usize]; (0..nr_cpus as usize).for_each(|index| { mask[index] = 1; }); Self { nrcpus: nr_cpus, sockets: nr_sockets, dies: nr_dies, clusters: nr_clusters, cores: nr_cores, threads: nr_threads, max_cpus, online_mask: Arc::new(Mutex::new(mask)), } } /// Get online mask for a cpu. /// /// # Notes /// /// When `online_mask` is `0`, vcpu is offline. When `online_mask` is `1`, /// vcpu is online. /// /// # Arguments /// /// * `vcpu_id` - ID of vcpu. pub fn get_mask(&self, vcpu_id: usize) -> u8 { let mask = self.online_mask.lock().unwrap(); mask[vcpu_id] } /// Get single cpu topology for vcpu, return this vcpu's `socket-id`, /// `core-id` and `thread-id`. /// /// # Arguments /// /// * `vcpu_id` - ID of vcpu. fn get_topo_item(&self, vcpu_id: usize) -> (u8, u8, u8, u8, u8) { let socketid: u8 = vcpu_id as u8 / (self.dies * self.clusters * self.cores * self.threads); let dieid: u8 = (vcpu_id as u8 / (self.clusters * self.cores * self.threads)) % self.dies; let clusterid: u8 = (vcpu_id as u8 / (self.cores * self.threads)) % self.clusters; let coreid: u8 = (vcpu_id as u8 / self.threads) % self.cores; let threadid: u8 = vcpu_id as u8 % self.threads; (socketid, dieid, clusterid, coreid, threadid) } pub fn get_topo_instance_for_qmp(&self, cpu_index: usize) -> qmp_schema::CpuInstanceProperties { let (socketid, _dieid, _clusterid, coreid, threadid) = self.get_topo_item(cpu_index); qmp_schema::CpuInstanceProperties { node_id: None, socket_id: Some(socketid as isize), #[cfg(target_arch = "x86_64")] die_id: Some(_dieid as isize), #[cfg(target_arch = "aarch64")] cluster_id: Some(_clusterid as isize), core_id: Some(coreid as isize), thread_id: Some(threadid as isize), } } } /// Capture the boot signal that trap from guest kernel, and then record /// kernel boot timestamp. #[cfg(feature = "boot_time")] pub fn capture_boot_signal(addr: u64, data: &[u8]) { if addr == MAGIC_SIGNAL_GUEST_BOOT { if data[0] == MAGIC_VALUE_SIGNAL_GUEST_BOOT_START { info!("Kernel starts to boot!"); } else if data[0] == MAGIC_VALUE_SIGNAL_GUEST_BOOT_COMPLETE { info!("Kernel boot complete!"); } } } #[cfg(test)] mod tests { use std::sync::{Arc, Mutex}; use super::*; #[test] fn test_cpu_get_topu() { let test_nr_cpus: u8 = 16; let mask = Vec::with_capacity(test_nr_cpus as usize); let microvm_cpu_topo = CpuTopology { sockets: test_nr_cpus, dies: 1, clusters: 1, cores: 1, threads: 1, nrcpus: test_nr_cpus, max_cpus: test_nr_cpus, online_mask: Arc::new(Mutex::new(mask)), }; assert_eq!(microvm_cpu_topo.get_topo_item(0), (0, 0, 0, 0, 0)); assert_eq!(microvm_cpu_topo.get_topo_item(4), (4, 0, 0, 0, 0)); assert_eq!(microvm_cpu_topo.get_topo_item(8), (8, 0, 0, 0, 0)); assert_eq!(microvm_cpu_topo.get_topo_item(15), (15, 0, 0, 0, 0)); let mask = Vec::with_capacity(test_nr_cpus as usize); let microvm_cpu_topo_x86 = CpuTopology { sockets: 1, dies: 2, clusters: 1, cores: 4, threads: 2, nrcpus: test_nr_cpus, max_cpus: test_nr_cpus, online_mask: Arc::new(Mutex::new(mask)), }; assert_eq!(microvm_cpu_topo_x86.get_topo_item(0), (0, 0, 0, 0, 0)); assert_eq!(microvm_cpu_topo_x86.get_topo_item(4), (0, 0, 0, 2, 0)); assert_eq!(microvm_cpu_topo_x86.get_topo_item(8), (0, 1, 0, 0, 0)); assert_eq!(microvm_cpu_topo_x86.get_topo_item(15), (0, 1, 0, 3, 1)); let mask = Vec::with_capacity(test_nr_cpus as usize); let microvm_cpu_topo_arm = CpuTopology { sockets: 1, dies: 1, clusters: 2, cores: 4, threads: 2, nrcpus: test_nr_cpus, max_cpus: test_nr_cpus, online_mask: Arc::new(Mutex::new(mask)), }; assert_eq!(microvm_cpu_topo_arm.get_topo_item(0), (0, 0, 0, 0, 0)); assert_eq!(microvm_cpu_topo_arm.get_topo_item(4), (0, 0, 0, 2, 0)); assert_eq!(microvm_cpu_topo_arm.get_topo_item(8), (0, 0, 1, 0, 0)); assert_eq!(microvm_cpu_topo_arm.get_topo_item(15), (0, 0, 1, 3, 1)); let test_nr_cpus: u8 = 32; let mask = Vec::with_capacity(test_nr_cpus as usize); let test_cpu_topo = CpuTopology { sockets: 2, dies: 1, clusters: 1, cores: 4, threads: 2, nrcpus: test_nr_cpus, max_cpus: test_nr_cpus, online_mask: Arc::new(Mutex::new(mask)), }; assert_eq!(test_cpu_topo.get_topo_item(0), (0, 0, 0, 0, 0)); assert_eq!(test_cpu_topo.get_topo_item(4), (0, 0, 0, 2, 0)); assert_eq!(test_cpu_topo.get_topo_item(7), (0, 0, 0, 3, 1)); assert_eq!(test_cpu_topo.get_topo_item(11), (1, 0, 0, 1, 1)); assert_eq!(test_cpu_topo.get_topo_item(15), (1, 0, 0, 3, 1)); assert_eq!(test_cpu_topo.get_topo_item(17), (2, 0, 0, 0, 1)); assert_eq!(test_cpu_topo.get_topo_item(23), (2, 0, 0, 3, 1)); assert_eq!(test_cpu_topo.get_topo_item(29), (3, 0, 0, 2, 1)); assert_eq!(test_cpu_topo.get_topo_item(31), (3, 0, 0, 3, 1)); } }