mirror of https://git.suyu.dev/suyu/suyu
core: Make the main System class use the PImpl idiom
core.h is kind of a massive header in terms what it includes within itself. It includes VFS utilities, kernel headers, file_sys header, ARM-related headers, etc. This means that changing anything in the headers included by core.h essentially requires you to rebuild almost all of core. Instead, we can modify the System class to use the PImpl idiom, which allows us to move all of those headers to the cpp file and forward declare the bulk of the types that would otherwise be included, reducing compile times. This change specifically only performs the PImpl portion.
This commit is contained in:
parent
5094dfa081
commit
e2457418da
|
@ -27,71 +27,9 @@ namespace Core {
|
||||||
|
|
||||||
/*static*/ System System::s_instance;
|
/*static*/ System System::s_instance;
|
||||||
|
|
||||||
System::System() = default;
|
namespace {
|
||||||
|
FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs,
|
||||||
System::~System() = default;
|
const std::string& path) {
|
||||||
|
|
||||||
/// Runs a CPU core while the system is powered on
|
|
||||||
static void RunCpuCore(std::shared_ptr<Cpu> cpu_state) {
|
|
||||||
while (Core::System::GetInstance().IsPoweredOn()) {
|
|
||||||
cpu_state->RunLoop(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Cpu& System::CurrentCpuCore() {
|
|
||||||
// If multicore is enabled, use host thread to figure out the current CPU core
|
|
||||||
if (Settings::values.use_multi_core) {
|
|
||||||
const auto& search = thread_to_cpu.find(std::this_thread::get_id());
|
|
||||||
ASSERT(search != thread_to_cpu.end());
|
|
||||||
ASSERT(search->second);
|
|
||||||
return *search->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, use single-threaded mode active_core variable
|
|
||||||
return *cpu_cores[active_core];
|
|
||||||
}
|
|
||||||
|
|
||||||
System::ResultStatus System::RunLoop(bool tight_loop) {
|
|
||||||
status = ResultStatus::Success;
|
|
||||||
|
|
||||||
// Update thread_to_cpu in case Core 0 is run from a different host thread
|
|
||||||
thread_to_cpu[std::this_thread::get_id()] = cpu_cores[0];
|
|
||||||
|
|
||||||
if (GDBStub::IsServerEnabled()) {
|
|
||||||
GDBStub::HandlePacket();
|
|
||||||
|
|
||||||
// If the loop is halted and we want to step, use a tiny (1) number of instructions to
|
|
||||||
// execute. Otherwise, get out of the loop function.
|
|
||||||
if (GDBStub::GetCpuHaltFlag()) {
|
|
||||||
if (GDBStub::GetCpuStepFlag()) {
|
|
||||||
tight_loop = false;
|
|
||||||
} else {
|
|
||||||
return ResultStatus::Success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (active_core = 0; active_core < NUM_CPU_CORES; ++active_core) {
|
|
||||||
cpu_cores[active_core]->RunLoop(tight_loop);
|
|
||||||
if (Settings::values.use_multi_core) {
|
|
||||||
// Cores 1-3 are run on other threads in this mode
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GDBStub::IsServerEnabled()) {
|
|
||||||
GDBStub::SetCpuStepFlag(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
System::ResultStatus System::SingleStep() {
|
|
||||||
return RunLoop(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
static FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs,
|
|
||||||
const std::string& path) {
|
|
||||||
// To account for split 00+01+etc files.
|
// To account for split 00+01+etc files.
|
||||||
std::string dir_name;
|
std::string dir_name;
|
||||||
std::string filename;
|
std::string filename;
|
||||||
|
@ -121,41 +59,267 @@ static FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem
|
||||||
return vfs->OpenFile(path, FileSys::Mode::Read);
|
return vfs->OpenFile(path, FileSys::Mode::Read);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs a CPU core while the system is powered on
|
||||||
|
void RunCpuCore(std::shared_ptr<Cpu> cpu_state) {
|
||||||
|
while (Core::System::GetInstance().IsPoweredOn()) {
|
||||||
|
cpu_state->RunLoop(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
struct System::Impl {
|
||||||
|
Cpu& CurrentCpuCore() {
|
||||||
|
if (Settings::values.use_multi_core) {
|
||||||
|
const auto& search = thread_to_cpu.find(std::this_thread::get_id());
|
||||||
|
ASSERT(search != thread_to_cpu.end());
|
||||||
|
ASSERT(search->second);
|
||||||
|
return *search->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use single-threaded mode active_core variable
|
||||||
|
return *cpu_cores[active_core];
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultStatus RunLoop(bool tight_loop) {
|
||||||
|
status = ResultStatus::Success;
|
||||||
|
|
||||||
|
// Update thread_to_cpu in case Core 0 is run from a different host thread
|
||||||
|
thread_to_cpu[std::this_thread::get_id()] = cpu_cores[0];
|
||||||
|
|
||||||
|
if (GDBStub::IsServerEnabled()) {
|
||||||
|
GDBStub::HandlePacket();
|
||||||
|
|
||||||
|
// If the loop is halted and we want to step, use a tiny (1) number of instructions to
|
||||||
|
// execute. Otherwise, get out of the loop function.
|
||||||
|
if (GDBStub::GetCpuHaltFlag()) {
|
||||||
|
if (GDBStub::GetCpuStepFlag()) {
|
||||||
|
tight_loop = false;
|
||||||
|
} else {
|
||||||
|
return ResultStatus::Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (active_core = 0; active_core < NUM_CPU_CORES; ++active_core) {
|
||||||
|
cpu_cores[active_core]->RunLoop(tight_loop);
|
||||||
|
if (Settings::values.use_multi_core) {
|
||||||
|
// Cores 1-3 are run on other threads in this mode
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GDBStub::IsServerEnabled()) {
|
||||||
|
GDBStub::SetCpuStepFlag(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultStatus Init(Frontend::EmuWindow& emu_window) {
|
||||||
|
LOG_DEBUG(HW_Memory, "initialized OK");
|
||||||
|
|
||||||
|
CoreTiming::Init();
|
||||||
|
kernel.Initialize();
|
||||||
|
|
||||||
|
// Create a default fs if one doesn't already exist.
|
||||||
|
if (virtual_filesystem == nullptr)
|
||||||
|
virtual_filesystem = std::make_shared<FileSys::RealVfsFilesystem>();
|
||||||
|
|
||||||
|
current_process = Kernel::Process::Create(kernel, "main");
|
||||||
|
|
||||||
|
cpu_barrier = std::make_shared<CpuBarrier>();
|
||||||
|
cpu_exclusive_monitor = Cpu::MakeExclusiveMonitor(cpu_cores.size());
|
||||||
|
for (size_t index = 0; index < cpu_cores.size(); ++index) {
|
||||||
|
cpu_cores[index] = std::make_shared<Cpu>(cpu_exclusive_monitor, cpu_barrier, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry_session = std::make_unique<Core::TelemetrySession>();
|
||||||
|
service_manager = std::make_shared<Service::SM::ServiceManager>();
|
||||||
|
|
||||||
|
Service::Init(service_manager, virtual_filesystem);
|
||||||
|
GDBStub::Init();
|
||||||
|
|
||||||
|
renderer = VideoCore::CreateRenderer(emu_window);
|
||||||
|
if (!renderer->Init()) {
|
||||||
|
return ResultStatus::ErrorVideoCore;
|
||||||
|
}
|
||||||
|
|
||||||
|
gpu_core = std::make_unique<Tegra::GPU>(renderer->Rasterizer());
|
||||||
|
|
||||||
|
// Create threads for CPU cores 1-3, and build thread_to_cpu map
|
||||||
|
// CPU core 0 is run on the main thread
|
||||||
|
thread_to_cpu[std::this_thread::get_id()] = cpu_cores[0];
|
||||||
|
if (Settings::values.use_multi_core) {
|
||||||
|
for (size_t index = 0; index < cpu_core_threads.size(); ++index) {
|
||||||
|
cpu_core_threads[index] =
|
||||||
|
std::make_unique<std::thread>(RunCpuCore, cpu_cores[index + 1]);
|
||||||
|
thread_to_cpu[cpu_core_threads[index]->get_id()] = cpu_cores[index + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(Core, "Initialized OK");
|
||||||
|
|
||||||
|
// Reset counters and set time origin to current frame
|
||||||
|
GetAndResetPerfStats();
|
||||||
|
perf_stats.BeginSystemFrame();
|
||||||
|
|
||||||
|
return ResultStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultStatus Load(Frontend::EmuWindow& emu_window, const std::string& filepath) {
|
||||||
|
app_loader = Loader::GetLoader(GetGameFileFromPath(virtual_filesystem, filepath));
|
||||||
|
|
||||||
|
if (!app_loader) {
|
||||||
|
LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath);
|
||||||
|
return ResultStatus::ErrorGetLoader;
|
||||||
|
}
|
||||||
|
std::pair<boost::optional<u32>, Loader::ResultStatus> system_mode =
|
||||||
|
app_loader->LoadKernelSystemMode();
|
||||||
|
|
||||||
|
if (system_mode.second != Loader::ResultStatus::Success) {
|
||||||
|
LOG_CRITICAL(Core, "Failed to determine system mode (Error {})!",
|
||||||
|
static_cast<int>(system_mode.second));
|
||||||
|
|
||||||
|
return ResultStatus::ErrorSystemMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultStatus init_result{Init(emu_window)};
|
||||||
|
if (init_result != ResultStatus::Success) {
|
||||||
|
LOG_CRITICAL(Core, "Failed to initialize system (Error {})!",
|
||||||
|
static_cast<int>(init_result));
|
||||||
|
Shutdown();
|
||||||
|
return init_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Loader::ResultStatus load_result{app_loader->Load(current_process)};
|
||||||
|
if (load_result != Loader::ResultStatus::Success) {
|
||||||
|
LOG_CRITICAL(Core, "Failed to load ROM (Error {})!", static_cast<int>(load_result));
|
||||||
|
Shutdown();
|
||||||
|
|
||||||
|
return static_cast<ResultStatus>(static_cast<u32>(ResultStatus::ErrorLoader) +
|
||||||
|
static_cast<u32>(load_result));
|
||||||
|
}
|
||||||
|
status = ResultStatus::Success;
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shutdown() {
|
||||||
|
// Log last frame performance stats
|
||||||
|
auto perf_results = GetAndResetPerfStats();
|
||||||
|
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_EmulationSpeed",
|
||||||
|
perf_results.emulation_speed * 100.0);
|
||||||
|
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_Framerate",
|
||||||
|
perf_results.game_fps);
|
||||||
|
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_Frametime",
|
||||||
|
perf_results.frametime * 1000.0);
|
||||||
|
|
||||||
|
// Shutdown emulation session
|
||||||
|
renderer.reset();
|
||||||
|
GDBStub::Shutdown();
|
||||||
|
Service::Shutdown();
|
||||||
|
service_manager.reset();
|
||||||
|
telemetry_session.reset();
|
||||||
|
gpu_core.reset();
|
||||||
|
|
||||||
|
// Close all CPU/threading state
|
||||||
|
cpu_barrier->NotifyEnd();
|
||||||
|
if (Settings::values.use_multi_core) {
|
||||||
|
for (auto& thread : cpu_core_threads) {
|
||||||
|
thread->join();
|
||||||
|
thread.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread_to_cpu.clear();
|
||||||
|
for (auto& cpu_core : cpu_cores) {
|
||||||
|
cpu_core.reset();
|
||||||
|
}
|
||||||
|
cpu_barrier.reset();
|
||||||
|
|
||||||
|
// Shutdown kernel and core timing
|
||||||
|
kernel.Shutdown();
|
||||||
|
CoreTiming::Shutdown();
|
||||||
|
|
||||||
|
// Close app loader
|
||||||
|
app_loader.reset();
|
||||||
|
|
||||||
|
LOG_DEBUG(Core, "Shutdown OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::ResultStatus GetGameName(std::string& out) const {
|
||||||
|
if (app_loader == nullptr)
|
||||||
|
return Loader::ResultStatus::ErrorNotInitialized;
|
||||||
|
return app_loader->ReadTitle(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetStatus(ResultStatus new_status, const char* details = nullptr) {
|
||||||
|
status = new_status;
|
||||||
|
if (details) {
|
||||||
|
status_details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PerfStats::Results GetAndResetPerfStats() {
|
||||||
|
return perf_stats.GetAndResetStats(CoreTiming::GetGlobalTimeUs());
|
||||||
|
}
|
||||||
|
|
||||||
|
Kernel::KernelCore kernel;
|
||||||
|
/// RealVfsFilesystem instance
|
||||||
|
FileSys::VirtualFilesystem virtual_filesystem;
|
||||||
|
/// AppLoader used to load the current executing application
|
||||||
|
std::unique_ptr<Loader::AppLoader> app_loader;
|
||||||
|
std::unique_ptr<VideoCore::RendererBase> renderer;
|
||||||
|
std::unique_ptr<Tegra::GPU> gpu_core;
|
||||||
|
std::shared_ptr<Tegra::DebugContext> debug_context;
|
||||||
|
Kernel::SharedPtr<Kernel::Process> current_process;
|
||||||
|
std::shared_ptr<ExclusiveMonitor> cpu_exclusive_monitor;
|
||||||
|
std::shared_ptr<CpuBarrier> cpu_barrier;
|
||||||
|
std::array<std::shared_ptr<Cpu>, NUM_CPU_CORES> cpu_cores;
|
||||||
|
std::array<std::unique_ptr<std::thread>, NUM_CPU_CORES - 1> cpu_core_threads;
|
||||||
|
size_t active_core{}; ///< Active core, only used in single thread mode
|
||||||
|
|
||||||
|
/// Service manager
|
||||||
|
std::shared_ptr<Service::SM::ServiceManager> service_manager;
|
||||||
|
|
||||||
|
/// Telemetry session for this emulation session
|
||||||
|
std::unique_ptr<Core::TelemetrySession> telemetry_session;
|
||||||
|
|
||||||
|
ResultStatus status = ResultStatus::Success;
|
||||||
|
std::string status_details = "";
|
||||||
|
|
||||||
|
/// Map of guest threads to CPU cores
|
||||||
|
std::map<std::thread::id, std::shared_ptr<Cpu>> thread_to_cpu;
|
||||||
|
|
||||||
|
Core::PerfStats perf_stats;
|
||||||
|
Core::FrameLimiter frame_limiter;
|
||||||
|
};
|
||||||
|
|
||||||
|
System::System() : impl{std::make_unique<Impl>()} {}
|
||||||
|
System::~System() = default;
|
||||||
|
|
||||||
|
Cpu& System::CurrentCpuCore() {
|
||||||
|
return impl->CurrentCpuCore();
|
||||||
|
}
|
||||||
|
|
||||||
|
System::ResultStatus System::RunLoop(bool tight_loop) {
|
||||||
|
return impl->RunLoop(tight_loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
System::ResultStatus System::SingleStep() {
|
||||||
|
return RunLoop(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void System::InvalidateCpuInstructionCaches() {
|
||||||
|
for (auto& cpu : impl->cpu_cores) {
|
||||||
|
cpu->ArmInterface().ClearInstructionCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath) {
|
System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath) {
|
||||||
app_loader = Loader::GetLoader(GetGameFileFromPath(virtual_filesystem, filepath));
|
return impl->Load(emu_window, filepath);
|
||||||
|
}
|
||||||
|
|
||||||
if (!app_loader) {
|
bool System::IsPoweredOn() const {
|
||||||
LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath);
|
return impl->cpu_barrier && impl->cpu_barrier->IsAlive();
|
||||||
return ResultStatus::ErrorGetLoader;
|
|
||||||
}
|
|
||||||
std::pair<boost::optional<u32>, Loader::ResultStatus> system_mode =
|
|
||||||
app_loader->LoadKernelSystemMode();
|
|
||||||
|
|
||||||
if (system_mode.second != Loader::ResultStatus::Success) {
|
|
||||||
LOG_CRITICAL(Core, "Failed to determine system mode (Error {})!",
|
|
||||||
static_cast<int>(system_mode.second));
|
|
||||||
|
|
||||||
return ResultStatus::ErrorSystemMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
ResultStatus init_result{Init(emu_window)};
|
|
||||||
if (init_result != ResultStatus::Success) {
|
|
||||||
LOG_CRITICAL(Core, "Failed to initialize system (Error {})!",
|
|
||||||
static_cast<int>(init_result));
|
|
||||||
System::Shutdown();
|
|
||||||
return init_result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Loader::ResultStatus load_result{app_loader->Load(current_process)};
|
|
||||||
if (load_result != Loader::ResultStatus::Success) {
|
|
||||||
LOG_CRITICAL(Core, "Failed to load ROM (Error {})!", static_cast<int>(load_result));
|
|
||||||
System::Shutdown();
|
|
||||||
|
|
||||||
return static_cast<ResultStatus>(static_cast<u32>(ResultStatus::ErrorLoader) +
|
|
||||||
static_cast<u32>(load_result));
|
|
||||||
}
|
|
||||||
status = ResultStatus::Success;
|
|
||||||
return status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void System::PrepareReschedule() {
|
void System::PrepareReschedule() {
|
||||||
|
@ -163,131 +327,134 @@ void System::PrepareReschedule() {
|
||||||
}
|
}
|
||||||
|
|
||||||
PerfStats::Results System::GetAndResetPerfStats() {
|
PerfStats::Results System::GetAndResetPerfStats() {
|
||||||
return perf_stats.GetAndResetStats(CoreTiming::GetGlobalTimeUs());
|
return impl->GetAndResetPerfStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
Core::TelemetrySession& System::TelemetrySession() const {
|
||||||
|
return *impl->telemetry_session;
|
||||||
|
}
|
||||||
|
|
||||||
|
ARM_Interface& System::CurrentArmInterface() {
|
||||||
|
return CurrentCpuCore().ArmInterface();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t System::CurrentCoreIndex() {
|
||||||
|
return CurrentCpuCore().CoreIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
Kernel::Scheduler& System::CurrentScheduler() {
|
||||||
|
return *CurrentCpuCore().Scheduler();
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::shared_ptr<Kernel::Scheduler>& System::Scheduler(size_t core_index) {
|
const std::shared_ptr<Kernel::Scheduler>& System::Scheduler(size_t core_index) {
|
||||||
ASSERT(core_index < NUM_CPU_CORES);
|
ASSERT(core_index < NUM_CPU_CORES);
|
||||||
return cpu_cores[core_index]->Scheduler();
|
return impl->cpu_cores[core_index]->Scheduler();
|
||||||
}
|
}
|
||||||
|
|
||||||
Kernel::KernelCore& System::Kernel() {
|
Kernel::SharedPtr<Kernel::Process>& System::CurrentProcess() {
|
||||||
return kernel;
|
return impl->current_process;
|
||||||
}
|
|
||||||
|
|
||||||
const Kernel::KernelCore& System::Kernel() const {
|
|
||||||
return kernel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ARM_Interface& System::ArmInterface(size_t core_index) {
|
ARM_Interface& System::ArmInterface(size_t core_index) {
|
||||||
ASSERT(core_index < NUM_CPU_CORES);
|
ASSERT(core_index < NUM_CPU_CORES);
|
||||||
return cpu_cores[core_index]->ArmInterface();
|
return impl->cpu_cores[core_index]->ArmInterface();
|
||||||
}
|
}
|
||||||
|
|
||||||
Cpu& System::CpuCore(size_t core_index) {
|
Cpu& System::CpuCore(size_t core_index) {
|
||||||
ASSERT(core_index < NUM_CPU_CORES);
|
ASSERT(core_index < NUM_CPU_CORES);
|
||||||
return *cpu_cores[core_index];
|
return *impl->cpu_cores[core_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
ExclusiveMonitor& System::Monitor() {
|
||||||
|
return *impl->cpu_exclusive_monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tegra::GPU& System::GPU() {
|
||||||
|
return *impl->gpu_core;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tegra::GPU& System::GPU() const {
|
||||||
|
return *impl->gpu_core;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoCore::RendererBase& System::Renderer() {
|
||||||
|
return *impl->renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoCore::RendererBase& System::Renderer() const {
|
||||||
|
return *impl->renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Kernel::KernelCore& System::Kernel() {
|
||||||
|
return impl->kernel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Kernel::KernelCore& System::Kernel() const {
|
||||||
|
return impl->kernel;
|
||||||
|
}
|
||||||
|
|
||||||
|
Core::PerfStats& System::GetPerfStats() {
|
||||||
|
return impl->perf_stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Core::PerfStats& System::GetPerfStats() const {
|
||||||
|
return impl->perf_stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
Core::FrameLimiter& System::FrameLimiter() {
|
||||||
|
return impl->frame_limiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Core::FrameLimiter& System::FrameLimiter() const {
|
||||||
|
return impl->frame_limiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::ResultStatus System::GetGameName(std::string& out) const {
|
||||||
|
return impl->GetGameName(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void System::SetStatus(ResultStatus new_status, const char* details) {
|
||||||
|
impl->SetStatus(new_status, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& System::GetStatusDetails() const {
|
||||||
|
return impl->status_details;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::AppLoader& System::GetAppLoader() const {
|
||||||
|
return *impl->app_loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void System::SetGPUDebugContext(std::shared_ptr<Tegra::DebugContext> context) {
|
||||||
|
impl->debug_context = std::move(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Tegra::DebugContext> System::GetGPUDebugContext() const {
|
||||||
|
return impl->debug_context;
|
||||||
|
}
|
||||||
|
|
||||||
|
void System::SetFilesystem(FileSys::VirtualFilesystem vfs) {
|
||||||
|
impl->virtual_filesystem = std::move(vfs);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSys::VirtualFilesystem System::GetFilesystem() const {
|
||||||
|
return impl->virtual_filesystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
System::ResultStatus System::Init(Frontend::EmuWindow& emu_window) {
|
System::ResultStatus System::Init(Frontend::EmuWindow& emu_window) {
|
||||||
LOG_DEBUG(HW_Memory, "initialized OK");
|
return impl->Init(emu_window);
|
||||||
|
|
||||||
CoreTiming::Init();
|
|
||||||
kernel.Initialize();
|
|
||||||
|
|
||||||
// Create a default fs if one doesn't already exist.
|
|
||||||
if (virtual_filesystem == nullptr)
|
|
||||||
virtual_filesystem = std::make_shared<FileSys::RealVfsFilesystem>();
|
|
||||||
|
|
||||||
current_process = Kernel::Process::Create(kernel, "main");
|
|
||||||
|
|
||||||
cpu_barrier = std::make_shared<CpuBarrier>();
|
|
||||||
cpu_exclusive_monitor = Cpu::MakeExclusiveMonitor(cpu_cores.size());
|
|
||||||
for (size_t index = 0; index < cpu_cores.size(); ++index) {
|
|
||||||
cpu_cores[index] = std::make_shared<Cpu>(cpu_exclusive_monitor, cpu_barrier, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
telemetry_session = std::make_unique<Core::TelemetrySession>();
|
|
||||||
service_manager = std::make_shared<Service::SM::ServiceManager>();
|
|
||||||
|
|
||||||
Service::Init(service_manager, virtual_filesystem);
|
|
||||||
GDBStub::Init();
|
|
||||||
|
|
||||||
renderer = VideoCore::CreateRenderer(emu_window);
|
|
||||||
if (!renderer->Init()) {
|
|
||||||
return ResultStatus::ErrorVideoCore;
|
|
||||||
}
|
|
||||||
|
|
||||||
gpu_core = std::make_unique<Tegra::GPU>(renderer->Rasterizer());
|
|
||||||
|
|
||||||
// Create threads for CPU cores 1-3, and build thread_to_cpu map
|
|
||||||
// CPU core 0 is run on the main thread
|
|
||||||
thread_to_cpu[std::this_thread::get_id()] = cpu_cores[0];
|
|
||||||
if (Settings::values.use_multi_core) {
|
|
||||||
for (size_t index = 0; index < cpu_core_threads.size(); ++index) {
|
|
||||||
cpu_core_threads[index] =
|
|
||||||
std::make_unique<std::thread>(RunCpuCore, cpu_cores[index + 1]);
|
|
||||||
thread_to_cpu[cpu_core_threads[index]->get_id()] = cpu_cores[index + 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DEBUG(Core, "Initialized OK");
|
|
||||||
|
|
||||||
// Reset counters and set time origin to current frame
|
|
||||||
GetAndResetPerfStats();
|
|
||||||
perf_stats.BeginSystemFrame();
|
|
||||||
|
|
||||||
return ResultStatus::Success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void System::Shutdown() {
|
void System::Shutdown() {
|
||||||
// Log last frame performance stats
|
impl->Shutdown();
|
||||||
auto perf_results = GetAndResetPerfStats();
|
|
||||||
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_EmulationSpeed",
|
|
||||||
perf_results.emulation_speed * 100.0);
|
|
||||||
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_Framerate",
|
|
||||||
perf_results.game_fps);
|
|
||||||
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_Frametime",
|
|
||||||
perf_results.frametime * 1000.0);
|
|
||||||
|
|
||||||
// Shutdown emulation session
|
|
||||||
renderer.reset();
|
|
||||||
GDBStub::Shutdown();
|
|
||||||
Service::Shutdown();
|
|
||||||
service_manager.reset();
|
|
||||||
telemetry_session.reset();
|
|
||||||
gpu_core.reset();
|
|
||||||
|
|
||||||
// Close all CPU/threading state
|
|
||||||
cpu_barrier->NotifyEnd();
|
|
||||||
if (Settings::values.use_multi_core) {
|
|
||||||
for (auto& thread : cpu_core_threads) {
|
|
||||||
thread->join();
|
|
||||||
thread.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thread_to_cpu.clear();
|
|
||||||
for (auto& cpu_core : cpu_cores) {
|
|
||||||
cpu_core.reset();
|
|
||||||
}
|
|
||||||
cpu_barrier.reset();
|
|
||||||
|
|
||||||
// Shutdown kernel and core timing
|
|
||||||
kernel.Shutdown();
|
|
||||||
CoreTiming::Shutdown();
|
|
||||||
|
|
||||||
// Close app loader
|
|
||||||
app_loader.reset();
|
|
||||||
|
|
||||||
LOG_DEBUG(Core, "Shutdown OK");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Service::SM::ServiceManager& System::ServiceManager() {
|
Service::SM::ServiceManager& System::ServiceManager() {
|
||||||
return *service_manager;
|
return *impl->service_manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Service::SM::ServiceManager& System::ServiceManager() const {
|
const Service::SM::ServiceManager& System::ServiceManager() const {
|
||||||
return *service_manager;
|
return *impl->service_manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Core
|
} // namespace Core
|
||||||
|
|
138
src/core/core.h
138
src/core/core.h
|
@ -94,11 +94,7 @@ public:
|
||||||
* This function should only be used by GDB Stub to support breakpoints, memory updates and
|
* This function should only be used by GDB Stub to support breakpoints, memory updates and
|
||||||
* step/continue commands.
|
* step/continue commands.
|
||||||
*/
|
*/
|
||||||
void InvalidateCpuInstructionCaches() {
|
void InvalidateCpuInstructionCaches();
|
||||||
for (auto& cpu : cpu_cores) {
|
|
||||||
cpu->ArmInterface().ClearInstructionCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shutdown the emulated system.
|
/// Shutdown the emulated system.
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
|
@ -117,17 +113,13 @@ public:
|
||||||
* application).
|
* application).
|
||||||
* @returns True if the emulated system is powered on, otherwise false.
|
* @returns True if the emulated system is powered on, otherwise false.
|
||||||
*/
|
*/
|
||||||
bool IsPoweredOn() const {
|
bool IsPoweredOn() const;
|
||||||
return cpu_barrier && cpu_barrier->IsAlive();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a reference to the telemetry session for this emulation session.
|
* Returns a reference to the telemetry session for this emulation session.
|
||||||
* @returns Reference to the telemetry session.
|
* @returns Reference to the telemetry session.
|
||||||
*/
|
*/
|
||||||
Core::TelemetrySession& TelemetrySession() const {
|
Core::TelemetrySession& TelemetrySession() const;
|
||||||
return *telemetry_session;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepare the core emulation for a reschedule
|
/// Prepare the core emulation for a reschedule
|
||||||
void PrepareReschedule();
|
void PrepareReschedule();
|
||||||
|
@ -136,14 +128,13 @@ public:
|
||||||
PerfStats::Results GetAndResetPerfStats();
|
PerfStats::Results GetAndResetPerfStats();
|
||||||
|
|
||||||
/// Gets an ARM interface to the CPU core that is currently running
|
/// Gets an ARM interface to the CPU core that is currently running
|
||||||
ARM_Interface& CurrentArmInterface() {
|
ARM_Interface& CurrentArmInterface();
|
||||||
return CurrentCpuCore().ArmInterface();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the index of the currently running CPU core
|
/// Gets the index of the currently running CPU core
|
||||||
size_t CurrentCoreIndex() {
|
size_t CurrentCoreIndex();
|
||||||
return CurrentCpuCore().CoreIndex();
|
|
||||||
}
|
/// Gets the scheduler for the CPU core that is currently running
|
||||||
|
Kernel::Scheduler& CurrentScheduler();
|
||||||
|
|
||||||
/// Gets an ARM interface to the CPU core with the specified index
|
/// Gets an ARM interface to the CPU core with the specified index
|
||||||
ARM_Interface& ArmInterface(size_t core_index);
|
ARM_Interface& ArmInterface(size_t core_index);
|
||||||
|
@ -151,43 +142,26 @@ public:
|
||||||
/// Gets a CPU interface to the CPU core with the specified index
|
/// Gets a CPU interface to the CPU core with the specified index
|
||||||
Cpu& CpuCore(size_t core_index);
|
Cpu& CpuCore(size_t core_index);
|
||||||
|
|
||||||
|
/// Gets the exclusive monitor
|
||||||
|
ExclusiveMonitor& Monitor();
|
||||||
|
|
||||||
/// Gets a mutable reference to the GPU interface
|
/// Gets a mutable reference to the GPU interface
|
||||||
Tegra::GPU& GPU() {
|
Tegra::GPU& GPU();
|
||||||
return *gpu_core;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets an immutable reference to the GPU interface.
|
/// Gets an immutable reference to the GPU interface.
|
||||||
const Tegra::GPU& GPU() const {
|
const Tegra::GPU& GPU() const;
|
||||||
return *gpu_core;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a mutable reference to the renderer.
|
/// Gets a mutable reference to the renderer.
|
||||||
VideoCore::RendererBase& Renderer() {
|
VideoCore::RendererBase& Renderer();
|
||||||
return *renderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets an immutable reference to the renderer.
|
/// Gets an immutable reference to the renderer.
|
||||||
const VideoCore::RendererBase& Renderer() const {
|
const VideoCore::RendererBase& Renderer() const;
|
||||||
return *renderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the scheduler for the CPU core that is currently running
|
|
||||||
Kernel::Scheduler& CurrentScheduler() {
|
|
||||||
return *CurrentCpuCore().Scheduler();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the exclusive monitor
|
|
||||||
ExclusiveMonitor& Monitor() {
|
|
||||||
return *cpu_exclusive_monitor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the scheduler for the CPU core with the specified index
|
/// Gets the scheduler for the CPU core with the specified index
|
||||||
const std::shared_ptr<Kernel::Scheduler>& Scheduler(size_t core_index);
|
const std::shared_ptr<Kernel::Scheduler>& Scheduler(size_t core_index);
|
||||||
|
|
||||||
/// Gets the current process
|
/// Gets the current process
|
||||||
Kernel::SharedPtr<Kernel::Process>& CurrentProcess() {
|
Kernel::SharedPtr<Kernel::Process>& CurrentProcess();
|
||||||
return current_process;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provides a reference to the kernel instance.
|
/// Provides a reference to the kernel instance.
|
||||||
Kernel::KernelCore& Kernel();
|
Kernel::KernelCore& Kernel();
|
||||||
|
@ -195,49 +169,37 @@ public:
|
||||||
/// Provides a constant reference to the kernel instance.
|
/// Provides a constant reference to the kernel instance.
|
||||||
const Kernel::KernelCore& Kernel() const;
|
const Kernel::KernelCore& Kernel() const;
|
||||||
|
|
||||||
|
/// Provides a reference to the internal PerfStats instance.
|
||||||
|
Core::PerfStats& GetPerfStats();
|
||||||
|
|
||||||
|
/// Provides a constant reference to the internal PerfStats instance.
|
||||||
|
const Core::PerfStats& GetPerfStats() const;
|
||||||
|
|
||||||
|
/// Provides a reference to the frame limiter;
|
||||||
|
Core::FrameLimiter& FrameLimiter();
|
||||||
|
|
||||||
|
/// Provides a constant referent to the frame limiter
|
||||||
|
const Core::FrameLimiter& FrameLimiter() const;
|
||||||
|
|
||||||
/// Gets the name of the current game
|
/// Gets the name of the current game
|
||||||
Loader::ResultStatus GetGameName(std::string& out) const {
|
Loader::ResultStatus GetGameName(std::string& out) const;
|
||||||
if (app_loader == nullptr)
|
|
||||||
return Loader::ResultStatus::ErrorNotInitialized;
|
|
||||||
return app_loader->ReadTitle(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
PerfStats perf_stats;
|
void SetStatus(ResultStatus new_status, const char* details);
|
||||||
FrameLimiter frame_limiter;
|
|
||||||
|
|
||||||
void SetStatus(ResultStatus new_status, const char* details = nullptr) {
|
const std::string& GetStatusDetails() const;
|
||||||
status = new_status;
|
|
||||||
if (details) {
|
|
||||||
status_details = details;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& GetStatusDetails() const {
|
Loader::AppLoader& GetAppLoader() const;
|
||||||
return status_details;
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader::AppLoader& GetAppLoader() const {
|
|
||||||
return *app_loader;
|
|
||||||
}
|
|
||||||
|
|
||||||
Service::SM::ServiceManager& ServiceManager();
|
Service::SM::ServiceManager& ServiceManager();
|
||||||
const Service::SM::ServiceManager& ServiceManager() const;
|
const Service::SM::ServiceManager& ServiceManager() const;
|
||||||
|
|
||||||
void SetGPUDebugContext(std::shared_ptr<Tegra::DebugContext> context) {
|
void SetGPUDebugContext(std::shared_ptr<Tegra::DebugContext> context);
|
||||||
debug_context = std::move(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<Tegra::DebugContext> GetGPUDebugContext() const {
|
std::shared_ptr<Tegra::DebugContext> GetGPUDebugContext() const;
|
||||||
return debug_context;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetFilesystem(FileSys::VirtualFilesystem vfs) {
|
void SetFilesystem(FileSys::VirtualFilesystem vfs);
|
||||||
virtual_filesystem = std::move(vfs);
|
|
||||||
}
|
|
||||||
|
|
||||||
FileSys::VirtualFilesystem GetFilesystem() const {
|
FileSys::VirtualFilesystem GetFilesystem() const;
|
||||||
return virtual_filesystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
System();
|
System();
|
||||||
|
@ -253,34 +215,10 @@ private:
|
||||||
*/
|
*/
|
||||||
ResultStatus Init(Frontend::EmuWindow& emu_window);
|
ResultStatus Init(Frontend::EmuWindow& emu_window);
|
||||||
|
|
||||||
Kernel::KernelCore kernel;
|
struct Impl;
|
||||||
/// RealVfsFilesystem instance
|
std::unique_ptr<Impl> impl;
|
||||||
FileSys::VirtualFilesystem virtual_filesystem;
|
|
||||||
/// AppLoader used to load the current executing application
|
|
||||||
std::unique_ptr<Loader::AppLoader> app_loader;
|
|
||||||
std::unique_ptr<VideoCore::RendererBase> renderer;
|
|
||||||
std::unique_ptr<Tegra::GPU> gpu_core;
|
|
||||||
std::shared_ptr<Tegra::DebugContext> debug_context;
|
|
||||||
Kernel::SharedPtr<Kernel::Process> current_process;
|
|
||||||
std::shared_ptr<ExclusiveMonitor> cpu_exclusive_monitor;
|
|
||||||
std::shared_ptr<CpuBarrier> cpu_barrier;
|
|
||||||
std::array<std::shared_ptr<Cpu>, NUM_CPU_CORES> cpu_cores;
|
|
||||||
std::array<std::unique_ptr<std::thread>, NUM_CPU_CORES - 1> cpu_core_threads;
|
|
||||||
size_t active_core{}; ///< Active core, only used in single thread mode
|
|
||||||
|
|
||||||
/// Service manager
|
|
||||||
std::shared_ptr<Service::SM::ServiceManager> service_manager;
|
|
||||||
|
|
||||||
/// Telemetry session for this emulation session
|
|
||||||
std::unique_ptr<Core::TelemetrySession> telemetry_session;
|
|
||||||
|
|
||||||
static System s_instance;
|
static System s_instance;
|
||||||
|
|
||||||
ResultStatus status = ResultStatus::Success;
|
|
||||||
std::string status_details = "";
|
|
||||||
|
|
||||||
/// Map of guest threads to CPU cores
|
|
||||||
std::map<std::thread::id, std::shared_ptr<Cpu>> thread_to_cpu;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline ARM_Interface& CurrentArmInterface() {
|
inline ARM_Interface& CurrentArmInterface() {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
|
#include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
|
||||||
#include "core/hle/service/nvdrv/devices/nvmap.h"
|
#include "core/hle/service/nvdrv/devices/nvmap.h"
|
||||||
|
#include "core/perf_stats.h"
|
||||||
#include "video_core/gpu.h"
|
#include "video_core/gpu.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ void nvdisp_disp0::flip(u32 buffer_handle, u32 offset, u32 format, u32 width, u3
|
||||||
transform, crop_rect};
|
transform, crop_rect};
|
||||||
|
|
||||||
auto& instance = Core::System::GetInstance();
|
auto& instance = Core::System::GetInstance();
|
||||||
instance.perf_stats.EndGameFrame();
|
instance.GetPerfStats().EndGameFrame();
|
||||||
instance.Renderer().SwapBuffers(framebuffer);
|
instance.Renderer().SwapBuffers(framebuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
#include "core/hle/service/nvdrv/nvdrv.h"
|
#include "core/hle/service/nvdrv/nvdrv.h"
|
||||||
#include "core/hle/service/nvflinger/buffer_queue.h"
|
#include "core/hle/service/nvflinger/buffer_queue.h"
|
||||||
#include "core/hle/service/nvflinger/nvflinger.h"
|
#include "core/hle/service/nvflinger/nvflinger.h"
|
||||||
|
#include "core/perf_stats.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
|
@ -137,7 +138,7 @@ void NVFlinger::Compose() {
|
||||||
auto& system_instance = Core::System::GetInstance();
|
auto& system_instance = Core::System::GetInstance();
|
||||||
|
|
||||||
// There was no queued buffer to draw, render previous frame
|
// There was no queued buffer to draw, render previous frame
|
||||||
system_instance.perf_stats.EndGameFrame();
|
system_instance.GetPerfStats().EndGameFrame();
|
||||||
system_instance.Renderer().SwapBuffers({});
|
system_instance.Renderer().SwapBuffers({});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
#include "core/core_timing.h"
|
#include "core/core_timing.h"
|
||||||
#include "core/frontend/emu_window.h"
|
#include "core/frontend/emu_window.h"
|
||||||
#include "core/memory.h"
|
#include "core/memory.h"
|
||||||
|
#include "core/perf_stats.h"
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
#include "core/tracer/recorder.h"
|
#include "core/tracer/recorder.h"
|
||||||
#include "video_core/renderer_opengl/gl_rasterizer.h"
|
#include "video_core/renderer_opengl/gl_rasterizer.h"
|
||||||
|
@ -115,7 +116,7 @@ RendererOpenGL::~RendererOpenGL() = default;
|
||||||
void RendererOpenGL::SwapBuffers(boost::optional<const Tegra::FramebufferConfig&> framebuffer) {
|
void RendererOpenGL::SwapBuffers(boost::optional<const Tegra::FramebufferConfig&> framebuffer) {
|
||||||
ScopeAcquireGLContext acquire_context{render_window};
|
ScopeAcquireGLContext acquire_context{render_window};
|
||||||
|
|
||||||
Core::System::GetInstance().perf_stats.EndSystemFrame();
|
Core::System::GetInstance().GetPerfStats().EndSystemFrame();
|
||||||
|
|
||||||
// Maintain the rasterizer's state as a priority
|
// Maintain the rasterizer's state as a priority
|
||||||
OpenGLState prev_state = OpenGLState::GetCurState();
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
|
@ -140,8 +141,8 @@ void RendererOpenGL::SwapBuffers(boost::optional<const Tegra::FramebufferConfig&
|
||||||
|
|
||||||
render_window.PollEvents();
|
render_window.PollEvents();
|
||||||
|
|
||||||
Core::System::GetInstance().frame_limiter.DoFrameLimiting(CoreTiming::GetGlobalTimeUs());
|
Core::System::GetInstance().FrameLimiter().DoFrameLimiting(CoreTiming::GetGlobalTimeUs());
|
||||||
Core::System::GetInstance().perf_stats.BeginSystemFrame();
|
Core::System::GetInstance().GetPerfStats().BeginSystemFrame();
|
||||||
|
|
||||||
// Restore the rasterizer state
|
// Restore the rasterizer state
|
||||||
prev_state.Apply();
|
prev_state.Apply();
|
||||||
|
|
Loading…
Reference in New Issue