Implement execute breakpoints
This commit is contained in:
		
							parent
							
								
									856ee00999
								
							
						
					
					
						commit
						d6eb8ec7ef
					
				| 
						 | 
					@ -18,7 +18,7 @@ use bytemuck::NoUninit;
 | 
				
			||||||
use egui_toast::{Toast, ToastKind, ToastOptions};
 | 
					use egui_toast::{Toast, ToastKind, ToastOptions};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{audio::Audio, graphics::TextureSink};
 | 
					use crate::{audio::Audio, graphics::TextureSink};
 | 
				
			||||||
use shrooms_vb_core::{Sim, EXPECTED_FRAME_SIZE};
 | 
					use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE};
 | 
				
			||||||
pub use shrooms_vb_core::{VBKey, VBRegister};
 | 
					pub use shrooms_vb_core::{VBKey, VBRegister};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod shrooms_vb_core;
 | 
					mod shrooms_vb_core;
 | 
				
			||||||
| 
						 | 
					@ -302,6 +302,9 @@ impl Emulator {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn stop_debugging(&mut self, sim_id: SimId) {
 | 
					    fn stop_debugging(&mut self, sim_id: SimId) {
 | 
				
			||||||
 | 
					        if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
 | 
				
			||||||
 | 
					            sim.clear_breakpoints();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        self.debuggers.remove(&sim_id);
 | 
					        self.debuggers.remove(&sim_id);
 | 
				
			||||||
        if self.debuggers.is_empty() {
 | 
					        if self.debuggers.is_empty() {
 | 
				
			||||||
            let _ = self.state.compare_exchange(
 | 
					            let _ = self.state.compare_exchange(
 | 
				
			||||||
| 
						 | 
					@ -314,17 +317,17 @@ impl Emulator {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn debug_interrupt(&mut self, sim_id: SimId) {
 | 
					    fn debug_interrupt(&mut self, sim_id: SimId) {
 | 
				
			||||||
 | 
					        self.debug_stop(sim_id, DebugStopReason::Trapped);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn debug_stop(&mut self, sim_id: SimId, reason: DebugStopReason) {
 | 
				
			||||||
        let Some(debugger) = self.debuggers.get_mut(&sim_id) else {
 | 
					        let Some(debugger) = self.debuggers.get_mut(&sim_id) else {
 | 
				
			||||||
            self.stop_debugging(sim_id);
 | 
					            self.stop_debugging(sim_id);
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        if !matches!(debugger.stop_reason, Some(DebugStopReason::Trapped)) {
 | 
					        if debugger.stop_reason != Some(reason) {
 | 
				
			||||||
            debugger.stop_reason = Some(DebugStopReason::Trapped);
 | 
					            debugger.stop_reason = Some(reason);
 | 
				
			||||||
            if debugger
 | 
					            if debugger.sender.send(DebugEvent::Stopped(reason)).is_err() {
 | 
				
			||||||
                .sender
 | 
					 | 
				
			||||||
                .send(DebugEvent::Stopped(DebugStopReason::Trapped))
 | 
					 | 
				
			||||||
                .is_err()
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                self.stop_debugging(sim_id);
 | 
					                self.stop_debugging(sim_id);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -389,6 +392,21 @@ impl Emulator {
 | 
				
			||||||
            self.sims[SimId::Player2.to_index()].emulate();
 | 
					            self.sims[SimId::Player2.to_index()].emulate();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Debug state
 | 
				
			||||||
 | 
					        if state == EmulatorState::Debugging {
 | 
				
			||||||
 | 
					            for sim_id in SimId::values() {
 | 
				
			||||||
 | 
					                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                if let Some(reason) = sim.stop_reason() {
 | 
				
			||||||
 | 
					                    let stop_reason = match reason {
 | 
				
			||||||
 | 
					                        StopReason::Breakpoint => DebugStopReason::Breakpoint,
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    self.debug_stop(sim_id, stop_reason);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Video
 | 
					        // Video
 | 
				
			||||||
        for sim_id in SimId::values() {
 | 
					        for sim_id in SimId::values() {
 | 
				
			||||||
            let Some(renderer) = self.renderers.get_mut(&sim_id) else {
 | 
					            let Some(renderer) = self.renderers.get_mut(&sim_id) else {
 | 
				
			||||||
| 
						 | 
					@ -493,6 +511,18 @@ impl Emulator {
 | 
				
			||||||
                sim.read_memory(addresses, &mut buffer);
 | 
					                sim.read_memory(addresses, &mut buffer);
 | 
				
			||||||
                let _ = done.send(buffer);
 | 
					                let _ = done.send(buffer);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            EmulatorCommand::AddBreakpoint(sim_id, address) => {
 | 
				
			||||||
 | 
					                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                sim.add_breakpoint(address);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            EmulatorCommand::RemoveBreakpoint(sim_id, address) => {
 | 
				
			||||||
 | 
					                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                sim.remove_breakpoint(address);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            EmulatorCommand::SetAudioEnabled(p1, p2) => {
 | 
					            EmulatorCommand::SetAudioEnabled(p1, p2) => {
 | 
				
			||||||
                self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
 | 
					                self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
 | 
				
			||||||
                self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
 | 
					                self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
 | 
				
			||||||
| 
						 | 
					@ -556,6 +586,8 @@ pub enum EmulatorCommand {
 | 
				
			||||||
    DebugContinue(SimId),
 | 
					    DebugContinue(SimId),
 | 
				
			||||||
    ReadRegister(SimId, VBRegister, oneshot::Sender<u32>),
 | 
					    ReadRegister(SimId, VBRegister, oneshot::Sender<u32>),
 | 
				
			||||||
    ReadMemory(SimId, Range<u32>, Vec<u8>, oneshot::Sender<Vec<u8>>),
 | 
					    ReadMemory(SimId, Range<u32>, Vec<u8>, oneshot::Sender<Vec<u8>>),
 | 
				
			||||||
 | 
					    AddBreakpoint(SimId, u32),
 | 
				
			||||||
 | 
					    RemoveBreakpoint(SimId, u32),
 | 
				
			||||||
    SetAudioEnabled(bool, bool),
 | 
					    SetAudioEnabled(bool, bool),
 | 
				
			||||||
    Link,
 | 
					    Link,
 | 
				
			||||||
    Unlink,
 | 
					    Unlink,
 | 
				
			||||||
| 
						 | 
					@ -584,6 +616,8 @@ type DebugSender = tokio::sync::mpsc::UnboundedSender<DebugEvent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 | 
					#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 | 
				
			||||||
pub enum DebugStopReason {
 | 
					pub enum DebugStopReason {
 | 
				
			||||||
 | 
					    // We hit a breakpoint
 | 
				
			||||||
 | 
					    Breakpoint,
 | 
				
			||||||
    // The debugger told us to pause
 | 
					    // The debugger told us to pause
 | 
				
			||||||
    Trapped,
 | 
					    Trapped,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,6 +63,8 @@ pub enum VBRegister {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type OnFrame = extern "C" fn(sim: *mut VB) -> c_int;
 | 
					type OnFrame = extern "C" fn(sim: *mut VB) -> c_int;
 | 
				
			||||||
 | 
					type OnExecute =
 | 
				
			||||||
 | 
					    extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[link(name = "vb")]
 | 
					#[link(name = "vb")]
 | 
				
			||||||
extern "C" {
 | 
					extern "C" {
 | 
				
			||||||
| 
						 | 
					@ -109,8 +111,10 @@ extern "C" {
 | 
				
			||||||
    fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int;
 | 
					    fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int;
 | 
				
			||||||
    #[link_name = "vbSetCartROM"]
 | 
					    #[link_name = "vbSetCartROM"]
 | 
				
			||||||
    fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int;
 | 
					    fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int;
 | 
				
			||||||
 | 
					    #[link_name = "vbSetExecuteCallback"]
 | 
				
			||||||
 | 
					    fn vb_set_execute_callback(sim: *mut VB, callback: Option<OnExecute>);
 | 
				
			||||||
    #[link_name = "vbSetFrameCallback"]
 | 
					    #[link_name = "vbSetFrameCallback"]
 | 
				
			||||||
    fn vb_set_frame_callback(sim: *mut VB, on_frame: OnFrame);
 | 
					    fn vb_set_frame_callback(sim: *mut VB, callback: Option<OnFrame>);
 | 
				
			||||||
    #[link_name = "vbSetKeys"]
 | 
					    #[link_name = "vbSetKeys"]
 | 
				
			||||||
    fn vb_set_keys(sim: *mut VB, keys: u16) -> u16;
 | 
					    fn vb_set_keys(sim: *mut VB, keys: u16) -> u16;
 | 
				
			||||||
    #[link_name = "vbSetOption"]
 | 
					    #[link_name = "vbSetOption"]
 | 
				
			||||||
| 
						 | 
					@ -130,7 +134,7 @@ extern "C" {
 | 
				
			||||||
    fn vb_size_of() -> usize;
 | 
					    fn vb_size_of() -> usize;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
extern "C" fn on_frame(sim: *mut VB) -> i32 {
 | 
					extern "C" fn on_frame(sim: *mut VB) -> c_int {
 | 
				
			||||||
    // SAFETY: the *mut VB owns its userdata.
 | 
					    // SAFETY: the *mut VB owns its userdata.
 | 
				
			||||||
    // There is no way for the userdata to be null or otherwise invalid.
 | 
					    // There is no way for the userdata to be null or otherwise invalid.
 | 
				
			||||||
    let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
 | 
					    let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
 | 
				
			||||||
| 
						 | 
					@ -138,12 +142,27 @@ extern "C" fn on_frame(sim: *mut VB) -> i32 {
 | 
				
			||||||
    1
 | 
					    1
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int {
 | 
				
			||||||
 | 
					    // SAFETY: the *mut VB owns its userdata.
 | 
				
			||||||
 | 
					    // There is no way for the userdata to be null or otherwise invalid.
 | 
				
			||||||
 | 
					    let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if data.breakpoints.binary_search(&address).is_err() {
 | 
				
			||||||
 | 
					        return 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    data.stop_reason = Some(StopReason::Breakpoint);
 | 
				
			||||||
 | 
					    1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4;
 | 
					const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4;
 | 
				
			||||||
const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2;
 | 
					const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2;
 | 
				
			||||||
pub const EXPECTED_FRAME_SIZE: usize = 834 * 2;
 | 
					pub const EXPECTED_FRAME_SIZE: usize = 834 * 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct VBState {
 | 
					struct VBState {
 | 
				
			||||||
    frame_seen: bool,
 | 
					    frame_seen: bool,
 | 
				
			||||||
 | 
					    stop_reason: Option<StopReason>,
 | 
				
			||||||
 | 
					    breakpoints: Vec<u32>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[repr(transparent)]
 | 
					#[repr(transparent)]
 | 
				
			||||||
| 
						 | 
					@ -151,8 +170,9 @@ pub struct Sim {
 | 
				
			||||||
    sim: *mut VB,
 | 
					    sim: *mut VB,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SAFETY: the memory pointed to by sim is valid
 | 
					pub enum StopReason {
 | 
				
			||||||
unsafe impl Send for Sim {}
 | 
					    Breakpoint,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Sim {
 | 
					impl Sim {
 | 
				
			||||||
    pub fn new() -> Self {
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
| 
						 | 
					@ -166,9 +186,13 @@ impl Sim {
 | 
				
			||||||
        unsafe { vb_reset(sim) };
 | 
					        unsafe { vb_reset(sim) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // set up userdata
 | 
					        // set up userdata
 | 
				
			||||||
        let state = VBState { frame_seen: false };
 | 
					        let state = VBState {
 | 
				
			||||||
 | 
					            frame_seen: false,
 | 
				
			||||||
 | 
					            stop_reason: None,
 | 
				
			||||||
 | 
					            breakpoints: vec![],
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
        unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
 | 
					        unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
 | 
				
			||||||
        unsafe { vb_set_frame_callback(sim, on_frame) };
 | 
					        unsafe { vb_set_frame_callback(sim, Some(on_frame)) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // set up audio buffer
 | 
					        // set up audio buffer
 | 
				
			||||||
        let audio_buffer = vec![0.0f32; AUDIO_CAPACITY_FLOATS];
 | 
					        let audio_buffer = vec![0.0f32; AUDIO_CAPACITY_FLOATS];
 | 
				
			||||||
| 
						 | 
					@ -257,9 +281,7 @@ impl Sim {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool {
 | 
					    pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool {
 | 
				
			||||||
        // SAFETY: the *mut VB owns its userdata.
 | 
					        let data = self.get_state();
 | 
				
			||||||
        // There is no way for the userdata to be null or otherwise invalid.
 | 
					 | 
				
			||||||
        let data: &mut VBState = unsafe { &mut *vb_get_user_data(self.sim).cast() };
 | 
					 | 
				
			||||||
        if !data.frame_seen {
 | 
					        if !data.frame_seen {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -323,6 +345,44 @@ impl Sim {
 | 
				
			||||||
            into.push(byte as u8);
 | 
					            into.push(byte as u8);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn add_breakpoint(&mut self, address: u32) {
 | 
				
			||||||
 | 
					        let data = self.get_state();
 | 
				
			||||||
 | 
					        if let Err(index) = data.breakpoints.binary_search(&address) {
 | 
				
			||||||
 | 
					            data.breakpoints.insert(index, address);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        unsafe {
 | 
				
			||||||
 | 
					            vb_set_execute_callback(self.sim, Some(on_execute));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn remove_breakpoint(&mut self, address: u32) {
 | 
				
			||||||
 | 
					        let data = self.get_state();
 | 
				
			||||||
 | 
					        if let Ok(index) = data.breakpoints.binary_search(&address) {
 | 
				
			||||||
 | 
					            data.breakpoints.remove(index);
 | 
				
			||||||
 | 
					            if data.breakpoints.is_empty() {
 | 
				
			||||||
 | 
					                unsafe { vb_set_execute_callback(self.sim, None) };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn clear_breakpoints(&mut self) {
 | 
				
			||||||
 | 
					        let data = self.get_state();
 | 
				
			||||||
 | 
					        data.breakpoints.clear();
 | 
				
			||||||
 | 
					        unsafe { vb_set_execute_callback(self.sim, None) };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn stop_reason(&mut self) -> Option<StopReason> {
 | 
				
			||||||
 | 
					        let data = self.get_state();
 | 
				
			||||||
 | 
					        data.stop_reason.take()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_state(&mut self) -> &mut VBState {
 | 
				
			||||||
 | 
					        // SAFETY: the *mut VB owns its userdata.
 | 
				
			||||||
 | 
					        // There is no way for the userdata to be null or otherwise invalid.
 | 
				
			||||||
 | 
					        unsafe { &mut *vb_get_user_data(self.sim).cast() }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Drop for Sim {
 | 
					impl Drop for Sim {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -307,6 +307,22 @@ impl GdbConnection {
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                self.response()
 | 
					                self.response()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        } else if req.match_str("Z0,") {
 | 
				
			||||||
 | 
					            if let Some(address) = req.match_hex() {
 | 
				
			||||||
 | 
					                self.client
 | 
				
			||||||
 | 
					                    .send_command(EmulatorCommand::AddBreakpoint(self.sim_id, address));
 | 
				
			||||||
 | 
					                self.response().write_str("OK")
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                self.response()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if req.match_str("z0,") {
 | 
				
			||||||
 | 
					            if let Some(address) = req.match_hex() {
 | 
				
			||||||
 | 
					                self.client
 | 
				
			||||||
 | 
					                    .send_command(EmulatorCommand::RemoveBreakpoint(self.sim_id, address));
 | 
				
			||||||
 | 
					                self.response().write_str("OK")
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                self.response()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            // unrecognized command
 | 
					            // unrecognized command
 | 
				
			||||||
            self.response()
 | 
					            self.response()
 | 
				
			||||||
| 
						 | 
					@ -331,7 +347,8 @@ impl Drop for GdbConnection {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> &'static str {
 | 
					fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> &'static str {
 | 
				
			||||||
    match reason {
 | 
					    match reason {
 | 
				
			||||||
        Some(DebugStopReason::Trapped) => "T05;thread:p1.t1;threads:p1.t1;reason:trap;",
 | 
					        Some(DebugStopReason::Breakpoint) => "T05;thread:p1.t1;threads;p1.t1;reason:breakpoint",
 | 
				
			||||||
 | 
					        Some(DebugStopReason::Trapped) => "T05;swbreak;thread:p1.t1;threads:p1.t1;reason:trap;",
 | 
				
			||||||
        None => "T00;thread:p1.t1;threads:p1.t1;",
 | 
					        None => "T00;thread:p1.t1;threads:p1.t1;",
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue