diff --git a/src/common/IPCHybrid.hpp b/src/common/IPCHybrid.hpp index 079a1baaf..a0a1ec215 100644 --- a/src/common/IPCHybrid.hpp +++ b/src/common/IPCHybrid.hpp @@ -49,8 +49,9 @@ void ipc_send_gui_update(IPC_UPDATE_GUI command, const unsigned int value); // ****************************************************************** typedef enum class _IPC_UPDATE_KERNEL { - CONFIG_LOGGING_SYNC = 0 - , CONFIG_INPUT_SYNC + CONFIG_LOGGING_SYNC = 0, + CONFIG_INPUT_SYNC, + CONFIG_CHANGE_TIME } IPC_UPDATE_KERNEL; void ipc_send_kernel_update(IPC_UPDATE_KERNEL command, const int value, const unsigned int hwnd); diff --git a/src/common/Timer.cpp b/src/common/Timer.cpp index c9515856c..422fb32a5 100644 --- a/src/common/Timer.cpp +++ b/src/common/Timer.cpp @@ -25,19 +25,45 @@ // * // ****************************************************************** -#ifdef _WIN32 +#include + #include -#endif #include #include #include +#include #include "Timer.h" #include "common\util\CxbxUtil.h" #include "core\kernel\support\EmuFS.h" -#include "core/kernel/exports/EmuKrnlPs.hpp" -#ifdef __linux__ -#include -#endif +#include "core\kernel\exports\EmuKrnlPs.hpp" +#include "core\kernel\exports\EmuKrnl.h" +#include "devices\Xbox.h" +#include "devices\usb\OHCI.h" +#include "core\hle\DSOUND\DirectSound\DirectSoundGlobal.hpp" + + +static std::atomic_uint64_t last_qpc; // last time when QPC was called +static std::atomic_uint64_t exec_time; // total execution time in us since the emulation started +static uint64_t pit_last; // last time when the pit time was updated +static uint64_t pit_last_qpc; // last QPC time of the pit +// The frequency of the high resolution clock of the host, and the start time +int64_t HostQPCFrequency, HostQPCStartTime; + + +void timer_init() +{ + QueryPerformanceFrequency(reinterpret_cast(&HostQPCFrequency)); + QueryPerformanceCounter(reinterpret_cast(&HostQPCStartTime)); + pit_last_qpc = last_qpc = HostQPCStartTime; + pit_last = get_now(); + + // Synchronize xbox system time with host time + LARGE_INTEGER HostSystemTime; + GetSystemTimeAsFileTime((LPFILETIME)&HostSystemTime); + xbox::KeSystemTime.High2Time = HostSystemTime.u.HighPart; + xbox::KeSystemTime.LowPart = HostSystemTime.u.LowPart; + xbox::KeSystemTime.High1Time = HostSystemTime.u.HighPart; +} // More precise sleep, but with increased CPU usage void SleepPrecise(std::chrono::steady_clock::time_point targetTime) @@ -69,174 +95,83 @@ void SleepPrecise(std::chrono::steady_clock::time_point targetTime) } } -// Virtual clocks will probably become useful once LLE CPU is implemented, but for now we don't need them. -// See the QEMUClockType QEMU_CLOCK_VIRTUAL of XQEMU for more info. -#define CLOCK_REALTIME 0 -//#define CLOCK_VIRTUALTIME 1 - - -// Vector storing all the timers created -static std::vector TimerList; -// The frequency of the high resolution clock of the host, and the start time -int64_t HostQPCFrequency, HostQPCStartTime; -// Lock to acquire when accessing TimerList -std::mutex TimerMtx; - - -// Returns the current time of the timer -uint64_t GetTime_NS(TimerObject* Timer) +// NOTE: the pit device is not implemented right now, so we put this here +static uint64_t pit_next(uint64_t now) { -#ifdef _WIN32 - uint64_t Ret = Timer_GetScaledPerformanceCounter(SCALE_S_IN_NS); -#elif __linux__ - static struct timespec ts; - clock_gettime(CLOCK_MONOTONIC_RAW, &ts); - uint64_t Ret = Muldiv64(ts.tv_sec, SCALE_S_IN_NS, 1) + ts.tv_nsec; -#else -#error "Unsupported OS" -#endif - return Ret; + constexpr uint64_t pit_period = 1000; + uint64_t next = pit_last + pit_period; + + if (now >= next) { + xbox::KiClockIsr(now - pit_last); + pit_last = get_now(); + return pit_period; + } + + return pit_last + pit_period - now; // time remaining until next clock interrupt } -// Calculates the next expire time of the timer -static inline uint64_t GetNextExpireTime(TimerObject* Timer) +static void update_non_periodic_events() { - return GetTime_NS(Timer) + Timer->ExpireTime_MS.load(); + // update dsound + dsound_worker(); + + // check for hw interrupts + for (int i = 0; i < MAX_BUS_INTERRUPT_LEVEL; i++) { + // If the interrupt is pending and connected, process it + if (g_bEnableAllInterrupts && HalSystemInterrupts[i].IsPending() && EmuInterruptList[i] && EmuInterruptList[i]->Connected) { + HalSystemInterrupts[i].Trigger(EmuInterruptList[i]); + } + } } -// Deallocates the memory of the timer -void Timer_Destroy(TimerObject* Timer) +uint64_t get_now() { - unsigned int index, i; - std::lock_guardlock(TimerMtx); - - index = TimerList.size(); - for (i = 0; i < index; i++) { - if (Timer == TimerList[i]) { - index = i; - } - } - - assert(index != TimerList.size()); - delete Timer; - TimerList.erase(TimerList.begin() + index); + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + uint64_t elapsed_us = now.QuadPart - last_qpc; + last_qpc = now.QuadPart; + elapsed_us *= 1000000; + elapsed_us /= HostQPCFrequency; + exec_time += elapsed_us; + return exec_time; } -void Timer_Shutdown() +static uint64_t get_next(uint64_t now) { - unsigned i, iXboxThreads = 0; - TimerMtx.lock(); - - for (i = 0; i < TimerList.size(); i++) { - TimerObject* Timer = TimerList[i]; - // We only need to terminate host threads. - if (!Timer->IsXboxTimer) { - Timer_Exit(Timer); - } - // If the thread is xbox, we need to increment for while statement check - else { - iXboxThreads++; - } - } - - // Only perform wait for host threads, otherwise xbox threads are - // already handled within xbox kernel for shutdown process. See CxbxrKrnlSuspendThreads function. - int counter = 0; - while (iXboxThreads != TimerList.size()) { - if (counter >= 8) { - break; - } - TimerMtx.unlock(); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - TimerMtx.lock(); - counter++; - } - TimerList.clear(); - TimerMtx.unlock(); + std::array next = { + pit_next(now), + g_NV2A->vblank_next(now), + g_NV2A->ptimer_next(now), + g_USB0->m_HostController->OHCI_next(now), + dsound_next(now) + }; + return *std::min_element(next.begin(), next.end()); } -// Thread that runs the timer -void NTAPI ClockThread(void *TimerArg) +xbox::void_xt NTAPI system_events(xbox::PVOID arg) { - TimerObject *Timer = static_cast(TimerArg); - if (!Timer->Name.empty()) { - CxbxSetThreadName(Timer->Name.c_str()); - } - if (!Timer->IsXboxTimer) { - g_AffinityPolicy->SetAffinityOther(); - } + // Testing shows that, if this thread has the same priority of the other xbox threads, it can take tens, even hundreds of ms to complete a single loop. + // So we increase its priority to above normal, so that it scheduled more often + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); - uint64_t NewExpireTime = GetNextExpireTime(Timer); + // Always run this thread at dpc level to prevent it from ever executing APCs/DPCs + xbox::KeRaiseIrqlToDpcLevel(); while (true) { - if (GetTime_NS(Timer) > NewExpireTime) { - if (Timer->Exit.load()) { - Timer_Destroy(Timer); - return; + const uint64_t last_time = get_now(); + const uint64_t nearest_next = get_next(last_time); + + while (true) { + update_non_periodic_events(); + uint64_t elapsed_us = get_now() - last_time; + if (elapsed_us >= nearest_next) { + break; } - Timer->Callback(Timer->Opaque); - NewExpireTime = GetNextExpireTime(Timer); + std::this_thread::yield(); } - Sleep(1); // prevent burning the cpu } } -// Changes the expire time of a timer -void Timer_ChangeExpireTime(TimerObject* Timer, uint64_t Expire_ms) -{ - Timer->ExpireTime_MS.store(Expire_ms); -} - -// Destroys the timer -void Timer_Exit(TimerObject* Timer) -{ - Timer->Exit.store(true); -} - -// Allocates the memory for the timer object -TimerObject* Timer_Create(TimerCB Callback, void* Arg, std::string Name, bool IsXboxTimer) -{ - std::lock_guardlock(TimerMtx); - TimerObject* pTimer = new TimerObject; - pTimer->Type = CLOCK_REALTIME; - pTimer->Callback = Callback; - pTimer->ExpireTime_MS.store(0); - pTimer->Exit.store(false); - pTimer->Opaque = Arg; - pTimer->Name = Name.empty() ? "Unnamed thread" : std::move(Name); - pTimer->IsXboxTimer = IsXboxTimer; - TimerList.emplace_back(pTimer); - - return pTimer; -} - -// Starts the timer -// Expire_MS must be expressed in NS -void Timer_Start(TimerObject* Timer, uint64_t Expire_MS) -{ - Timer->ExpireTime_MS.store(Expire_MS); - if (Timer->IsXboxTimer) { - xbox::HANDLE hThread; - CxbxrCreateThread(&hThread, xbox::zeroptr, ClockThread, Timer, FALSE); - } - else { - std::thread(ClockThread, Timer).detach(); - } -} - -// Retrives the frequency of the high resolution clock of the host -void Timer_Init() -{ -#ifdef _WIN32 - QueryPerformanceFrequency(reinterpret_cast(&HostQPCFrequency)); - QueryPerformanceCounter(reinterpret_cast(&HostQPCStartTime)); -#elif __linux__ - ClockFrequency = 0; -#else -#error "Unsupported OS" -#endif -} - int64_t Timer_GetScaledPerformanceCounter(int64_t Period) { LARGE_INTEGER currentQPC; diff --git a/src/common/Timer.h b/src/common/Timer.h index 007c75536..5096c3ff3 100644 --- a/src/common/Timer.h +++ b/src/common/Timer.h @@ -40,33 +40,12 @@ #define SCALE_MS_IN_US 1000 #define SCALE_US_IN_US 1 -/* typedef of the timer object and the callback function */ -typedef void(*TimerCB)(void*); -typedef struct _TimerObject -{ - int Type; // timer type - std::atomic_uint64_t ExpireTime_MS; // when the timer expires (ms) - std::atomic_bool Exit; // indicates that the timer should be destroyed - TimerCB Callback; // function to call when the timer expires - void* Opaque; // opaque argument to pass to the callback - std::string Name; // the name of the timer thread (if any) - bool IsXboxTimer; // indicates that the timer should run on the Xbox CPU -} -TimerObject; extern int64_t HostQPCFrequency; -/* Timer exported functions */ -TimerObject* Timer_Create(TimerCB Callback, void* Arg, std::string Name, bool IsXboxTimer); -void Timer_Start(TimerObject* Timer, uint64_t Expire_MS); -void Timer_Exit(TimerObject* Timer); -void Timer_ChangeExpireTime(TimerObject* Timer, uint64_t Expire_ms); -uint64_t GetTime_NS(TimerObject* Timer); -void Timer_Init(); -void Timer_Shutdown(); - +void timer_init(); +uint64_t get_now(); int64_t Timer_GetScaledPerformanceCounter(int64_t Period); - void SleepPrecise(std::chrono::steady_clock::time_point targetTime); #endif diff --git a/src/common/cxbxr.cpp b/src/common/cxbxr.cpp index 50e651069..37042e4cc 100644 --- a/src/common/cxbxr.cpp +++ b/src/common/cxbxr.cpp @@ -121,9 +121,6 @@ bool HandleFirstLaunch() } // NOTE: Require to be after g_renderbase's shutdown process. - // Next thing we need to do is shutdown our timer threads. - Timer_Shutdown(); - // NOTE: Must be last step of shutdown process and before CxbxUnlockFilePath call! // Shutdown the memory manager g_VMManager.Shutdown(); diff --git a/src/common/win32/IPCWindows.cpp b/src/common/win32/IPCWindows.cpp index 82a342d89..cbf04f2c7 100644 --- a/src/common/win32/IPCWindows.cpp +++ b/src/common/win32/IPCWindows.cpp @@ -101,6 +101,10 @@ void ipc_send_kernel_update(IPC_UPDATE_KERNEL command, const int value, const un cmdParam = ID_SYNC_CONFIG_INPUT; break; + case IPC_UPDATE_KERNEL::CONFIG_CHANGE_TIME: + cmdParam = ID_SYNC_TIME_CHANGE; + break; + default: cmdParam = 0; break; diff --git a/src/common/xbe/Xbe.cpp b/src/common/xbe/Xbe.cpp index 36b93ab5c..3f9c07fe3 100644 --- a/src/common/xbe/Xbe.cpp +++ b/src/common/xbe/Xbe.cpp @@ -72,7 +72,7 @@ Xbe::Xbe(const char *x_szFilename) // This is necessary because CxbxInitWindow internally calls g_AffinityPolicy->SetAffinityOther. If we are launched directly from the command line and the dashboard // cannot be opened, we will crash below because g_AffinityPolicy will be empty g_AffinityPolicy = AffinityPolicy::InitPolicy(); - CxbxInitWindow(false); + CxbxInitWindow(); ULONG FatalErrorCode = FATAL_ERROR_XBE_DASH_GENERIC; diff --git a/src/core/hle/D3D8/Direct3D9/Direct3D9.cpp b/src/core/hle/D3D8/Direct3D9/Direct3D9.cpp index ac58da250..174caede8 100644 --- a/src/core/hle/D3D8/Direct3D9/Direct3D9.cpp +++ b/src/core/hle/D3D8/Direct3D9/Direct3D9.cpp @@ -38,6 +38,7 @@ #include "core\kernel\support\Emu.h" #include "core\kernel\support\EmuFS.h" #include "core\kernel\support\NativeHandle.h" +#include "core\kernel\exports\EmuKrnlKe.h" #include "EmuShared.h" #include "..\FixedFunctionState.h" #include "core\hle\D3D8\ResourceTracker.h" @@ -160,8 +161,6 @@ static IDirect3DQuery *g_pHostQueryWaitForIdle = nullptr; static IDirect3DQuery *g_pHostQueryCallbackEvent = nullptr; static int g_RenderUpscaleFactor = 1; -static std::condition_variable g_VBConditionVariable; // Used in BlockUntilVerticalBlank -static std::mutex g_VBConditionMutex; // Used in BlockUntilVerticalBlank static DWORD g_VBLastSwap = 0; static xbox::dword_xt g_Xbox_PresentationInterval_Default = D3DPRESENT_INTERVAL_IMMEDIATE; @@ -169,7 +168,6 @@ static xbox::dword_xt g_Xbox_PresentationInterval_Default = D3 static xbox::X_D3DSWAPDATA g_Xbox_SwapData = {0}; // current swap information static xbox::X_D3DSWAPCALLBACK g_pXbox_SwapCallback = xbox::zeroptr; // Swap/Present callback routine static xbox::X_D3DVBLANKDATA g_Xbox_VBlankData = {0}; // current vertical blank information -static xbox::X_D3DVBLANKCALLBACK g_pXbox_VerticalBlankCallback = xbox::zeroptr; // Vertical-Blank callback routine xbox::X_D3DSurface *g_pXbox_BackBufferSurface = xbox::zeroptr; static xbox::X_D3DSurface *g_pXbox_DefaultDepthStencilSurface = xbox::zeroptr; @@ -229,7 +227,6 @@ static xbox::dword_xt *g_Xbox_D3DDevice; // TODO: This should b // Static Function(s) static DWORD WINAPI EmuRenderWindow(LPVOID); static LRESULT WINAPI EmuMsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); -static xbox::void_xt NTAPI EmuUpdateTickCount(xbox::PVOID Arg); static inline void EmuVerifyResourceIsRegistered(xbox::X_D3DResource *pResource, DWORD D3DUsage, int iTextureStage, DWORD dwSize); static void UpdateCurrentMSpFAndFPS(); // Used for benchmarking/fps count static void CxbxImpl_SetRenderTarget(xbox::X_D3DSurface *pRenderTarget, xbox::X_D3DSurface *pNewZStencil); @@ -629,25 +626,13 @@ const char *D3DErrorString(HRESULT hResult) return buffer; } -void CxbxInitWindow(bool bFullInit) +void CxbxInitWindow() { g_EmuShared->GetVideoSettings(&g_XBVideo); if(g_XBVideo.bFullScreen) CxbxKrnl_hEmuParent = NULL; - // create timing thread - if (bFullInit && !bLLE_GPU) - { - xbox::HANDLE hThread; - xbox::PsCreateSystemThread(&hThread, xbox::zeroptr, EmuUpdateTickCount, xbox::zeroptr, FALSE); - // We set the priority of this thread a bit higher, to assure reliable timing : - auto nativeHandle = GetNativeHandle(hThread); - assert(nativeHandle); - SetThreadPriority(*nativeHandle, THREAD_PRIORITY_ABOVE_NORMAL); - g_AffinityPolicy->SetAffinityOther(*nativeHandle); - } - /* TODO : Port this Dxbx code : // create vblank handling thread { @@ -1706,6 +1691,13 @@ static LRESULT WINAPI EmuMsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lPar } break; + case ID_SYNC_TIME_CHANGE: + { + // Sent by the GUI when it detects WM_TIMECHANGE + xbox::KeSystemTimeChanged.test_and_set(); + } + break; + default: break; } @@ -1723,6 +1715,14 @@ static LRESULT WINAPI EmuMsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lPar } break; + case WM_TIMECHANGE: + { + // NOTE: this is only received if the loader was launched from the command line without the GUI + xbox::KeSystemTimeChanged.test_and_set(); + return DefWindowProc(hWnd, msg, wParam, lParam); + } + break; + case WM_SYSKEYDOWN: { if(wParam == VK_RETURN) @@ -1930,47 +1930,25 @@ std::chrono::steady_clock::time_point GetNextVBlankTime() return steady_clock::now() + duration_cast(ms); } -// timing thread procedure -static xbox::void_xt NTAPI EmuUpdateTickCount(xbox::PVOID Arg) +void hle_vblank() { - CxbxSetThreadName("Cxbx Timing Thread"); + // Note: This whole code block can be removed once NV2A interrupts are implemented + // And Both Swap and Present can be ran unpatched + // Once that is in place, MiniPort + Direct3D will handle this on it's own! + g_Xbox_VBlankData.VBlank++; - EmuLog(LOG_LEVEL::DEBUG, "Timing thread is running."); + // TODO: Fixme. This may not be right... + g_Xbox_SwapData.SwapVBlank = 1; - auto nextVBlankTime = GetNextVBlankTime(); + g_Xbox_VBlankData.Swap = 0; - while(true) - { - // Wait for VBlank - // Note: This whole code block can be removed once NV2A interrupts are implemented - // And Both Swap and Present can be ran unpatched - // Once that is in place, MiniPort + Direct3D will handle this on it's own! - SleepPrecise(nextVBlankTime); - nextVBlankTime = GetNextVBlankTime(); + // TODO: This can't be accurate... + g_Xbox_SwapData.TimeUntilSwapVBlank = 0; - // Increment the VBlank Counter and Wake all threads there were waiting for the VBlank to occur - std::unique_lock lk(g_VBConditionMutex); - g_Xbox_VBlankData.VBlank++; - g_VBConditionVariable.notify_all(); - - // TODO: Fixme. This may not be right... - g_Xbox_SwapData.SwapVBlank = 1; - - if(g_pXbox_VerticalBlankCallback != xbox::zeroptr) - { - g_pXbox_VerticalBlankCallback(&g_Xbox_VBlankData); - } - - g_Xbox_VBlankData.Swap = 0; - - // TODO: This can't be accurate... - g_Xbox_SwapData.TimeUntilSwapVBlank = 0; - - // TODO: Recalculate this for PAL version if necessary. - // Also, we should check the D3DPRESENT_INTERVAL value for accurracy. - // g_Xbox_SwapData.TimeBetweenSwapVBlanks = 1/60; - g_Xbox_SwapData.TimeBetweenSwapVBlanks = 0; - } + // TODO: Recalculate this for PAL version if necessary. + // Also, we should check the D3DPRESENT_INTERVAL value for accurracy. + // g_Xbox_SwapData.TimeBetweenSwapVBlanks = 1/60; + g_Xbox_SwapData.TimeBetweenSwapVBlanks = 0; } void UpdateDepthStencilFlags(IDirect3DSurface *pDepthStencilSurface) @@ -6541,31 +6519,6 @@ xbox::bool_xt WINAPI xbox::EMUPATCH(D3DDevice_GetOverlayUpdateStatus)() return TRUE; } -// ****************************************************************** -// * patch: D3DDevice_BlockUntilVerticalBlank -// ****************************************************************** -xbox::void_xt WINAPI xbox::EMUPATCH(D3DDevice_BlockUntilVerticalBlank)() -{ - LOG_FUNC(); - - std::unique_lock lk(g_VBConditionMutex); - g_VBConditionVariable.wait(lk); -} - -// ****************************************************************** -// * patch: D3DDevice_SetVerticalBlankCallback -// ****************************************************************** -xbox::void_xt WINAPI xbox::EMUPATCH(D3DDevice_SetVerticalBlankCallback) -( - X_D3DVBLANKCALLBACK pCallback -) -{ - LOG_FUNC_ONE_ARG(pCallback); - - g_pXbox_VerticalBlankCallback = pCallback; -} - - // ****************************************************************** // * patch: D3DDevice_SetRenderState_Simple // ****************************************************************** diff --git a/src/core/hle/D3D8/Direct3D9/Direct3D9.cpp.unused-patches b/src/core/hle/D3D8/Direct3D9/Direct3D9.cpp.unused-patches index 457813b46..0783f0ae2 100644 --- a/src/core/hle/D3D8/Direct3D9/Direct3D9.cpp.unused-patches +++ b/src/core/hle/D3D8/Direct3D9/Direct3D9.cpp.unused-patches @@ -4256,3 +4256,27 @@ HRESULT WINAPI XTL::EMUPATCH(D3DDevice_GetVertexShaderInput) return 0; } + +// ****************************************************************** +// * patch: D3DDevice_BlockUntilVerticalBlank +// ****************************************************************** +xbox::void_xt WINAPI xbox::EMUPATCH(D3DDevice_BlockUntilVerticalBlank)() +{ + LOG_FUNC(); + + std::unique_lock lk(g_VBConditionMutex); + g_VBConditionVariable.wait(lk); +} + +// ****************************************************************** +// * patch: D3DDevice_SetVerticalBlankCallback +// ****************************************************************** +xbox::void_xt WINAPI xbox::EMUPATCH(D3DDevice_SetVerticalBlankCallback) +( + X_D3DVBLANKCALLBACK pCallback +) +{ + LOG_FUNC_ONE_ARG(pCallback); + + g_pXbox_VerticalBlankCallback = pCallback; +} diff --git a/src/core/hle/D3D8/Direct3D9/Direct3D9.h b/src/core/hle/D3D8/Direct3D9/Direct3D9.h index aa13b1dd4..e508ff39a 100644 --- a/src/core/hle/D3D8/Direct3D9/Direct3D9.h +++ b/src/core/hle/D3D8/Direct3D9/Direct3D9.h @@ -40,7 +40,7 @@ void LookupTrampolinesD3D(); // initialize render window -extern void CxbxInitWindow(bool bFullInit); +extern void CxbxInitWindow(); void CxbxUpdateNativeD3DResources(); diff --git a/src/core/hle/DSOUND/DirectSound/DirectSound.cpp b/src/core/hle/DSOUND/DirectSound/DirectSound.cpp index f87daaad1..4cd5b087a 100644 --- a/src/core/hle/DSOUND/DirectSound/DirectSound.cpp +++ b/src/core/hle/DSOUND/DirectSound/DirectSound.cpp @@ -53,6 +53,7 @@ // TODO: Move these to LLE APUDevice once we have one! static constexpr uint32_t APU_TIMER_FREQUENCY = 48000; +static uint64_t dsound_last; uint32_t GetAPUTime() { @@ -85,10 +86,6 @@ uint32_t GetAPUTime() there is chance of failure which contain value greater than 0. */ -// Managed memory xbox audio variables -static std::thread dsound_thread; -static void dsound_thread_worker(LPVOID); - #include "DirectSoundInline.hpp" #ifdef __cplusplus @@ -98,6 +95,7 @@ extern "C" { void CxbxInitAudio() { g_EmuShared->GetAudioSettings(&g_XBAudio); + dsound_last = get_now(); } #ifdef __cplusplus @@ -125,10 +123,6 @@ xbox::hresult_xt WINAPI xbox::EMUPATCH(DirectSoundCreate) HRESULT hRet = DS_OK; - if (!initialized) { - dsound_thread = std::thread(dsound_thread_worker, nullptr); - } - // Set this flag when this function is called g_bDSoundCreateCalled = TRUE; @@ -454,51 +448,51 @@ void StreamBufferAudio(xbox::XbHybridDSBuffer* pHybridBuffer, float msToCopy) { } } -static void dsound_thread_worker(LPVOID nullPtr) +void dsound_async_worker() { - g_AffinityPolicy->SetAffinityOther(); + DSoundMutexGuardLock; - const int dsStreamInterval = 300; - int waitCounter = 0; + xbox::LARGE_INTEGER getTime; + xbox::KeQuerySystemTime(&getTime); + DirectSoundDoWork_Stream(getTime); +} - while (true) { - // FIXME time this loop more accurately - // and account for variation in the length of Sleep calls +void dsound_worker() +{ + // Testcase: Gauntlet Dark Legacy, if Sleep(1) then intro videos start to starved often + // unless console is open with logging enabled. This is the cause of stopping intro videos often. - // Testcase: Gauntlet Dark Legacy, if Sleep(1) then intro videos start to starved often - // unless console is open with logging enabled. This is the cause of stopping intro videos often. - Sleep(g_dsBufferStreaming.streamInterval); - waitCounter += g_dsBufferStreaming.streamInterval; + // Enforce mutex guard lock only occur inside below bracket for proper compile build. + DSoundMutexGuardLock; - // Enforce mutex guard lock only occur inside below bracket for proper compile build. - { - DSoundMutexGuardLock; - - if (waitCounter > dsStreamInterval) { - waitCounter = 0; - - // For Async process purpose only - xbox::LARGE_INTEGER getTime; - xbox::KeQuerySystemTime(&getTime); - DirectSoundDoWork_Stream(getTime); + // Stream sound buffer audio + // because the title may change the content of sound buffers at any time + for (auto& pBuffer : g_pDSoundBufferCache) { + // Avoid expensive calls to DirectSound on buffers unless they've been played at least once + // Since some titles create a large amount of buffers, but only use a few + if (pBuffer->emuDSBuffer->EmuStreamingInfo.playRequested) { + DWORD status; + HRESULT hRet = pBuffer->emuDSBuffer->EmuDirectSoundBuffer8->GetStatus(&status); + if (hRet == 0 && status & DSBSTATUS_PLAYING) { + auto streamMs = g_dsBufferStreaming.streamInterval + g_dsBufferStreaming.streamAhead; + StreamBufferAudio(pBuffer, streamMs); } + } + } +} - // Stream sound buffer audio - // because the title may change the content of sound buffers at any time - for (auto& pBuffer : g_pDSoundBufferCache) { - // Avoid expensive calls to DirectSound on buffers unless they've been played at least once - // Since some titles create a large amount of buffers, but only use a few - if (pBuffer->emuDSBuffer->EmuStreamingInfo.playRequested) { - DWORD status; - HRESULT hRet = pBuffer->emuDSBuffer->EmuDirectSoundBuffer8->GetStatus(&status); - if (hRet == 0 && status & DSBSTATUS_PLAYING) { - auto streamMs = g_dsBufferStreaming.streamInterval + g_dsBufferStreaming.streamAhead; - StreamBufferAudio(pBuffer, streamMs); - } - } - } - } +uint64_t dsound_next(uint64_t now) +{ + constexpr uint64_t dsound_period = 300 * 1000; + uint64_t next = dsound_last + dsound_period; + + if (now >= next) { + dsound_async_worker(); + dsound_last = get_now(); + return dsound_period; } + + return dsound_last + dsound_period - now; // time remaining until next dsound async event } // Kismet given name for RadWolfie's experiment major issue in the mutt. diff --git a/src/core/hle/DSOUND/DirectSound/DirectSoundGlobal.hpp b/src/core/hle/DSOUND/DirectSound/DirectSoundGlobal.hpp index 23e650ed5..289a92da3 100644 --- a/src/core/hle/DSOUND/DirectSound/DirectSoundGlobal.hpp +++ b/src/core/hle/DSOUND/DirectSound/DirectSoundGlobal.hpp @@ -81,3 +81,6 @@ extern DsBufferStreaming g_dsBufferStreaming; extern void DirectSoundDoWork_Buffer(xbox::LARGE_INTEGER& time); extern void DirectSoundDoWork_Stream(xbox::LARGE_INTEGER& time); +extern void dsound_async_worker(); +extern void dsound_worker(); +extern uint64_t dsound_next(uint64_t now); diff --git a/src/core/hle/Patches.cpp b/src/core/hle/Patches.cpp index 236b4c9d2..d4ccf44cc 100644 --- a/src/core/hle/Patches.cpp +++ b/src/core/hle/Patches.cpp @@ -66,7 +66,7 @@ std::map g_PatchTable = { PATCH_ENTRY("D3DDevice_BeginPush_8", xbox::EMUPATCH(D3DDevice_BeginPush_8), PATCH_HLE_D3D), PATCH_ENTRY("D3DDevice_BeginVisibilityTest", xbox::EMUPATCH(D3DDevice_BeginVisibilityTest), PATCH_HLE_D3D), PATCH_ENTRY("D3DDevice_BlockOnFence", xbox::EMUPATCH(D3DDevice_BlockOnFence), PATCH_HLE_D3D), - PATCH_ENTRY("D3DDevice_BlockUntilVerticalBlank", xbox::EMUPATCH(D3DDevice_BlockUntilVerticalBlank), PATCH_HLE_D3D), + //PATCH_ENTRY("D3DDevice_BlockUntilVerticalBlank", xbox::EMUPATCH(D3DDevice_BlockUntilVerticalBlank), PATCH_HLE_D3D), PATCH_ENTRY("D3DDevice_Clear", xbox::EMUPATCH(D3DDevice_Clear), PATCH_HLE_D3D), PATCH_ENTRY("D3DDevice_CopyRects", xbox::EMUPATCH(D3DDevice_CopyRects), PATCH_HLE_D3D), // PATCH_ENTRY("D3DDevice_CreateVertexShader", xbox::EMUPATCH(D3DDevice_CreateVertexShader), PATCH_HLE_D3D), @@ -183,7 +183,7 @@ std::map g_PatchTable = { PATCH_ENTRY("D3DDevice_SetVertexShaderConstant_8", xbox::EMUPATCH(D3DDevice_SetVertexShaderConstant_8), PATCH_HLE_D3D), PATCH_ENTRY("D3DDevice_SetVertexShaderInput", xbox::EMUPATCH(D3DDevice_SetVertexShaderInput), PATCH_HLE_D3D), //PATCH_ENTRY("D3DDevice_SetVertexShaderInputDirect", xbox::EMUPATCH(D3DDevice_SetVertexShaderInputDirect), PATCH_HLE_D3D), - PATCH_ENTRY("D3DDevice_SetVerticalBlankCallback", xbox::EMUPATCH(D3DDevice_SetVerticalBlankCallback), PATCH_HLE_D3D), + //PATCH_ENTRY("D3DDevice_SetVerticalBlankCallback", xbox::EMUPATCH(D3DDevice_SetVerticalBlankCallback), PATCH_HLE_D3D), PATCH_ENTRY("D3DDevice_SetViewport", xbox::EMUPATCH(D3DDevice_SetViewport), PATCH_HLE_D3D), PATCH_ENTRY("D3DDevice_Swap", xbox::EMUPATCH(D3DDevice_Swap), PATCH_HLE_D3D), PATCH_ENTRY("D3DDevice_Swap_0", xbox::EMUPATCH(D3DDevice_Swap_0), PATCH_HLE_D3D), diff --git a/src/core/hle/XAPI/Xapi.cpp b/src/core/hle/XAPI/Xapi.cpp index a17c1682f..429a7c610 100644 --- a/src/core/hle/XAPI/Xapi.cpp +++ b/src/core/hle/XAPI/Xapi.cpp @@ -960,15 +960,41 @@ xbox::dword_xt WINAPI xbox::EMUPATCH(SignalObjectAndWait) NewTime.QuadPart += (static_cast(dwMilliseconds) * CLOCK_TIME_INCREMENT); } - xbox::dword_xt ret = WaitApc([hObjectToSignal, hObjectToWaitOn, bAlertable]() -> std::optional { + PKTHREAD kThread = KeGetCurrentThread(); + kThread->WaitStatus = X_STATUS_SUCCESS; + if (!AddWaitObject(kThread, Timeout)) { + RETURN(WAIT_TIMEOUT); + } + + xbox::ntstatus_xt status = WaitApc([hObjectToSignal, hObjectToWaitOn, bAlertable](xbox::PKTHREAD kThread) -> std::optional { DWORD dwRet = SignalObjectAndWait(hObjectToSignal, hObjectToWaitOn, 0, bAlertable); if (dwRet == WAIT_TIMEOUT) { return std::nullopt; } - return std::make_optional(dwRet); - }, Timeout, bAlertable, UserMode); + // If the wait was satisfied with the host, then also unwait the thread on the guest side, to be sure to remove WaitBlocks that might have been added + // to the thread + xbox::ntstatus_xt Status; + switch (dwRet) + { + case WAIT_ABANDONED: Status = X_STATUS_ABANDONED; break; + case WAIT_IO_COMPLETION: Status = X_STATUS_USER_APC; break; + case WAIT_OBJECT_0: Status = X_STATUS_SUCCESS; break; + default: Status = X_STATUS_INVALID_HANDLE; + } + xbox::KiUnwaitThreadAndLock(kThread, Status, 0); + return std::make_optional(kThread->WaitStatus); + }, Timeout, bAlertable, UserMode, kThread); - RETURN((ret == X_STATUS_USER_APC) ? WAIT_IO_COMPLETION : (ret == X_STATUS_TIMEOUT) ? WAIT_TIMEOUT : ret); + xbox::dword_xt ret; + switch (status) + { + case X_STATUS_ABANDONED: ret = WAIT_ABANDONED; break; + case X_STATUS_USER_APC: ret = WAIT_IO_COMPLETION; break; + case X_STATUS_SUCCESS: ret = WAIT_OBJECT_0; break; + case X_STATUS_TIMEOUT: ret = WAIT_TIMEOUT; break; + default: ret = WAIT_FAILED; + } + RETURN(ret); } // ****************************************************************** diff --git a/src/core/kernel/common/rtl.h b/src/core/kernel/common/rtl.h index 924d11ed8..8af147374 100644 --- a/src/core/kernel/common/rtl.h +++ b/src/core/kernel/common/rtl.h @@ -605,6 +605,9 @@ XBSYSAPI EXPORTNUM(352) void_xt NTAPI RtlRip PCHAR Message ); +void_xt RtlInitSystem(); +extern RTL_CRITICAL_SECTION NtSystemTimeCritSec; + } #endif diff --git a/src/core/kernel/common/types.h b/src/core/kernel/common/types.h index da159c68c..367b29cb4 100644 --- a/src/core/kernel/common/types.h +++ b/src/core/kernel/common/types.h @@ -98,6 +98,8 @@ typedef void* LPSECURITY_ATTRIBUTES; #define X_STATUS_FILE_IS_A_DIRECTORY 0xC00000BAL #define X_STATUS_END_OF_FILE 0xC0000011L #define X_STATUS_INVALID_PAGE_PROTECTION 0xC0000045L +#define X_STATUS_SUSPEND_COUNT_EXCEEDED 0xC000004AL +#define X_STATUS_THREAD_IS_TERMINATING 0xC000004BL #define X_STATUS_CONFLICTING_ADDRESSES 0xC0000018L #define X_STATUS_UNABLE_TO_FREE_VM 0xC000001AL #define X_STATUS_FREE_VM_NOT_AT_BASE 0xC000009FL @@ -1945,7 +1947,7 @@ typedef struct _KTHREAD /* 0x56/86 */ char_xt WaitNext; /* 0x57/87 */ char_xt WaitReason; /* 0x58/88 */ PKWAIT_BLOCK WaitBlockList; - /* 0x5C/92 */ LIST_ENTRY WaitListEntry; + /* 0x5C/92 */ LIST_ENTRY WaitListEntry; // Used to place the thread in the ready list of the scheduler /* 0x64/100 */ ulong_xt WaitTime; /* 0x68/104 */ ulong_xt KernelApcDisable; /* 0x6C/108 */ ulong_xt Quantum; @@ -1969,6 +1971,8 @@ typedef struct _KTHREAD } KTHREAD, *PKTHREAD, *RESTRICTED_POINTER PRKTHREAD; +#define X_MAXIMUM_SUSPEND_COUNT 0x7F + // ****************************************************************** // * ETHREAD // ****************************************************************** diff --git a/src/core/kernel/exports/EmuKrnl.cpp b/src/core/kernel/exports/EmuKrnl.cpp index 6934cd1b5..252a0dac7 100644 --- a/src/core/kernel/exports/EmuKrnl.cpp +++ b/src/core/kernel/exports/EmuKrnl.cpp @@ -85,6 +85,8 @@ void InsertTailList(xbox::PLIST_ENTRY pListHead, xbox::PLIST_ENTRY pEntry) //#define RemoveEntryList(e) do { PLIST_ENTRY f = (e)->Flink, b = (e)->Blink; f->Blink = b; b->Flink = f; (e)->Flink = (e)->Blink = NULL; } while (0) // Returns TRUE if the list has become empty after removing the element, FALSE otherwise. +// NOTE: this function is a mess. _EX_Flink and _EX_Flink should never be nullptr, and it should never be called on a detached element either. Try to fix +// the bugs in the caller instead of trying to handle it here with these hacks xbox::boolean_xt RemoveEntryList(xbox::PLIST_ENTRY pEntry) { xbox::PLIST_ENTRY _EX_Flink = pEntry->Flink; @@ -140,8 +142,6 @@ void RestoreInterruptMode(bool value) g_bInterruptsEnabled = value; } -extern void ExecuteDpcQueue(); - void KiUnexpectedInterrupt() { xbox::KeBugCheck(TRAP_CAUSE_UNKNOWN); // see @@ -178,6 +178,33 @@ void CallSoftwareInterrupt(const xbox::KIRQL SoftwareIrql) HalInterruptRequestRegister ^= (1 << SoftwareIrql); } +bool AddWaitObject(xbox::PKTHREAD kThread, xbox::PLARGE_INTEGER Timeout) +{ + // Use the built-in ktimer as a dummy wait object, so that KiUnwaitThreadAndLock can still work + xbox::KiTimerLock(); + xbox::PKWAIT_BLOCK WaitBlock = &kThread->TimerWaitBlock; + kThread->WaitBlockList = WaitBlock; + xbox::PKTIMER Timer = &kThread->Timer; + WaitBlock->NextWaitBlock = WaitBlock; + Timer->Header.WaitListHead.Flink = &WaitBlock->WaitListEntry; + Timer->Header.WaitListHead.Blink = &WaitBlock->WaitListEntry; + if (Timeout && Timeout->QuadPart) { + // Setup a timer so that KiTimerExpiration can discover the timeout and yield to us. + // Otherwise, we will only be able to discover the timeout when Windows decides to schedule us again, and testing shows that + // tends to happen much later than the due time + if (xbox::KiInsertTreeTimer(Timer, *Timeout) == FALSE) { + // Sanity check: set WaitBlockList to nullptr so that we can catch the case where a waiter starts a new wait but forgets to setup a new wait block. This + // way, we will crash instead of silently using the pointer to the old block + kThread->WaitBlockList = xbox::zeroptr; + xbox::KiTimerUnlock(); + return false; + } + } + kThread->State = xbox::Waiting; + xbox::KiTimerUnlock(); + return true; +} + // This masks have been verified to be correct against a kernel dump const DWORD IrqlMasks[] = { 0xFFFFFFFE, // IRQL 0 diff --git a/src/core/kernel/exports/EmuKrnl.h b/src/core/kernel/exports/EmuKrnl.h index 0b8a691d3..17e458b68 100644 --- a/src/core/kernel/exports/EmuKrnl.h +++ b/src/core/kernel/exports/EmuKrnl.h @@ -52,8 +52,8 @@ xbox::PLIST_ENTRY RemoveTailList(xbox::PLIST_ENTRY pListHead); extern xbox::LAUNCH_DATA_PAGE DefaultLaunchDataPage; extern xbox::PKINTERRUPT EmuInterruptList[MAX_BUS_INTERRUPT_LEVEL + 1]; -inline std::condition_variable g_InterruptSignal; -inline std::atomic_bool g_AnyInterruptAsserted = false; +// Indicates to disable/enable all interrupts when cli and sti instructions are executed +inline std::atomic_bool g_bEnableAllInterrupts = true; class HalSystemInterrupt { public: @@ -64,8 +64,6 @@ public: } m_Asserted = state; - g_AnyInterruptAsserted = true; - g_InterruptSignal.notify_one(); }; void Enable() { @@ -110,17 +108,18 @@ extern HalSystemInterrupt HalSystemInterrupts[MAX_BUS_INTERRUPT_LEVEL + 1]; bool DisableInterrupts(); void RestoreInterruptMode(bool value); void CallSoftwareInterrupt(const xbox::KIRQL SoftwareIrql); +bool AddWaitObject(xbox::PKTHREAD kThread, xbox::PLARGE_INTEGER Timeout); template -std::optional SatisfyWait(T &&Lambda, xbox::PETHREAD eThread, xbox::boolean_xt Alertable, xbox::char_xt WaitMode) +std::optional SatisfyWait(T &&Lambda, xbox::PKTHREAD kThread, xbox::boolean_xt Alertable, xbox::char_xt WaitMode) { - if (const auto ret = Lambda()) { + if (const auto ret = Lambda(kThread)) { return ret; } xbox::KiApcListMtx.lock(); - bool EmptyKernel = IsListEmpty(&eThread->Tcb.ApcState.ApcListHead[xbox::KernelMode]); - bool EmptyUser = IsListEmpty(&eThread->Tcb.ApcState.ApcListHead[xbox::UserMode]); + bool EmptyKernel = IsListEmpty(&kThread->ApcState.ApcListHead[xbox::KernelMode]); + bool EmptyUser = IsListEmpty(&kThread->ApcState.ApcListHead[xbox::UserMode]); xbox::KiApcListMtx.unlock(); if (EmptyKernel == false) { @@ -131,56 +130,65 @@ std::optional SatisfyWait(T &&Lambda, xbox::PETHREAD eThread, (Alertable == TRUE) && (WaitMode == xbox::UserMode)) { xbox::KiExecuteUserApc(); - return X_STATUS_USER_APC; + xbox::KiUnwaitThreadAndLock(kThread, X_STATUS_USER_APC, 0); + return kThread->WaitStatus; } return std::nullopt; } -template -xbox::ntstatus_xt WaitApc(T &&Lambda, xbox::PLARGE_INTEGER Timeout, xbox::boolean_xt Alertable, xbox::char_xt WaitMode) +template +xbox::ntstatus_xt WaitApc(T &&Lambda, xbox::PLARGE_INTEGER Timeout, xbox::boolean_xt Alertable, xbox::char_xt WaitMode, xbox::PKTHREAD kThread) { - // NOTE: kThread->Alerted is currently never set. When the alerted mechanism is implemented, the alerts should - // also interrupt the wait - - xbox::PETHREAD eThread = reinterpret_cast(EmuKeGetPcr()->Prcb->CurrentThread); + // NOTE1: kThread->Alerted is currently never set. When the alerted mechanism is implemented, the alerts should + // also interrupt the wait. + xbox::ntstatus_xt status; if (Timeout == nullptr) { // No timout specified, so this is an infinite wait until an alert, a user apc or the object(s) become(s) signalled while (true) { - if (const auto ret = SatisfyWait(Lambda, eThread, Alertable, WaitMode)) { - return *ret; + if (const auto ret = SatisfyWait(Lambda, kThread, Alertable, WaitMode)) { + status = *ret; + break; } std::this_thread::yield(); } } else if (Timeout->QuadPart == 0) { + assert(host_wait); // A zero timeout means that we only have to check the conditions once and then return immediately if they are not satisfied - if (const auto ret = SatisfyWait(Lambda, eThread, Alertable, WaitMode)) { - return *ret; + if (const auto ret = SatisfyWait(Lambda, kThread, Alertable, WaitMode)) { + status = *ret; } else { - return X_STATUS_TIMEOUT; + // If the wait failed, then always remove the wait block. Note that this can only happen with host waits, since guest waits never call us at all + // when Timeout->QuadPart == 0. Test case: Halo 2 (sporadically when playing the intro video) + xbox::KiUnwaitThreadAndLock(kThread, X_STATUS_TIMEOUT, 0); + status = kThread->WaitStatus; } } else { // A non-zero timeout means we have to check the conditions until we reach the requested time - xbox::LARGE_INTEGER ExpireTime, DueTime, NewTime; - xbox::ulonglong_xt Now; - ExpireTime.QuadPart = DueTime.QuadPart = Timeout->QuadPart; // either positive, negative, but not NULL - xbox::PLARGE_INTEGER AbsoluteExpireTime = xbox::KiComputeWaitInterval(&ExpireTime, &DueTime, &NewTime, &Now); - while (Now <= static_cast(AbsoluteExpireTime->QuadPart)) { - if (const auto ret = SatisfyWait(Lambda, eThread, Alertable, WaitMode)) { - return *ret; + while (true) { + if (const auto ret = SatisfyWait(Lambda, kThread, Alertable, WaitMode)) { + status = *ret; + break; + } + + if (host_wait && (kThread->State == xbox::Ready)) { + status = kThread->WaitStatus; + break; } std::this_thread::yield(); - Now = xbox::KeQueryInterruptTime(); } - - return X_STATUS_TIMEOUT; } + + if constexpr (host_wait) { + kThread->State = xbox::Running; + } + return status; } #endif diff --git a/src/core/kernel/exports/EmuKrnlKe.cpp b/src/core/kernel/exports/EmuKrnlKe.cpp index 8bbfbc8d5..72ed32a37 100644 --- a/src/core/kernel/exports/EmuKrnlKe.cpp +++ b/src/core/kernel/exports/EmuKrnlKe.cpp @@ -97,10 +97,12 @@ namespace NtDll typedef struct _DpcData { CRITICAL_SECTION Lock; std::atomic_flag IsDpcActive; + std::atomic_flag IsDpcPending; xbox::LIST_ENTRY DpcQueue; // TODO : Use KeGetCurrentPrcb()->DpcListHead instead } DpcData; DpcData g_DpcData = { 0 }; // Note : g_DpcData is initialized in InitDpcData() +std::atomic_flag xbox::KeSystemTimeChanged; xbox::ulonglong_xt LARGE_INTEGER2ULONGLONG(xbox::LARGE_INTEGER value) { @@ -130,6 +132,33 @@ xbox::ulonglong_xt LARGE_INTEGER2ULONGLONG(xbox::LARGE_INTEGER value) break; \ } +xbox::void_xt xbox::KeResumeThreadEx +( + IN PKTHREAD Thread +) +{ + // This is only to be used to synchronize new thread creation with the thread that spawned it + + Thread->SuspendSemaphore.Header.SignalState = 1; + KiWaitListLock(); + KiWaitTest(&Thread->SuspendSemaphore, 0); +} + +xbox::void_xt xbox::KeSuspendThreadEx +( + IN PKTHREAD Thread +) +{ + // This is only to be used to synchronize new thread creation with the thread that spawned it + + Thread->SuspendSemaphore.Header.SignalState = 0; + KiInsertQueueApc(&Thread->SuspendApc, 0); +} + +xbox::void_xt xbox::KeWaitForDpc() +{ + g_DpcData.IsDpcPending.wait(false); +} // ****************************************************************** // * EmuKeGetPcr() @@ -166,7 +195,7 @@ xbox::void_xt NTAPI xbox::KeSetSystemTime ) { KIRQL OldIrql, OldIrql2; - LARGE_INTEGER DeltaTime, HostTime; + LARGE_INTEGER DeltaTime; PLIST_ENTRY ListHead, NextEntry; PKTIMER Timer; LIST_ENTRY TempList, TempList2; @@ -184,10 +213,6 @@ xbox::void_xt NTAPI xbox::KeSetSystemTime /* Query the system time now */ KeQuerySystemTime(OldTime); - /* Surely, we won't set the system time here, but we CAN remember a delta to the host system time */ - HostTime.QuadPart = OldTime->QuadPart - HostSystemTimeDelta.load(); - HostSystemTimeDelta = NewTime->QuadPart - HostTime.QuadPart; - /* Calculate the difference between the new and the old time */ DeltaTime.QuadPart = NewTime->QuadPart - OldTime->QuadPart; @@ -246,7 +271,7 @@ xbox::void_xt NTAPI xbox::KeSetSystemTime } } - /* Process expired timers. This releases the dispatcher and timer locks */ + /* Process expired timers. This releases the dispatcher and timer locks, then it yields */ KiTimerListExpire(&TempList2, OldIrql); } @@ -463,6 +488,7 @@ void ExecuteDpcQueue() g_DpcData.IsDpcActive.test_and_set(); KeGetCurrentPrcb()->DpcRoutineActive = TRUE; // Experimental LeaveCriticalSection(&(g_DpcData.Lock)); + EmuLog(LOG_LEVEL::DEBUG, "Global DpcQueue, calling DPC object 0x%.8X at 0x%.8X", pkdpc, pkdpc->DeferredRoutine); // Call the Deferred Procedure : @@ -477,6 +503,8 @@ void ExecuteDpcQueue() g_DpcData.IsDpcActive.clear(); } + g_DpcData.IsDpcPending.clear(); + // Assert(g_DpcData._dwThreadId == GetCurrentThreadId()); // Assert(g_DpcData._dwDpcThreadId == g_DpcData._dwThreadId); // g_DpcData._dwDpcThreadId = 0; @@ -715,7 +743,13 @@ XBSYSAPI EXPORTNUM(99) xbox::ntstatus_xt NTAPI xbox::KeDelayExecutionThread // We can't remove NtDll::NtDelayExecution until all APCs queued by Io are implemented by our kernel as well // Test case: Metal Slug 3 - xbox::ntstatus_xt ret = WaitApc([Alertable]() -> std::optional { + PKTHREAD kThread = KeGetCurrentThread(); + kThread->WaitStatus = X_STATUS_SUCCESS; + if (!AddWaitObject(kThread, Interval)) { + RETURN(X_STATUS_TIMEOUT); + } + + xbox::ntstatus_xt ret = WaitApc([Alertable](xbox::PKTHREAD kThread) -> std::optional { NtDll::LARGE_INTEGER ExpireTime; ExpireTime.QuadPart = 0; NTSTATUS Status = NtDll::NtDelayExecution(Alertable, &ExpireTime); @@ -723,8 +757,11 @@ XBSYSAPI EXPORTNUM(99) xbox::ntstatus_xt NTAPI xbox::KeDelayExecutionThread if (Status >= 0 && Status != STATUS_ALERTED && Status != STATUS_USER_APC) { return std::nullopt; } - return std::make_optional(Status); - }, Interval, Alertable, WaitMode); + // If the wait was satisfied with the host, then also unwait the thread on the guest side, to be sure to remove WaitBlocks that might have been added + // to the thread. Test case: Steel Battalion + xbox::KiUnwaitThreadAndLock(kThread, Status, 0); + return std::make_optional(kThread->WaitStatus); + }, Interval, Alertable, WaitMode, kThread); if (ret == X_STATUS_TIMEOUT) { // NOTE: this function considers a timeout a success @@ -1206,35 +1243,9 @@ XBSYSAPI EXPORTNUM(118) xbox::boolean_xt NTAPI xbox::KeInsertQueueApc Apc->SystemArgument1 = SystemArgument1; Apc->SystemArgument2 = SystemArgument2; - if (Apc->Inserted) { - KfLowerIrql(OldIrql); - RETURN(FALSE); - } - else { - KiApcListMtx.lock(); - InsertTailList(&kThread->ApcState.ApcListHead[Apc->ApcMode], &Apc->ApcListEntry); - Apc->Inserted = TRUE; - KiApcListMtx.unlock(); - - // We can only attempt to execute the queued apc right away if it is been inserted in the current thread, because otherwise the KTHREAD - // in the fs selector will not be correct - if (kThread == KeGetCurrentThread()) { - if (Apc->ApcMode == KernelMode) { // kernel apc - // NOTE: this is wrong, we should check the thread state instead of just signaling the kernel apc, but we currently - // don't set the appropriate state in kthread - kThread->ApcState.KernelApcPending = TRUE; - KiExecuteKernelApc(); - } - else if ((kThread->WaitMode == UserMode) && (kThread->Alertable)) { // user apc - // NOTE: this should also check the thread state - kThread->ApcState.UserApcPending = TRUE; - KiExecuteUserApc(); - } - } - - KfLowerIrql(OldIrql); - RETURN(TRUE); - } + boolean_xt result = KiInsertQueueApc(Apc, Increment); + KfLowerIrql(OldIrql); + RETURN(result); } } @@ -1266,18 +1277,25 @@ XBSYSAPI EXPORTNUM(119) xbox::boolean_xt NTAPI xbox::KeInsertQueueDpc Dpc->SystemArgument1 = SystemArgument1; Dpc->SystemArgument2 = SystemArgument2; InsertTailList(&(g_DpcData.DpcQueue), &(Dpc->DpcListEntry)); + LeaveCriticalSection(&(g_DpcData.Lock)); + g_DpcData.IsDpcPending.test_and_set(); + g_DpcData.IsDpcPending.notify_one(); + // TODO : Instead of DpcQueue, add the DPC to KeGetCurrentPrcb()->DpcListHead // Signal the Dpc handling code there's work to do if (!IsDpcActive()) { HalRequestSoftwareInterrupt(DISPATCH_LEVEL); } + // OpenXbox has this instead: // if (!pKPRCB->DpcRoutineActive && !pKPRCB->DpcInterruptRequested) { // pKPRCB->DpcInterruptRequested = TRUE; } + else { + LeaveCriticalSection(&(g_DpcData.Lock)); + } // Thread-safety is no longer required anymore - LeaveCriticalSection(&(g_DpcData.Lock)); // TODO : Instead, enable interrupts - use KeLowerIrql(OldIrql) ? RETURN(NeedsInsertion); @@ -1353,13 +1371,14 @@ XBSYSAPI EXPORTNUM(123) xbox::long_xt NTAPI xbox::KePulseEvent } LONG OldState = Event->Header.SignalState; + KiWaitListLock(); if ((OldState == 0) && (IsListEmpty(&Event->Header.WaitListHead) == FALSE)) { Event->Header.SignalState = 1; - // TODO: KiWaitTest(Event, Increment); - // For now, we just sleep to give other threads time to wake - // KiWaitTest and related functions require correct thread scheduling to implement first - // This will have to wait until CPU emulation at v1.0 - Sleep(1); + KiWaitTest(Event, Increment); + std::this_thread::yield(); + } + else { + KiWaitListUnlock(); } Event->Header.SignalState = 0; @@ -1385,9 +1404,7 @@ XBSYSAPI EXPORTNUM(124) xbox::long_xt NTAPI xbox::KeQueryBasePriorityThread KIRQL OldIrql; KiLockDispatcherDatabase(&OldIrql); - // It cannot fail because all thread handles are created by ob - const auto& nativeHandle = GetNativeHandle(reinterpret_cast(Thread)->UniqueThread); - long_xt ret = GetThreadPriority(*nativeHandle); + long_xt ret = Thread->Priority; KiUnlockDispatcherDatabase(OldIrql); @@ -1540,12 +1557,14 @@ XBSYSAPI EXPORTNUM(132) xbox::long_xt NTAPI xbox::KeReleaseSemaphore } Semaphore->Header.SignalState = adjusted_signalstate; - //TODO: Implement KiWaitTest -#if 0 + KiWaitListLock(); if ((initial_state == 0) && (IsListEmpty(&Semaphore->Header.WaitListHead) == FALSE)) { KiWaitTest(&Semaphore->Header, Increment); + std::this_thread::yield(); + } + else { + KiWaitListUnlock(); } -#endif if (Wait) { PKTHREAD current_thread = KeGetCurrentThread(); @@ -1759,11 +1778,29 @@ XBSYSAPI EXPORTNUM(140) xbox::ulong_xt NTAPI xbox::KeResumeThread { LOG_FUNC_ONE_ARG(Thread); - NTSTATUS ret = X_STATUS_SUCCESS; + KIRQL OldIrql; + KiLockDispatcherDatabase(&OldIrql); - LOG_UNIMPLEMENTED(); + char_xt OldCount = Thread->SuspendCount; + if (OldCount != 0) { + --Thread->SuspendCount; + if (Thread->SuspendCount == 0) { +#if 0 + ++Thread->SuspendSemaphore.Header.SignalState; + KiWaitListLock(); + KiWaitTest(&Thread->SuspendSemaphore, 0); + std::this_thread::yield(); +#else + if (const auto &nativeHandle = GetNativeHandle(reinterpret_cast(Thread)->UniqueThread)) { + ResumeThread(*nativeHandle); + } +#endif + } + } - RETURN(ret); + KiUnlockDispatcherDatabase(OldIrql); + + RETURN(OldCount); } XBSYSAPI EXPORTNUM(141) xbox::PLIST_ENTRY NTAPI xbox::KeRundownQueue @@ -1805,25 +1842,15 @@ XBSYSAPI EXPORTNUM(143) xbox::long_xt NTAPI xbox::KeSetBasePriorityThread ) { LOG_FUNC_BEGIN - LOG_FUNC_ARG_OUT(Thread) - LOG_FUNC_ARG_OUT(Priority) + LOG_FUNC_ARG(Thread) + LOG_FUNC_ARG(Priority) LOG_FUNC_END; KIRQL oldIRQL; KiLockDispatcherDatabase(&oldIRQL); - // It cannot fail because all thread handles are created by ob - const auto &nativeHandle = GetNativeHandle(reinterpret_cast(Thread)->UniqueThread); - LONG ret = GetThreadPriority(*nativeHandle); - - // This would work normally, but it will slow down the emulation, - // don't do that if the priority is higher then normal (so our own)! - if (Priority <= THREAD_PRIORITY_NORMAL) { - BOOL result = SetThreadPriority(*nativeHandle, Priority); - if (!result) { - EmuLog(LOG_LEVEL::WARNING, "SetThreadPriority failed: %s", WinError2Str().c_str()); - } - } + Thread->Priority = Priority; + long_xt ret = Thread->Priority; KiUnlockDispatcherDatabase(oldIRQL); @@ -1844,17 +1871,9 @@ XBSYSAPI EXPORTNUM(144) xbox::boolean_xt NTAPI xbox::KeSetDisableBoostThread KIRQL oldIRQL; KiLockDispatcherDatabase(&oldIRQL); - // It cannot fail because all thread handles are created by ob - const auto &nativeHandle = GetNativeHandle(reinterpret_cast(Thread)->UniqueThread); - boolean_xt prevDisableBoost = Thread->DisableBoost; Thread->DisableBoost = (CHAR)Disable; - BOOL bRet = SetThreadPriorityBoost(*nativeHandle, Disable); - if (!bRet) { - EmuLog(LOG_LEVEL::WARNING, "SetThreadPriorityBoost failed: %s", WinError2Str().c_str()); - } - KiUnlockDispatcherDatabase(oldIRQL); RETURN(prevDisableBoost); @@ -1889,7 +1908,9 @@ XBSYSAPI EXPORTNUM(145) xbox::long_xt NTAPI xbox::KeSetEvent } LONG OldState = Event->Header.SignalState; + KiWaitListLock(); if (IsListEmpty(&Event->Header.WaitListHead) != FALSE) { + KiWaitListUnlock(); Event->Header.SignalState = 1; } else { PKWAIT_BLOCK WaitBlock = CONTAINING_RECORD(Event->Header.WaitListHead.Flink, KWAIT_BLOCK, WaitListEntry); @@ -1897,16 +1918,14 @@ XBSYSAPI EXPORTNUM(145) xbox::long_xt NTAPI xbox::KeSetEvent (WaitBlock->WaitType != WaitAny)) { if (OldState == 0) { Event->Header.SignalState = 1; - // TODO: KiWaitTest(Event, Increment); - // For now, we just sleep to give other threads time to wake - // See KePulseEvent - Sleep(1); + KiWaitTest(Event, Increment); + } + else { + KiWaitListUnlock(); } } else { - // TODO: KiUnwaitThread(WaitBlock->Thread, (NTSTATUS)WaitBlock->WaitKey, Increment); - // For now, we just sleep to give other threads time to wake - // See KePulseEvent - Sleep(1); + KiUnwaitThread(WaitBlock->Thread, (NTSTATUS)WaitBlock->WaitKey, Increment); + KiWaitListUnlock(); } } @@ -1943,6 +1962,7 @@ XBSYSAPI EXPORTNUM(146) xbox::void_xt NTAPI xbox::KeSetEventBoostPriority return; } + KiWaitListLock(); if (IsListEmpty(&Event->Header.WaitListHead) != FALSE) { Event->Header.SignalState = 1; } else { @@ -1953,11 +1973,9 @@ XBSYSAPI EXPORTNUM(146) xbox::void_xt NTAPI xbox::KeSetEventBoostPriority } WaitThread->Quantum = WaitThread->ApcState.Process->ThreadQuantum; - // TODO: KiUnwaitThread(WaitThread, X_STATUS_SUCCESS, 1); - // For now, we just sleep to give other threads time to wake - // See KePulseEvent - Sleep(1); + KiUnwaitThread(WaitThread, X_STATUS_SUCCESS, 1); } + KiWaitListUnlock(); KiUnlockDispatcherDatabase(OldIrql); } @@ -1989,8 +2007,8 @@ XBSYSAPI EXPORTNUM(148) xbox::boolean_xt NTAPI xbox::KeSetPriorityThread ) { LOG_FUNC_BEGIN - LOG_FUNC_ARG_OUT(Thread) - LOG_FUNC_ARG_OUT(Priority) + LOG_FUNC_ARG(Thread) + LOG_FUNC_ARG(Priority) LOG_FUNC_END; LOG_UNIMPLEMENTED(); @@ -2103,11 +2121,38 @@ XBSYSAPI EXPORTNUM(152) xbox::ulong_xt NTAPI xbox::KeSuspendThread { LOG_FUNC_ONE_ARG(Thread); - NTSTATUS ret = X_STATUS_SUCCESS; + KIRQL OldIrql; + KiLockDispatcherDatabase(&OldIrql); - LOG_UNIMPLEMENTED(); + char_xt OldCount = Thread->SuspendCount; + if (OldCount == X_MAXIMUM_SUSPEND_COUNT) { + KiUnlockDispatcherDatabase(OldIrql); + RETURN(X_STATUS_SUSPEND_COUNT_EXCEEDED); + } - RETURN(ret); + if (Thread->ApcState.ApcQueueable == TRUE) { + ++Thread->SuspendCount; + if (OldCount == 0) { +#if 0 + if (KiInsertQueueApc(&Thread->SuspendApc, 0) == FALSE) { + --Thread->SuspendSemaphore.Header.SignalState; + } +#else + // JSRF creates a thread at 0x0013BC30 and then it attempts to continuously suspend/resume it. Unfortunately, this thread performs a never ending loop (and + // terminates if it ever exit the loop), and never calls any kernel functions in the middle. This means that our suspend APC will never be executed and so + // we cannot suspend such thread. Thus, we will always have to rely on the host to do the suspension, as long as we do direct execution. Note that this is + // a general issue for all kernel APCs too. + if (const auto &nativeHandle = GetNativeHandle(reinterpret_cast(Thread)->UniqueThread)) { + SuspendThread(*nativeHandle); + } +#endif + } + + } + + KiUnlockDispatcherDatabase(OldIrql); + + RETURN(OldCount); } // ****************************************************************** @@ -2203,15 +2248,13 @@ XBSYSAPI EXPORTNUM(158) xbox::ntstatus_xt NTAPI xbox::KeWaitForMultipleObjects // Wait Loop // This loop ends PLARGE_INTEGER OriginalTime = Timeout; - LARGE_INTEGER DueTime, NewTime; - KWAIT_BLOCK StackWaitBlock; - PKWAIT_BLOCK WaitBlock = &StackWaitBlock; + PKWAIT_BLOCK WaitBlock; BOOLEAN WaitSatisfied; NTSTATUS WaitStatus; PKMUTANT ObjectMutant; - // Hack variable (remove this when the thread scheduler is here) - bool timeout_set = false; do { + Thread->WaitBlockList = WaitBlockArray; + // Check if we need to let an APC run. This should immediately trigger APC interrupt via a call to UnlockDispatcherDatabase if (Thread->ApcState.KernelApcPending && (Thread->WaitIrql < APC_LEVEL)) { KiUnlockDispatcherDatabase(Thread->WaitIrql); @@ -2270,7 +2313,7 @@ XBSYSAPI EXPORTNUM(158) xbox::ntstatus_xt NTAPI xbox::KeWaitForMultipleObjects // Check if the wait can be satisfied immediately if ((WaitType == WaitAll) && (WaitSatisfied)) { WaitBlock->NextWaitBlock = &WaitBlockArray[0]; - KiWaitSatisfyAll(WaitBlock); + KiWaitSatisfyAllAndLock(WaitBlock); WaitStatus = (NTSTATUS)Thread->WaitStatus; goto NoWait; } @@ -2285,36 +2328,20 @@ XBSYSAPI EXPORTNUM(158) xbox::ntstatus_xt NTAPI xbox::KeWaitForMultipleObjects goto NoWait; } - // Setup a timer for the thread but only once (for now) - if (!timeout_set) { - KiTimerLock(); - PKTIMER Timer = &Thread->Timer; - PKWAIT_BLOCK WaitTimer = &Thread->TimerWaitBlock; - WaitBlock->NextWaitBlock = WaitTimer; - Timer->Header.WaitListHead.Flink = &WaitTimer->WaitListEntry; - Timer->Header.WaitListHead.Blink = &WaitTimer->WaitListEntry; - WaitTimer->NextWaitBlock = WaitBlock; - if (KiInsertTreeTimer(Timer, *Timeout) == FALSE) { - WaitStatus = (NTSTATUS)STATUS_TIMEOUT; - KiTimerUnlock(); - goto NoWait; - } - - // Boring, ensure that we only set the thread timer once. Otherwise, this will cause to insert the same - // thread timer over and over in the timer list, which will prevent KiTimerExpiration from removing these - // duplicated timers and thus it will attempt to endlessly remove the same unremoved timers, causing a deadlock. - // This can be removed once KiSwapThread and the kernel/user APCs are implemented - timeout_set = true; - DueTime.QuadPart = Timer->DueTime.QuadPart; + // Setup a timer for the thread + KiTimerLock(); + PKTIMER Timer = &Thread->Timer; + PKWAIT_BLOCK WaitTimer = &Thread->TimerWaitBlock; + WaitBlock->NextWaitBlock = WaitTimer; + Timer->Header.WaitListHead.Flink = &WaitTimer->WaitListEntry; + Timer->Header.WaitListHead.Blink = &WaitTimer->WaitListEntry; + WaitTimer->NextWaitBlock = WaitBlock; + if (KiInsertTreeTimer(Timer, *Timeout) == FALSE) { + WaitStatus = (NTSTATUS)STATUS_TIMEOUT; KiTimerUnlock(); - } - - // KiTimerExpiration has removed the timer but the objects were not signaled, so we have a timeout - // (remove this when the thread scheduler is here) - if (Thread->Timer.Header.Inserted == FALSE) { - WaitStatus = (NTSTATUS)(STATUS_TIMEOUT); goto NoWait; } + KiTimerUnlock(); } else { WaitBlock->NextWaitBlock = WaitBlock; @@ -2322,12 +2349,13 @@ XBSYSAPI EXPORTNUM(158) xbox::ntstatus_xt NTAPI xbox::KeWaitForMultipleObjects WaitBlock->NextWaitBlock = &WaitBlockArray[0]; WaitBlock = &WaitBlockArray[0]; + KiWaitListLock(); do { ObjectMutant = (PKMUTANT)WaitBlock->Object; - //InsertTailList(&ObjectMutant->Header.WaitListHead, &WaitBlock->WaitListEntry); + InsertTailList(&ObjectMutant->Header.WaitListHead, &WaitBlock->WaitListEntry); WaitBlock = WaitBlock->NextWaitBlock; } while (WaitBlock != &WaitBlockArray[0]); - + KiWaitListUnlock(); /* TODO: We can't implement this and the return values until we have our own thread scheduler @@ -2335,9 +2363,6 @@ XBSYSAPI EXPORTNUM(158) xbox::ntstatus_xt NTAPI xbox::KeWaitForMultipleObjects This code can all be enabled once we have CPU emulation and our own scheduler in v1.0 */ - // Insert the WaitBlock - //InsertTailList(&ObjectMutant->Header.WaitListHead, &WaitBlock->WaitListEntry); - // If the current thread is processing a queue object, wake other treads using the same queue PRKQUEUE Queue = (PRKQUEUE)Thread->Queue; if (Queue != NULL) { @@ -2349,7 +2374,7 @@ XBSYSAPI EXPORTNUM(158) xbox::ntstatus_xt NTAPI xbox::KeWaitForMultipleObjects Thread->WaitMode = WaitMode; Thread->WaitReason = (UCHAR)WaitReason; Thread->WaitTime = KeTickCount; - //Thread->State = Waiting; + Thread->State = Waiting; //KiInsertWaitList(WaitMode, Thread); //WaitStatus = (NTSTATUS)KiSwapThread(); @@ -2364,12 +2389,15 @@ XBSYSAPI EXPORTNUM(158) xbox::ntstatus_xt NTAPI xbox::KeWaitForMultipleObjects //} // TODO: Remove this after we have our own scheduler and the above is implemented - Sleep(0); + WaitStatus = WaitApc([](PKTHREAD Thread) -> std::optional { + if (Thread->State == Ready) { + // We have been readied to resume execution, so exit the wait + return std::make_optional(Thread->WaitStatus); + } + return std::nullopt; + }, Timeout, Alertable, WaitMode, Thread); - // Reduce the timout if necessary - if (Timeout != nullptr) { - Timeout = KiComputeWaitInterval(OriginalTime, &DueTime, &NewTime); - } + break; } // Raise IRQL to DISPATCH_LEVEL and lock the database (only if it's not already at this level) @@ -2384,10 +2412,14 @@ XBSYSAPI EXPORTNUM(158) xbox::ntstatus_xt NTAPI xbox::KeWaitForMultipleObjects // The waiting thead has been alerted, or an APC needs to be delivered // So unlock the dispatcher database, lower the IRQ and return the status + Thread->State = Running; KiUnlockDispatcherDatabase(Thread->WaitIrql); +#if 0 + // No need for this at the moment, since WaitApc already executes user APCs if (WaitStatus == X_STATUS_USER_APC) { KiExecuteUserApc(); } +#endif RETURN(WaitStatus); @@ -2396,14 +2428,7 @@ NoWait: // Unlock the database and return the status //TODO: KiAdjustQuantumThread(Thread); - // Don't forget to remove the thread timer if the objects were signaled before the timer expired - // (remove this when the thread scheduler is here) - if (timeout_set && Thread->Timer.Header.Inserted == TRUE) { - KiTimerLock(); - KxRemoveTreeTimer(&Thread->Timer); - KiTimerUnlock(); - } - + Thread->State = Running; KiUnlockDispatcherDatabase(Thread->WaitIrql); if (WaitStatus == X_STATUS_USER_APC) { @@ -2445,12 +2470,9 @@ XBSYSAPI EXPORTNUM(159) xbox::ntstatus_xt NTAPI xbox::KeWaitForSingleObject // Wait Loop // This loop ends PLARGE_INTEGER OriginalTime = Timeout; - LARGE_INTEGER DueTime, NewTime; KWAIT_BLOCK StackWaitBlock; PKWAIT_BLOCK WaitBlock = &StackWaitBlock; - NTSTATUS WaitStatus; - // Hack variable (remove this when the thread scheduler is here) - bool timeout_set = false; + ntstatus_xt WaitStatus; do { // Check if we need to let an APC run. This should immediately trigger APC interrupt via a call to UnlockDispatcherDatabase if (Thread->ApcState.KernelApcPending && (Thread->WaitIrql < APC_LEVEL)) { @@ -2498,36 +2520,20 @@ XBSYSAPI EXPORTNUM(159) xbox::ntstatus_xt NTAPI xbox::KeWaitForSingleObject goto NoWait; } - // Setup a timer for the thread but only once (for now) - if (!timeout_set) { - KiTimerLock(); - PKTIMER Timer = &Thread->Timer; - PKWAIT_BLOCK WaitTimer = &Thread->TimerWaitBlock; - WaitBlock->NextWaitBlock = WaitTimer; - Timer->Header.WaitListHead.Flink = &WaitTimer->WaitListEntry; - Timer->Header.WaitListHead.Blink = &WaitTimer->WaitListEntry; - WaitTimer->NextWaitBlock = WaitBlock; - if (KiInsertTreeTimer(Timer, *Timeout) == FALSE) { - WaitStatus = (NTSTATUS)STATUS_TIMEOUT; - KiTimerUnlock(); - goto NoWait; - } - - // Boring, ensure that we only set the thread timer once. Otherwise, this will cause to insert the same - // thread timer over and over in the timer list, which will prevent KiTimerExpiration from removing these - // duplicated timers and thus it will attempt to endlessly remove the same unremoved timers, causing a deadlock. - // This can be removed once KiSwapThread and the kernel/user APCs are implemented - timeout_set = true; - DueTime.QuadPart = Timer->DueTime.QuadPart; + // Setup a timer for the thread + KiTimerLock(); + PKTIMER Timer = &Thread->Timer; + PKWAIT_BLOCK WaitTimer = &Thread->TimerWaitBlock; + WaitBlock->NextWaitBlock = WaitTimer; + Timer->Header.WaitListHead.Flink = &WaitTimer->WaitListEntry; + Timer->Header.WaitListHead.Blink = &WaitTimer->WaitListEntry; + WaitTimer->NextWaitBlock = WaitBlock; + if (KiInsertTreeTimer(Timer, *Timeout) == FALSE) { + WaitStatus = (NTSTATUS)STATUS_TIMEOUT; KiTimerUnlock(); - } - - // KiTimerExpiration has removed the timer but the object was not signaled, so we have a timeout - // (remove this when the thread scheduler is here) - if (Thread->Timer.Header.Inserted == FALSE) { - WaitStatus = (NTSTATUS)(STATUS_TIMEOUT); goto NoWait; } + KiTimerUnlock(); } else { WaitBlock->NextWaitBlock = WaitBlock; @@ -2539,9 +2545,6 @@ XBSYSAPI EXPORTNUM(159) xbox::ntstatus_xt NTAPI xbox::KeWaitForSingleObject This code can all be enabled once we have CPU emulation and our own scheduler in v1.0 */ - // Insert the WaitBlock - //InsertTailList(&ObjectMutant->Header.WaitListHead, &WaitBlock->WaitListEntry); - // If the current thread is processing a queue object, wake other treads using the same queue PRKQUEUE Queue = (PRKQUEUE)Thread->Queue; if (Queue != NULL) { @@ -2553,7 +2556,7 @@ XBSYSAPI EXPORTNUM(159) xbox::ntstatus_xt NTAPI xbox::KeWaitForSingleObject Thread->WaitMode = WaitMode; Thread->WaitReason = (UCHAR)WaitReason; Thread->WaitTime = KeTickCount; - // TODO: Thread->State = Waiting; + Thread->State = Waiting; //KiInsertWaitList(WaitMode, Thread); /* @@ -2568,13 +2571,21 @@ XBSYSAPI EXPORTNUM(159) xbox::ntstatus_xt NTAPI xbox::KeWaitForSingleObject return WaitStatus; } */ - // TODO: Remove this after we have our own scheduler and the above is implemented - Sleep(0); + // Insert the WaitBlock + KiWaitListLock(); + InsertTailList(&ObjectMutant->Header.WaitListHead, &WaitBlock->WaitListEntry); + KiWaitListUnlock(); - // Reduce the timout if necessary - if (Timeout != nullptr) { - Timeout = KiComputeWaitInterval(OriginalTime, &DueTime, &NewTime); - } + // TODO: Remove this after we have our own scheduler and the above is implemented + WaitStatus = WaitApc([](PKTHREAD Thread) -> std::optional { + if (Thread->State == Ready) { + // We have been readied to resume execution, so exit the wait + return std::make_optional(Thread->WaitStatus); + } + return std::nullopt; + }, Timeout, Alertable, WaitMode, Thread); + + break; } // Raise IRQL to DISPATCH_LEVEL and lock the database @@ -2589,10 +2600,14 @@ XBSYSAPI EXPORTNUM(159) xbox::ntstatus_xt NTAPI xbox::KeWaitForSingleObject // The waiting thead has been alerted, or an APC needs to be delivered // So unlock the dispatcher database, lower the IRQ and return the status + Thread->State = Running; KiUnlockDispatcherDatabase(Thread->WaitIrql); +#if 0 + // No need for this at the moment, since WaitApc already executes user APCs if (WaitStatus == X_STATUS_USER_APC) { KiExecuteUserApc(); } +#endif RETURN(WaitStatus); @@ -2601,14 +2616,7 @@ NoWait: // Unlock the database and return the status //TODO: KiAdjustQuantumThread(Thread); - // Don't forget to remove the thread timer if the object was signaled before the timer expired - // (remove this when the thread scheduler is here) - if (timeout_set && Thread->Timer.Header.Inserted == TRUE) { - KiTimerLock(); - KxRemoveTreeTimer(&Thread->Timer); - KiTimerUnlock(); - } - + Thread->State = Running; KiUnlockDispatcherDatabase(Thread->WaitIrql); if (WaitStatus == X_STATUS_USER_APC) { diff --git a/src/core/kernel/exports/EmuKrnlKe.h b/src/core/kernel/exports/EmuKrnlKe.h index a7e2ebbc7..820c26f78 100644 --- a/src/core/kernel/exports/EmuKrnlKe.h +++ b/src/core/kernel/exports/EmuKrnlKe.h @@ -27,6 +27,8 @@ namespace xbox { + extern std::atomic_flag KeSystemTimeChanged; + void_xt NTAPI KeSetSystemTime ( IN PLARGE_INTEGER NewTime, @@ -50,5 +52,16 @@ namespace xbox IN PKPROCESS Process ); + xbox::void_xt KeResumeThreadEx + ( + IN PKTHREAD Thread + ); + + xbox::void_xt KeSuspendThreadEx + ( + IN PKTHREAD Thread + ); + void_xt KeEmptyQueueApc(); + void_xt KeWaitForDpc(); } diff --git a/src/core/kernel/exports/EmuKrnlKi.cpp b/src/core/kernel/exports/EmuKrnlKi.cpp index 9e66a18b6..40c7210b9 100644 --- a/src/core/kernel/exports/EmuKrnlKi.cpp +++ b/src/core/kernel/exports/EmuKrnlKi.cpp @@ -59,6 +59,8 @@ * PROGRAMMERS: Alex Ionescu (alex.ionescu@reactos.org) */ +// Also from ReactOS: KiWaitTest, KiWaitSatisfyAll, KiUnwaitThread, KiUnlinkThread + // COPYING file: /* GNU GENERAL PUBLIC LICENSE @@ -84,15 +86,18 @@ the said software). #include "Logging.h" // For LOG_FUNC() #include "EmuKrnl.h" // for the list support functions #include "EmuKrnlKi.h" +#include "EmuKrnlKe.h" #define MAX_TIMER_DPCS 16 #define ASSERT_TIMER_LOCKED assert(KiTimerMtx.Acquired > 0) +#define ASSERT_WAIT_LIST_LOCKED assert(KiWaitListMtx.Acquired > 0) xbox::KPROCESS KiUniqueProcess; const xbox::ulong_xt CLOCK_TIME_INCREMENT = 0x2710; xbox::KDPC KiTimerExpireDpc; xbox::KI_TIMER_LOCK KiTimerMtx; +xbox::KI_WAIT_LIST_LOCK KiWaitListMtx; xbox::KTIMER_TABLE_ENTRY KiTimerTableListHead[TIMER_TABLE_SIZE]; xbox::LIST_ENTRY KiWaitInListHead; std::mutex xbox::KiApcListMtx; @@ -129,55 +134,81 @@ xbox::void_xt xbox::KiTimerUnlock() KiTimerMtx.Mtx.unlock(); } -xbox::void_xt xbox::KiClockIsr -( - unsigned int ScalingFactor -) +xbox::void_xt xbox::KiWaitListLock() { - KIRQL OldIrql; - LARGE_INTEGER InterruptTime; - LARGE_INTEGER HostSystemTime; + KiWaitListMtx.Mtx.lock(); + KiWaitListMtx.Acquired++; +} + +xbox::void_xt xbox::KiWaitListUnlock() +{ + KiWaitListMtx.Acquired--; + KiWaitListMtx.Mtx.unlock(); +} + +xbox::void_xt xbox::KiClockIsr(ulonglong_xt TotalUs) +{ + LARGE_INTEGER InterruptTime, SystemTime; ULONG Hand; DWORD OldKeTickCount; - - OldIrql = KfRaiseIrql(CLOCK_LEVEL); + static uint64_t LostUs; + uint64_t TotalMs = TotalUs / 1000; + LostUs += (TotalUs - TotalMs * 1000); + uint64_t RecoveredMs = LostUs / 1000; + TotalMs += RecoveredMs; + LostUs -= (RecoveredMs * 1000); // Update the interrupt time InterruptTime.u.LowPart = KeInterruptTime.LowPart; InterruptTime.u.HighPart = KeInterruptTime.High1Time; - InterruptTime.QuadPart += (CLOCK_TIME_INCREMENT * ScalingFactor); + InterruptTime.QuadPart += (CLOCK_TIME_INCREMENT * TotalMs); KeInterruptTime.High2Time = InterruptTime.u.HighPart; KeInterruptTime.LowPart = InterruptTime.u.LowPart; KeInterruptTime.High1Time = InterruptTime.u.HighPart; // Update the system time - // NOTE: I'm not sure if we should round down the host system time to the nearest multiple - // of the Xbox clock increment... - GetSystemTimeAsFileTime((LPFILETIME)&HostSystemTime); - HostSystemTime.QuadPart += HostSystemTimeDelta.load(); - KeSystemTime.High2Time = HostSystemTime.u.HighPart; - KeSystemTime.LowPart = HostSystemTime.u.LowPart; - KeSystemTime.High1Time = HostSystemTime.u.HighPart; + if (KeSystemTimeChanged.test()) [[unlikely]] { + KeSystemTimeChanged.clear(); + LARGE_INTEGER HostSystemTime, OldSystemTime; + GetSystemTimeAsFileTime((LPFILETIME)&HostSystemTime); + xbox::KeSystemTime.High2Time = HostSystemTime.u.HighPart; + xbox::KeSystemTime.LowPart = HostSystemTime.u.LowPart; + xbox::KeSystemTime.High1Time = HostSystemTime.u.HighPart; + KeSetSystemTime(&HostSystemTime, &OldSystemTime); + } + else { + SystemTime.u.LowPart = KeSystemTime.LowPart; + SystemTime.u.HighPart = KeSystemTime.High1Time; + SystemTime.QuadPart += (CLOCK_TIME_INCREMENT * TotalMs); + KeSystemTime.High2Time = SystemTime.u.HighPart; + KeSystemTime.LowPart = SystemTime.u.LowPart; + KeSystemTime.High1Time = SystemTime.u.HighPart; + } // Update the tick counter OldKeTickCount = KeTickCount; - KeTickCount += ScalingFactor; + KeTickCount += static_cast(TotalMs); // Because this function must be fast to continuously update the kernel clocks, if somebody else is currently // holding the lock, we won't wait and instead skip the check of the timers for this cycle if (KiTimerMtx.Mtx.try_lock()) { KiTimerMtx.Acquired++; // Check if a timer has expired - Hand = OldKeTickCount & (TIMER_TABLE_SIZE - 1); - if (KiTimerTableListHead[Hand].Entry.Flink != &KiTimerTableListHead[Hand].Entry && - (ULONGLONG)InterruptTime.QuadPart >= KiTimerTableListHead[Hand].Time.QuadPart) { - KeInsertQueueDpc(&KiTimerExpireDpc, (PVOID)Hand, 0); + // On real hw, this is called every ms, so it only needs to check a single timer index. However, testing on the emulator shows that this can have a delay + // larger than a ms. If we only check the index corresponding to OldKeTickCount, then we will miss timers that might have expired already, causing an unpredictable + // delay on threads that are waiting with those timeouts + dword_xt EndKeTickCount = (KeTickCount - OldKeTickCount) >= TIMER_TABLE_SIZE ? OldKeTickCount + TIMER_TABLE_SIZE : KeTickCount; + for (dword_xt i = OldKeTickCount; i < EndKeTickCount; ++i) { + Hand = i & (TIMER_TABLE_SIZE - 1); + if (KiTimerTableListHead[Hand].Entry.Flink != &KiTimerTableListHead[Hand].Entry && + (ULONGLONG)InterruptTime.QuadPart >= KiTimerTableListHead[Hand].Time.QuadPart) { + KeInsertQueueDpc(&KiTimerExpireDpc, (PVOID)OldKeTickCount, (PVOID)EndKeTickCount); + break; + } } KiTimerMtx.Acquired--; KiTimerMtx.Mtx.unlock(); } - - KfLowerIrql(OldIrql); } xbox::void_xt NTAPI xbox::KiCheckTimerTable @@ -328,8 +359,8 @@ xbox::boolean_xt FASTCALL xbox::KiInsertTimerTable IN xbox::ulong_xt Hand ) { - LARGE_INTEGER InterruptTime; - LONGLONG DueTime = Timer->DueTime.QuadPart; + ULARGE_INTEGER InterruptTime; + ULONGLONG DueTime = Timer->DueTime.QuadPart; BOOLEAN Expired = FALSE; PLIST_ENTRY ListHead, NextEntry; PKTIMER CurrentTimer; @@ -352,7 +383,7 @@ xbox::boolean_xt FASTCALL xbox::KiInsertTimerTable CurrentTimer = CONTAINING_RECORD(NextEntry, KTIMER, TimerListEntry); /* Now check if we can fit it before */ - if ((ULONGLONG)DueTime >= CurrentTimer->DueTime.QuadPart) break; + if (DueTime >= CurrentTimer->DueTime.QuadPart) break; /* Keep looping */ NextEntry = NextEntry->Blink; @@ -368,6 +399,10 @@ xbox::boolean_xt FASTCALL xbox::KiInsertTimerTable KiTimerTableListHead[Hand].Time.QuadPart = DueTime; /* Make sure it hasn't expired already */ + // NOTE: DueTime must be unsigned so that we can perform un unsigned comparison with the interrupt time. Otherwise, if DueTime is very large, it will be + // interpreted as a very small negative number, which will cause the function to think the timer has already expired, when it didn't. Test case: Metal Slug 3. + // It uses KeDelayExecutionThread with a relative timeout of 0x8000000000000000, which is then interpreted here as a negative number that immediately satisfies + // the wait. The title crashes shortly after, since the wait was supposed to end with a user APC queued by NtQueueApcThread instead InterruptTime.QuadPart = KeQueryInterruptTime(); if (DueTime <= InterruptTime.QuadPart) { EmuLog(LOG_LEVEL::DEBUG, "Timer %p already expired", Timer); @@ -484,19 +519,13 @@ xbox::boolean_xt FASTCALL xbox::KiSignalTimer Timer->Header.SignalState = TRUE; /* Check if the timer has waiters */ + KiWaitListLock(); if (!IsListEmpty(&Timer->Header.WaitListHead)) { - /* Check the type of event */ - if (Timer->Header.Type == TimerNotificationObject) - { - /* Unwait the thread */ - // KxUnwaitThread(&Timer->Header, IO_NO_INCREMENT); - } - else - { - /* Otherwise unwait the thread and signal the timer */ - // KxUnwaitThreadForEvent((PKEVENT)Timer, IO_NO_INCREMENT); - } + KiWaitTest(Timer, 0); + } + else { + KiWaitListUnlock(); } /* Check if we have a period */ @@ -532,7 +561,7 @@ xbox::void_xt NTAPI xbox::KiTimerExpiration { ULARGE_INTEGER SystemTime, InterruptTime; LARGE_INTEGER Interval; - LONG Limit, Index, i; + LONG i; ULONG Timers, ActiveTimers, DpcCalls; PLIST_ENTRY ListHead, NextEntry; KIRQL OldIrql; @@ -544,34 +573,25 @@ xbox::void_xt NTAPI xbox::KiTimerExpiration /* Query system and interrupt time */ KeQuerySystemTime((PLARGE_INTEGER)&SystemTime); InterruptTime.QuadPart = KeQueryInterruptTime(); - Limit = KeTickCount; /* Get the index of the timer and normalize it */ - Index = PtrToLong(SystemArgument1); - if ((Limit - Index) >= TIMER_TABLE_SIZE) - { - /* Normalize it */ - Limit = Index + TIMER_TABLE_SIZE - 1; - } - - /* Setup index and actual limit */ - Index--; - Limit &= (TIMER_TABLE_SIZE - 1); + dword_xt OldKeTickCount = PtrToLong(SystemArgument1); + dword_xt EndKeTickCount = PtrToLong(SystemArgument2); /* Setup accounting data */ DpcCalls = 0; Timers = 24; ActiveTimers = 4; - /* Lock the Database and Raise IRQL */ + /* Lock the Database */ KiTimerLock(); KiLockDispatcherDatabase(&OldIrql); /* Start expiration loop */ - do + for (dword_xt i = OldKeTickCount; i < EndKeTickCount; ++i) { /* Get the current index */ - Index = (Index + 1) & (TIMER_TABLE_SIZE - 1); + dword_xt Index = i & (TIMER_TABLE_SIZE - 1); /* Get list pointers and loop the list */ ListHead = &KiTimerTableListHead[Index].Entry; @@ -599,19 +619,13 @@ xbox::void_xt NTAPI xbox::KiTimerExpiration Period = Timer->Period; /* Check if there are any waiters */ + KiWaitListLock(); if (!IsListEmpty(&Timer->Header.WaitListHead)) { - /* Check the type of event */ - if (Timer->Header.Type == TimerNotificationObject) - { - /* Unwait the thread */ - // KxUnwaitThread(&Timer->Header, IO_NO_INCREMENT); - } - else - { - /* Otherwise unwait the thread and signal the timer */ - // KxUnwaitThreadForEvent((PKEVENT)Timer, IO_NO_INCREMENT); - } + KiWaitTest(Timer, 0); + } + else { + KiWaitListUnlock(); } /* Check if we have a period */ @@ -709,7 +723,7 @@ xbox::void_xt NTAPI xbox::KiTimerExpiration break; } } - } while (Index != Limit); + } /* Verify the timer table, on a debug kernel only */ if (g_bIsDebugKernel) { @@ -737,17 +751,17 @@ xbox::void_xt NTAPI xbox::KiTimerExpiration ); } + KiTimerUnlock(); /* Lower IRQL if we need to */ if (OldIrql != DISPATCH_LEVEL) { KfLowerIrql(OldIrql); } - KiTimerUnlock(); } else { /* Unlock the dispatcher */ - KiUnlockDispatcherDatabase(OldIrql); KiTimerUnlock(); + KiUnlockDispatcherDatabase(OldIrql); } } @@ -791,19 +805,13 @@ xbox::void_xt FASTCALL xbox::KiTimerListExpire Period = Timer->Period; /* Check if there's any waiters */ + KiWaitListLock(); if (!IsListEmpty(&Timer->Header.WaitListHead)) { - /* Check the type of event */ - if (Timer->Header.Type == TimerNotificationObject) - { - /* Unwait the thread */ - // KxUnwaitThread(&Timer->Header, IO_NO_INCREMENT); - } - else - { - /* Otherwise unwait the thread and signal the timer */ - // KxUnwaitThreadForEvent((PKEVENT)Timer, IO_NO_INCREMENT); - } + KiWaitTest(Timer, 0); + } + else { + KiWaitListUnlock(); } /* Check if we have a period */ @@ -826,6 +834,8 @@ xbox::void_xt FASTCALL xbox::KiTimerListExpire } } + KiTimerUnlock(); + /* Check if we still have DPC entries */ if (DpcCalls) { @@ -849,39 +859,14 @@ xbox::void_xt FASTCALL xbox::KiTimerListExpire /* Lower IRQL */ KfLowerIrql(OldIrql); - KiTimerUnlock(); } else { /* Unlock the dispatcher */ KiUnlockDispatcherDatabase(OldIrql); - KiTimerUnlock(); } } -xbox::void_xt FASTCALL xbox::KiWaitSatisfyAll -( - IN xbox::PKWAIT_BLOCK WaitBlock -) -{ - PKMUTANT Object; - PRKTHREAD Thread; - PKWAIT_BLOCK WaitBlock1; - - WaitBlock1 = WaitBlock; - Thread = WaitBlock1->Thread; - do { - if (WaitBlock1->WaitKey != (cshort_xt)STATUS_TIMEOUT) { - Object = (PKMUTANT)WaitBlock1->Object; - KiWaitSatisfyAny(Object, Thread); - } - - WaitBlock1 = WaitBlock1->NextWaitBlock; - } while (WaitBlock1 != WaitBlock); - - return; -} - template static xbox::void_xt KiExecuteApc() { @@ -907,12 +892,13 @@ static xbox::void_xt KiExecuteApc() Apc->Inserted = FALSE; xbox::KiApcListMtx.unlock(); - // NOTE: we never use KernelRoutine because that is only used for kernel APCs, which we currently don't use + // This is either KiFreeUserApc, which frees the memory of the apc, or KiSuspendNop, which does nothing + (Apc->KernelRoutine)(Apc, &Apc->NormalRoutine, &Apc->NormalContext, &Apc->SystemArgument1, &Apc->SystemArgument2); + if (Apc->NormalRoutine != xbox::zeroptr) { (Apc->NormalRoutine)(Apc->NormalContext, Apc->SystemArgument1, Apc->SystemArgument2); } - xbox::ExFreePool(Apc); xbox::KiApcListMtx.lock(); } @@ -966,7 +952,8 @@ xbox::PLARGE_INTEGER FASTCALL xbox::KiComputeWaitInterval } // Source: ReactOS -xbox::void_xt NTAPI xbox::KiSuspendNop( +xbox::void_xt NTAPI xbox::KiSuspendNop +( IN PKAPC Apc, IN PKNORMAL_ROUTINE* NormalRoutine, IN PVOID* NormalContext, @@ -974,7 +961,7 @@ xbox::void_xt NTAPI xbox::KiSuspendNop( IN PVOID* SystemArgument2 ) { - /* Does nothing */ + /* Does nothing because the memory of the suspend apc is part of kthread */ UNREFERENCED_PARAMETER(Apc); UNREFERENCED_PARAMETER(NormalRoutine); UNREFERENCED_PARAMETER(NormalContext); @@ -982,8 +969,21 @@ xbox::void_xt NTAPI xbox::KiSuspendNop( UNREFERENCED_PARAMETER(SystemArgument2); } +xbox::void_xt NTAPI xbox::KiFreeUserApc +( + IN PKAPC Apc, + IN PKNORMAL_ROUTINE *NormalRoutine, + IN PVOID *NormalContext, + IN PVOID *SystemArgument1, + IN PVOID *SystemArgument2 +) +{ + ExFreePool(Apc); +} + // Source: ReactOS -xbox::void_xt NTAPI xbox::KiSuspendThread( +xbox::void_xt NTAPI xbox::KiSuspendThread +( IN PVOID NormalContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2 @@ -1076,3 +1076,210 @@ xbox::void_xt xbox::KiInitializeContextThread( /* Save back the new value of the kernel stack. */ Thread->KernelStack = reinterpret_cast(CtxSwitchFrame); } + +xbox::boolean_xt xbox::KiInsertQueueApc +( + IN PRKAPC Apc, + IN KPRIORITY Increment +) +{ + PKTHREAD kThread = Apc->Thread; + KiApcListMtx.lock(); + if (Apc->Inserted) { + KiApcListMtx.unlock(); + return FALSE; + } + InsertTailList(&kThread->ApcState.ApcListHead[Apc->ApcMode], &Apc->ApcListEntry); + Apc->Inserted = TRUE; + KiApcListMtx.unlock(); + + // We can only attempt to execute the queued apc right away if it is been inserted in the current thread, because otherwise the KTHREAD + // in the fs selector will not be correct + if (Apc->ApcMode == KernelMode) { // kernel apc + kThread->ApcState.KernelApcPending = TRUE; + // NOTE: this is wrong, we should check the thread state instead of just signaling the kernel apc, but we currently + // don't set the appropriate state in kthread + if (kThread == KeGetCurrentThread()) { + KiExecuteKernelApc(); + } + } + else if ((kThread->WaitMode == UserMode) && (kThread->Alertable)) { // user apc + kThread->ApcState.UserApcPending = TRUE; + // NOTE: this should also check the thread state + if (kThread == KeGetCurrentThread()) { + KiExecuteUserApc(); + } + } + + return TRUE; +} + +xbox::void_xt xbox::KiWaitTest +( + IN PVOID Object, + IN KPRIORITY Increment +) +{ + PLIST_ENTRY WaitEntry, WaitList; + PKWAIT_BLOCK WaitBlock, NextBlock; + PKTHREAD WaitThread; + PKMUTANT FirstObject = (PKMUTANT)Object; + + ASSERT_WAIT_LIST_LOCKED; + + /* Loop the Wait Entries */ + WaitList = &FirstObject->Header.WaitListHead; + WaitEntry = WaitList->Flink; + while ((FirstObject->Header.SignalState > 0) && (WaitEntry != WaitList)) { + /* Get the current wait block */ + WaitBlock = CONTAINING_RECORD(WaitEntry, KWAIT_BLOCK, WaitListEntry); + WaitThread = WaitBlock->Thread; + + /* Check the current Wait Mode */ + if (WaitBlock->WaitType == WaitAny) { + /* Easy case, satisfy only this wait */ + KiWaitSatisfyAny(FirstObject, WaitThread); + } + else { + /* WaitAll, check that all the objects are signalled */ + NextBlock = WaitBlock->NextWaitBlock; + while (NextBlock != WaitBlock) { + if (NextBlock->WaitKey != X_STATUS_TIMEOUT) { + PKMUTANT Mutant = (PKMUTANT)NextBlock->Object; + // NOTE: we ignore mutants because we forward them to ntdll + if (Mutant->Header.SignalState <= 0) { + // We found at least one object not in the signalled state, so we cannot satisfy the wait + goto NextWaitEntry; + } + } + + NextBlock = NextBlock->NextWaitBlock; + } + + KiWaitSatisfyAll(WaitBlock); + } + + /* Now do the rest of the unwait */ + KiUnwaitThread(WaitThread, WaitBlock->WaitKey, Increment); +NextWaitEntry: + WaitEntry = WaitEntry->Flink; + } + KiWaitListUnlock(); +} + +xbox::void_xt xbox::KiWaitSatisfyAll +( + IN PKWAIT_BLOCK FirstBlock +) +{ + PKWAIT_BLOCK WaitBlock = FirstBlock; + PKTHREAD WaitThread = WaitBlock->Thread; + + ASSERT_WAIT_LIST_LOCKED; + + /* Loop through all the Wait Blocks, and wake each Object */ + do { + /* Make sure it hasn't timed out */ + if (WaitBlock->WaitKey != X_STATUS_TIMEOUT) { + /* Wake the Object */ + KiWaitSatisfyAny((PKMUTANT)WaitBlock->Object, WaitThread); + } + + /* Move to the next block */ + WaitBlock = WaitBlock->NextWaitBlock; + } while (WaitBlock != FirstBlock); +} + +xbox::void_xt xbox::KiWaitSatisfyAllAndLock +( + IN PKWAIT_BLOCK FirstBlock +) +{ + KiWaitListLock(); + KiWaitSatisfyAll(FirstBlock); + KiWaitListUnlock(); +} + +xbox::void_xt xbox::KiUnwaitThread +( + IN PKTHREAD Thread, + IN long_ptr_xt WaitStatus, + IN KPRIORITY Increment +) +{ + ASSERT_WAIT_LIST_LOCKED; + + if (Thread->State != Waiting) { + // Don't do anything if it was already unwaited + return; + } + + /* Unlink the thread */ + KiUnlinkThread(Thread, WaitStatus); + + // We cannot schedule the thread, so we'll just set its state to Ready + Thread->State = Ready; +} + +xbox::void_xt xbox::KiUnwaitThreadAndLock +( + IN PKTHREAD Thread, + IN long_ptr_xt WaitStatus, + IN KPRIORITY Increment +) +{ + KiWaitListLock(); + KiUnwaitThread(Thread, WaitStatus, Increment); + KiWaitListUnlock(); +} + +xbox::void_xt xbox::KiUnlinkThread +( + IN PKTHREAD Thread, + IN long_ptr_xt WaitStatus +) +{ + PKWAIT_BLOCK WaitBlock; + PKTIMER Timer; + + ASSERT_WAIT_LIST_LOCKED; + + /* Update wait status */ + Thread->WaitStatus |= WaitStatus; + + /* Remove the Wait Blocks from the list */ + WaitBlock = Thread->WaitBlockList; + do { + /* Remove it */ + RemoveEntryList(&WaitBlock->WaitListEntry); + + /* Go to the next one */ + WaitBlock = WaitBlock->NextWaitBlock; + } while (WaitBlock != Thread->WaitBlockList); + +#if 0 + // Disabled, as we currently don't put threads in the ready list + /* Remove the thread from the wait list! */ + if (Thread->WaitListEntry.Flink) { + RemoveEntryList(&Thread->WaitListEntry); + } +#endif + + /* Check if there's a Thread Timer */ + Timer = &Thread->Timer; + if (Timer->Header.Inserted) { + KiTimerLock(); + KxRemoveTreeTimer(Timer); + KiTimerUnlock(); + } + +#if 0 + // Disabled, because we don't support queues + /* Increment the Queue's active threads */ + if (Thread->Queue) Thread->Queue->CurrentCount++; +#endif + + // Sanity check: set WaitBlockList to nullptr so that we can catch the case where a waiter starts a new wait but forgets to setup a new wait block. This + // way, we will crash instead of silently using the pointer to the old block + Thread->WaitBlockList = zeroptr; +} diff --git a/src/core/kernel/exports/EmuKrnlKi.h b/src/core/kernel/exports/EmuKrnlKi.h index e23165d7e..efc929fbc 100644 --- a/src/core/kernel/exports/EmuKrnlKi.h +++ b/src/core/kernel/exports/EmuKrnlKi.h @@ -47,6 +47,12 @@ namespace xbox int Acquired; } KI_TIMER_LOCK; + typedef struct _KI_WAIT_LIST_LOCK + { + std::recursive_mutex Mtx; + int Acquired; + } KI_WAIT_LIST_LOCK; + // NOTE: since the apc list is per-thread, we could also create a different mutex for each kthread extern std::mutex KiApcListMtx; @@ -56,10 +62,11 @@ namespace xbox void_xt KiTimerUnlock(); - void_xt KiClockIsr - ( - IN unsigned int ScalingFactor - ); + void_xt KiWaitListLock(); + + void_xt KiWaitListUnlock(); + + void_xt KiClockIsr(ulonglong_xt TotalUs); xbox::void_xt NTAPI KiCheckTimerTable ( @@ -132,7 +139,7 @@ namespace xbox IN KIRQL OldIrql ); - void_xt FASTCALL KiWaitSatisfyAll + void_xt KiWaitSatisfyAll ( IN PKWAIT_BLOCK WaitBlock ); @@ -156,7 +163,8 @@ namespace xbox ); // Source: ReactOS - void_xt NTAPI KiSuspendNop( + void_xt NTAPI KiSuspendNop + ( IN PKAPC Apc, IN PKNORMAL_ROUTINE* NormalRoutine, IN PVOID* NormalContext, @@ -164,6 +172,15 @@ namespace xbox IN PVOID* SystemArgument2 ); + void_xt NTAPI KiFreeUserApc + ( + IN PKAPC Apc, + IN PKNORMAL_ROUTINE *NormalRoutine, + IN PVOID *NormalContext, + IN PVOID *SystemArgument1, + IN PVOID *SystemArgument2 + ); + // Source: ReactOS void_xt NTAPI KiSuspendThread( IN PVOID NormalContext, @@ -180,6 +197,48 @@ namespace xbox IN PKSTART_ROUTINE StartRoutine, IN PVOID StartContext ); + + boolean_xt KiInsertQueueApc + ( + IN PRKAPC Apc, + IN KPRIORITY Increment + ); + + void_xt KiWaitTest + ( + IN PVOID Object, + IN KPRIORITY Increment + ); + + void_xt KiWaitSatisfyAll + ( + IN PKWAIT_BLOCK FirstBlock + ); + + void_xt KiWaitSatisfyAllAndLock + ( + IN PKWAIT_BLOCK FirstBlock + ); + + void_xt KiUnwaitThread + ( + IN PKTHREAD Thread, + IN long_ptr_xt WaitStatus, + IN KPRIORITY Increment + ); + + void_xt KiUnwaitThreadAndLock + ( + IN PKTHREAD Thread, + IN long_ptr_xt WaitStatus, + IN KPRIORITY Increment + ); + + void_xt KiUnlinkThread + ( + IN PKTHREAD Thread, + IN long_ptr_xt WaitStatus + ); }; extern xbox::KPROCESS KiUniqueProcess; diff --git a/src/core/kernel/exports/EmuKrnlNt.cpp b/src/core/kernel/exports/EmuKrnlNt.cpp index 1e7d33fc8..8595019cb 100644 --- a/src/core/kernel/exports/EmuKrnlNt.cpp +++ b/src/core/kernel/exports/EmuKrnlNt.cpp @@ -58,7 +58,7 @@ namespace NtDll #include // Prevent setting the system time from multiple threads at the same time -std::mutex NtSystemTimeMtx; +xbox::RTL_CRITICAL_SECTION xbox::NtSystemTimeCritSec; // ****************************************************************** // * 0x00B8 - NtAllocateVirtualMemory() @@ -1039,7 +1039,7 @@ XBSYSAPI EXPORTNUM(206) xbox::ntstatus_xt NTAPI xbox::NtQueueApcThread PKAPC Apc = static_cast(ExAllocatePoolWithTag(sizeof(KAPC), 'pasP')); if (Apc != zeroptr) { - KeInitializeApc(Apc, &Thread->Tcb, zeroptr, zeroptr, reinterpret_cast(ApcRoutine), UserMode, ApcRoutineContext); + KeInitializeApc(Apc, &Thread->Tcb, KiFreeUserApc, zeroptr, reinterpret_cast(ApcRoutine), UserMode, ApcRoutineContext); if (!KeInsertQueueApc(Apc, ApcStatusBlock, ApcReserved, 0)) { ExFreePool(Apc); result = X_STATUS_UNSUCCESSFUL; @@ -1856,15 +1856,20 @@ XBSYSAPI EXPORTNUM(224) xbox::ntstatus_xt NTAPI xbox::NtResumeThread LOG_FUNC_ARG_OUT(PreviousSuspendCount) LOG_FUNC_END; - if (const auto &nativeHandle = GetNativeHandle(ThreadHandle)) { - // Thread handles are created by ob - RETURN(NtDll::NtResumeThread(*nativeHandle, (::PULONG)PreviousSuspendCount)); - } - else { - RETURN(X_STATUS_INVALID_HANDLE); + PETHREAD Thread; + ntstatus_xt result = ObReferenceObjectByHandle(ThreadHandle, &PsThreadObjectType, reinterpret_cast(&Thread)); + if (!X_NT_SUCCESS(result)) { + RETURN(result); } - // TODO : Once we do our own thread-switching, implement NtResumeThread using KetResumeThread + ulong_xt PrevSuspendCount = KeResumeThread(&Thread->Tcb); + ObfDereferenceObject(Thread); + + if (PreviousSuspendCount) { + *PreviousSuspendCount = PrevSuspendCount; + } + + RETURN(X_STATUS_SUCCESS); } // ****************************************************************** @@ -1971,7 +1976,7 @@ XBSYSAPI EXPORTNUM(228) xbox::ntstatus_xt NTAPI xbox::NtSetSystemTime ret = STATUS_ACCESS_VIOLATION; } else { - NtSystemTimeMtx.lock(); + RtlEnterCriticalSectionAndRegion(&NtSystemTimeCritSec); NewSystemTime = *SystemTime; if (NewSystemTime.u.HighPart > 0 && NewSystemTime.u.HighPart <= 0x20000000) { /* Convert the time and set it in HAL */ @@ -1991,7 +1996,7 @@ XBSYSAPI EXPORTNUM(228) xbox::ntstatus_xt NTAPI xbox::NtSetSystemTime else { ret = STATUS_INVALID_PARAMETER; } - NtSystemTimeMtx.unlock(); + RtlLeaveCriticalSectionAndRegion(&NtSystemTimeCritSec); } RETURN(ret); @@ -2079,15 +2084,30 @@ XBSYSAPI EXPORTNUM(231) xbox::ntstatus_xt NTAPI xbox::NtSuspendThread LOG_FUNC_ARG_OUT(PreviousSuspendCount) LOG_FUNC_END; - if (const auto &nativeHandle = GetNativeHandle(ThreadHandle)) { - // Thread handles are created by ob - RETURN(NtDll::NtSuspendThread(*nativeHandle, (::PULONG)PreviousSuspendCount)); - } - else { - RETURN(X_STATUS_INVALID_HANDLE); + PETHREAD Thread; + ntstatus_xt result = ObReferenceObjectByHandle(ThreadHandle, &PsThreadObjectType, reinterpret_cast(&Thread)); + if (!X_NT_SUCCESS(result)) { + RETURN(result); } - // TODO : Once we do our own thread-switching, implement NtSuspendThread using KeSuspendThread + if (Thread != PspGetCurrentThread()) { + if (Thread->Tcb.HasTerminated) { + ObfDereferenceObject(Thread); + RETURN(X_STATUS_THREAD_IS_TERMINATING); + } + } + + ulong_xt PrevSuspendCount = KeSuspendThread(&Thread->Tcb); + ObfDereferenceObject(Thread); + if (PrevSuspendCount == X_STATUS_SUSPEND_COUNT_EXCEEDED) { + RETURN(X_STATUS_SUSPEND_COUNT_EXCEEDED); + } + + if (PreviousSuspendCount) { + *PreviousSuspendCount = PrevSuspendCount; + } + + RETURN(X_STATUS_SUCCESS); } // ****************************************************************** @@ -2201,15 +2221,23 @@ XBSYSAPI EXPORTNUM(235) xbox::ntstatus_xt NTAPI xbox::NtWaitForMultipleObjectsEx if (const auto &nativeHandle = GetNativeHandle(Handles[i])) { // This is a ob handle, so replace it with its native counterpart nativeHandles[i] = *nativeHandle; + EmuLog(LOG_LEVEL::DEBUG, "xbox handle: %p", nativeHandles[i]); } else { nativeHandles[i] = Handles[i]; + EmuLog(LOG_LEVEL::DEBUG, "native handle: %p", nativeHandles[i]); } } // Because user APCs from NtQueueApcThread are now handled by the kernel, we need to wait for them ourselves - xbox::ntstatus_xt ret = WaitApc([Count, &nativeHandles, WaitType, Alertable]() -> std::optional { + PKTHREAD kThread = KeGetCurrentThread(); + kThread->WaitStatus = X_STATUS_SUCCESS; + if (!AddWaitObject(kThread, Timeout)) { + RETURN(X_STATUS_TIMEOUT); + } + + xbox::ntstatus_xt ret = WaitApc([Count, &nativeHandles, WaitType, Alertable](xbox::PKTHREAD kThread) -> std::optional { NtDll::LARGE_INTEGER ExpireTime; ExpireTime.QuadPart = 0; NTSTATUS Status = NtDll::NtWaitForMultipleObjects( @@ -2221,8 +2249,11 @@ XBSYSAPI EXPORTNUM(235) xbox::ntstatus_xt NTAPI xbox::NtWaitForMultipleObjectsEx if (Status == STATUS_TIMEOUT) { return std::nullopt; } - return std::make_optional(Status); - }, Timeout, Alertable, WaitMode); + // If the wait was satisfied with the host, then also unwait the thread on the guest side, to be sure to remove WaitBlocks that might have been added + // to the thread. Test case: Steel Battalion + xbox::KiUnwaitThreadAndLock(kThread, Status, 0); + return std::make_optional(kThread->WaitStatus); + }, Timeout, Alertable, WaitMode, kThread); RETURN(ret); } diff --git a/src/core/kernel/exports/EmuKrnlPs.cpp b/src/core/kernel/exports/EmuKrnlPs.cpp index 9c0a61ef3..8084ed97d 100644 --- a/src/core/kernel/exports/EmuKrnlPs.cpp +++ b/src/core/kernel/exports/EmuKrnlPs.cpp @@ -121,6 +121,8 @@ static unsigned int WINAPI PCSTProxy params.Ethread, params.TlsDataSize); + xbox::KiExecuteKernelApc(); + auto routine = (xbox::PKSYSTEM_ROUTINE)StartFrame->SystemRoutine; // Debugging notice : When the below line shows up with an Exception dialog and a // message like: "Exception thrown at 0x00026190 in cxbx.exe: 0xC0000005: Access @@ -406,13 +408,24 @@ XBSYSAPI EXPORTNUM(255) xbox::ntstatus_xt NTAPI xbox::PsCreateSystemThreadEx assert(dupHandle); RegisterXboxHandle(eThread->UniqueThread, dupHandle); + eThread->Tcb.Priority = GetThreadPriority(handle); g_AffinityPolicy->SetAffinityXbox(handle); - // Now that ThreadId is populated and affinity is changed, resume the thread (unless the guest passed CREATE_SUSPENDED) - if (!CreateSuspended) { - ResumeThread(handle); + // Wait for the initialization of the remaining thread state + KeSuspendThreadEx(&eThread->Tcb); + ResumeThread(handle); + while (eThread->Tcb.State == Initialized) { + std::this_thread::yield(); } + // Now that ThreadId is populated and affinity is changed, resume the thread (unless the guest passed CREATE_SUSPENDED), then wait until the new thread has + // finished initialization + if (CreateSuspended) { + KeSuspendThread(&eThread->Tcb); + } + + KeResumeThreadEx(&eThread->Tcb); + // Log ThreadID identical to how GetCurrentThreadID() is rendered : EmuLog(LOG_LEVEL::DEBUG, "Created Xbox proxy thread. Handle : 0x%X, ThreadId : [0x%.4X], Native Handle : 0x%X, Native ThreadId : [0x%.4X]", *ThreadHandle, eThread->UniqueThread, handle, ThreadId); @@ -493,10 +506,13 @@ XBSYSAPI EXPORTNUM(258) xbox::void_xt NTAPI xbox::PsTerminateSystemThread KeQuerySystemTime(&eThread->ExitTime); eThread->ExitStatus = ExitStatus; eThread->Tcb.Header.SignalState = 1; + KiWaitListLock(); if (!IsListEmpty(&eThread->Tcb.Header.WaitListHead)) { - // TODO: Implement KiWaitTest's relative objects usage - //KiWaitTest() - assert(0); + KiWaitTest((PVOID)&eThread->Tcb, 0); + std::this_thread::yield(); + } + else { + KiWaitListUnlock(); } if (GetNativeHandle(eThread->UniqueThread)) { diff --git a/src/core/kernel/exports/EmuKrnlRtl.cpp b/src/core/kernel/exports/EmuKrnlRtl.cpp index b309201f6..00e9d7f61 100644 --- a/src/core/kernel/exports/EmuKrnlRtl.cpp +++ b/src/core/kernel/exports/EmuKrnlRtl.cpp @@ -89,6 +89,11 @@ xbox::boolean_xt RtlpCaptureStackLimits( return TRUE; } +xbox::void_xt xbox::RtlInitSystem() +{ + xbox::RtlInitializeCriticalSection(&NtSystemTimeCritSec); +} + // ****************************************************************** // * 0x0104 - RtlAnsiStringToUnicodeString() // ****************************************************************** diff --git a/src/core/kernel/init/CxbxKrnl.cpp b/src/core/kernel/init/CxbxKrnl.cpp index d0447f83b..fd9dc2af5 100644 --- a/src/core/kernel/init/CxbxKrnl.cpp +++ b/src/core/kernel/init/CxbxKrnl.cpp @@ -99,9 +99,6 @@ bool g_bIsChihiro = false; bool g_bIsDevKit = false; bool g_bIsRetail = false; -// Indicates to disable/enable all interrupts when cli and sti instructions are executed -std::atomic_bool g_bEnableAllInterrupts = true; - // Set by the VMManager during initialization. Exported because it's needed in other parts of the emu size_t g_SystemMaxMemory = 0; @@ -113,6 +110,8 @@ ULONG g_CxbxFatalErrorCode = FATAL_ERROR_NONE; // Define function located in EmuXApi so we can call it from here void SetupXboxDeviceTypes(); +extern xbox::void_xt NTAPI system_events(xbox::PVOID arg); + void SetupPerTitleKeys() { // Generate per-title keys from the XBE Certificate @@ -329,64 +328,6 @@ void InitSoftwareInterrupts() } #endif -static xbox::void_xt NTAPI CxbxKrnlInterruptThread(xbox::PVOID param) -{ - CxbxSetThreadName("CxbxKrnl Interrupts"); - -#if 0 - InitSoftwareInterrupts(); -#endif - - std::mutex m; - std::unique_lock lock(m); - while (true) { - for (int i = 0; i < MAX_BUS_INTERRUPT_LEVEL; i++) { - // If the interrupt is pending and connected, process it - if (HalSystemInterrupts[i].IsPending() && EmuInterruptList[i] && EmuInterruptList[i]->Connected) { - HalSystemInterrupts[i].Trigger(EmuInterruptList[i]); - } - } - g_InterruptSignal.wait(lock, []() { return g_AnyInterruptAsserted.load() && g_bEnableAllInterrupts.load(); }); - g_AnyInterruptAsserted = false; - } - - assert(0); -} - -static void CxbxKrnlClockThread(void* pVoid) -{ - LARGE_INTEGER CurrentTicks; - uint64_t Delta; - uint64_t Microseconds; - unsigned int IncrementScaling; - static uint64_t LastTicks = 0; - static uint64_t Error = 0; - static uint64_t UnaccountedMicroseconds = 0; - - // This keeps track of how many us have elapsed between two cycles, so that the xbox clocks are updated - // with the proper increment (instead of blindly adding a single increment at every step) - - if (LastTicks == 0) { - QueryPerformanceCounter(&CurrentTicks); - LastTicks = CurrentTicks.QuadPart; - CurrentTicks.QuadPart = 0; - } - - QueryPerformanceCounter(&CurrentTicks); - Delta = CurrentTicks.QuadPart - LastTicks; - LastTicks = CurrentTicks.QuadPart; - - Error += (Delta * SCALE_S_IN_US); - Microseconds = Error / HostQPCFrequency; - Error -= (Microseconds * HostQPCFrequency); - - UnaccountedMicroseconds += Microseconds; - IncrementScaling = (unsigned int)(UnaccountedMicroseconds / 1000); // -> 1 ms = 1000us -> time between two xbox clock interrupts - UnaccountedMicroseconds -= (IncrementScaling * 1000); - - xbox::KiClockIsr(IncrementScaling); -} - void MapThunkTable(uint32_t* kt, uint32_t* pThunkTable) { const bool SendDebugReports = (pThunkTable == CxbxKrnl_KernelThunkTable) && CxbxDebugger::CanReport(); @@ -1220,7 +1161,7 @@ static void CxbxrKrnlInitHacks() g_pCertificate = &CxbxKrnl_Xbe->m_Certificate; // Initialize timer subsystem - Timer_Init(); + timer_init(); // for unicode conversions setlocale(LC_ALL, "English"); // Initialize DPC global @@ -1362,10 +1303,11 @@ static void CxbxrKrnlInitHacks() } xbox::PsInitSystem(); xbox::KiInitSystem(); + xbox::RtlInitSystem(); // initialize graphics EmuLogInit(LOG_LEVEL::DEBUG, "Initializing render window."); - CxbxInitWindow(true); + CxbxInitWindow(); // Now process the boot flags to see if there are any special conditions to handle if (BootFlags & BOOT_EJECT_PENDING) {} // TODO @@ -1474,23 +1416,17 @@ static void CxbxrKrnlInitHacks() #endif EmuX86_Init(); - // Create the interrupt processing thread + // Start the event thread xbox::HANDLE hThread; - CxbxrCreateThread(&hThread, xbox::zeroptr, CxbxKrnlInterruptThread, xbox::zeroptr, FALSE); - // Start the kernel clock thread - TimerObject* KernelClockThr = Timer_Create(CxbxKrnlClockThread, nullptr, "Kernel clock thread", true); - Timer_Start(KernelClockThr, SCALE_MS_IN_NS); - + xbox::PsCreateSystemThread(&hThread, xbox::zeroptr, system_events, xbox::zeroptr, FALSE); + // Launch the xbe xbox::PsCreateSystemThread(&hThread, xbox::zeroptr, CxbxLaunchXbe, Entry, FALSE); - EmuKeFreePcr(); - __asm add esp, Host2XbStackSizeReserved; - - // This will wait forever - std::condition_variable cv; - std::mutex m; - std::unique_lock lock(m); - cv.wait(lock, [] { return false; }); + xbox::KeRaiseIrqlToDpcLevel(); + while (true) { + xbox::KeWaitForDpc(); + ExecuteDpcQueue(); + } } // REMARK: the following is useless, but PatrickvL has asked to keep it for documentation purposes diff --git a/src/core/kernel/init/CxbxKrnl.h b/src/core/kernel/init/CxbxKrnl.h index c11af39cc..6269bc9fa 100644 --- a/src/core/kernel/init/CxbxKrnl.h +++ b/src/core/kernel/init/CxbxKrnl.h @@ -157,6 +157,7 @@ void CxbxKrnlNoFunc(); void InitDpcData(); // Implemented in EmuKrnlKe.cpp bool IsDpcActive(); +void ExecuteDpcQueue(); /*! kernel thunk table */ extern uint32_t CxbxKrnl_KernelThunkTable[379]; diff --git a/src/core/kernel/support/Emu.cpp b/src/core/kernel/support/Emu.cpp index da98ab08f..45f697d85 100644 --- a/src/core/kernel/support/Emu.cpp +++ b/src/core/kernel/support/Emu.cpp @@ -49,11 +49,6 @@ bool g_DisablePixelShaders = false; bool g_UseAllCores = false; bool g_SkipRdtscPatching = false; -// Delta added to host SystemTime, used in KiClockIsr and KeSetSystemTime -// This shouldn't need to be atomic, but because raising the IRQL to high lv in KeSetSystemTime doesn't really stop KiClockIsr from running, -// we need it for now to prevent reading a corrupted value while KeSetSystemTime is in the middle of updating it -std::atomic_int64_t HostSystemTimeDelta(0); - // Static Function(s) static int ExitException(LPEXCEPTION_POINTERS e); diff --git a/src/core/kernel/support/Emu.h b/src/core/kernel/support/Emu.h index d12f493ab..2087adc84 100644 --- a/src/core/kernel/support/Emu.h +++ b/src/core/kernel/support/Emu.h @@ -68,9 +68,6 @@ extern HWND g_hEmuWindow; extern HANDLE g_CurrentProcessHandle; // Set in CxbxKrnlMain -// Delta added to host SystemTime, used in KiClockIsr and KeSetSystemTime -extern std::atomic_int64_t HostSystemTimeDelta; - typedef struct DUMMY_KERNEL { IMAGE_DOS_HEADER DosHeader; diff --git a/src/devices/Xbox.cpp b/src/devices/Xbox.cpp index 3d81343db..467246e05 100644 --- a/src/devices/Xbox.cpp +++ b/src/devices/Xbox.cpp @@ -158,9 +158,7 @@ void InitXboxHardware(HardwareModel hardwareModel) g_NVNet = new NVNetDevice(); g_NV2A = new NV2ADevice(); g_ADM1032 = new ADM1032Device(); - if (bLLE_USB) { - g_USB0 = new USBDevice(); - } + g_USB0 = new USBDevice(); // Connect devices to SM bus g_SMBus->ConnectDevice(SMBUS_ADDRESS_SYSTEM_MICRO_CONTROLLER, g_SMC); // W 0x20 R 0x21 @@ -189,14 +187,12 @@ void InitXboxHardware(HardwareModel hardwareModel) //g_PCIBus->ConnectDevice(PCI_DEVID(0, PCI_DEVFN(5, 0)), g_NVAPU); //g_PCIBus->ConnectDevice(PCI_DEVID(0, PCI_DEVFN(6, 0)), g_AC97); g_PCIBus->ConnectDevice(PCI_DEVID(1, PCI_DEVFN(0, 0)), g_NV2A); - if (bLLE_USB) { - // ergo720: according to some research done by LukeUsher, only Xbox Alpha Kits have a two HCs configuration. This seems to also be confirmed by the xboxdevwiki, - // which states that it has a xircom PGPCI2(OPTI 82C861) 2 USB port PCI card -> 2 ports, not 4. Finally, I disassembled various xbe's and discovered that the number - // of ports per HC is hardcoded as 4 in the driver instead of being detected at runtime by reading the HcRhDescriptorA register and so a game would have to be - // recompiled to support 2 HCs, which further confirms the point. Because we are not going to emulate an Alpha Kit, we can simply ignore the USB1 device. + // ergo720: according to some research done by LukeUsher, only Xbox Alpha Kits have a two HCs configuration. This seems to also be confirmed by the xboxdevwiki, + // which states that it has a xircom PGPCI2(OPTI 82C861) 2 USB port PCI card -> 2 ports, not 4. Finally, I disassembled various xbe's and discovered that the number + // of ports per HC is hardcoded as 4 in the driver instead of being detected at runtime by reading the HcRhDescriptorA register and so a game would have to be + // recompiled to support 2 HCs, which further confirms the point. Because we are not going to emulate an Alpha Kit, we can simply ignore the USB1 device. - g_PCIBus->ConnectDevice(PCI_DEVID(0, PCI_DEVFN(2, 0)), g_USB0); - } + g_PCIBus->ConnectDevice(PCI_DEVID(0, PCI_DEVFN(2, 0)), g_USB0); // TODO : Handle other SMBUS Addresses, like PIC_ADDRESS, XCALIBUR_ADDRESS // Resources : http://pablot.com/misc/fancontroller.cpp diff --git a/src/devices/network/NVNetDevice.cpp b/src/devices/network/NVNetDevice.cpp index 10bde6a27..8b6b62740 100644 --- a/src/devices/network/NVNetDevice.cpp +++ b/src/devices/network/NVNetDevice.cpp @@ -476,15 +476,18 @@ void EmuNVNet_Write(xbox::addr_xt addr, uint32_t value, int size) } std::thread NVNetRecvThread; -static void NVNetRecvThreadProc(NvNetState_t *s) +void NVNetRecvThreadProc() { + // NOTE: profiling shows that the winpcap function can take up to 1/6th of the total cpu time of the loader process, so avoid placing + // this function in system_events g_AffinityPolicy->SetAffinityOther(); - uint8_t packet[65536]; + static std::unique_ptr packet(new uint8_t[65536]); while (true) { - int size = g_NVNet->PCAPReceive(packet, 65536); + int size = g_NVNet->PCAPReceive(packet.get(), 65536); if (size > 0) { - EmuNVNet_DMAPacketToGuest(packet, size); - } + EmuNVNet_DMAPacketToGuest(packet.get(), size); + } + _mm_pause(); } } @@ -527,7 +530,7 @@ void NVNetDevice::Init() }; PCAPInit(); - NVNetRecvThread = std::thread(NVNetRecvThreadProc, &NvNetState); + NVNetRecvThread = std::thread(NVNetRecvThreadProc); } void NVNetDevice::Reset() diff --git a/src/devices/usb/OHCI.cpp b/src/devices/usb/OHCI.cpp index 875aae5d1..3e0253ba2 100644 --- a/src/devices/usb/OHCI.cpp +++ b/src/devices/usb/OHCI.cpp @@ -279,11 +279,6 @@ OHCI::OHCI(USBDevice* UsbObj) OHCI_StateReset(); } -void OHCI::OHCI_FrameBoundaryWrapper(void* pVoid) -{ - static_cast(pVoid)->OHCI_FrameBoundaryWorker(); -} - void OHCI::OHCI_FrameBoundaryWorker() { OHCI_HCCA hcca; @@ -358,7 +353,7 @@ void OHCI::OHCI_FrameBoundaryWorker() } // Do SOF stuff here - OHCI_SOF(false); + OHCI_SOF(); // Writeback HCCA if (OHCI_WriteHCCA(m_Registers.HcHCCA, &hcca)) { @@ -877,32 +872,23 @@ void OHCI::OHCI_StateReset() void OHCI::OHCI_BusStart() { // Create the EOF timer. - m_pEOFtimer = Timer_Create(OHCI_FrameBoundaryWrapper, this, "", false); + m_pEOFtimer = true; EmuLog(LOG_LEVEL::DEBUG, "Operational event"); // SOF event - OHCI_SOF(true); + OHCI_SOF(); } void OHCI::OHCI_BusStop() { - if (m_pEOFtimer) { - // Delete existing EOF timer - Timer_Exit(m_pEOFtimer); - } - m_pEOFtimer = nullptr; + m_pEOFtimer = false; } -void OHCI::OHCI_SOF(bool bCreate) +void OHCI::OHCI_SOF() { // set current SOF time - m_SOFtime = GetTime_NS(m_pEOFtimer); - - // make timer expire at SOF + 1 ms from now - if (bCreate) { - Timer_Start(m_pEOFtimer, m_UsbFrameTime); - } + m_SOFtime = get_now(); OHCI_SetInterrupt(OHCI_INTR_SF); } @@ -1254,6 +1240,23 @@ void OHCI::OHCI_WriteRegister(xbox::addr_xt Addr, uint32_t Value) } } +uint64_t OHCI::OHCI_next(uint64_t now) +{ + if (m_pEOFtimer) { + constexpr uint64_t ohci_period = 1000; + uint64_t next = m_SOFtime + ohci_period; + + if (now >= next) { + OHCI_FrameBoundaryWorker(); + return ohci_period; + } + + return m_SOFtime + ohci_period - now; // time remaining until EOF + } + + return -1; +} + void OHCI::OHCI_UpdateInterrupt() { if ((m_Registers.HcInterrupt & OHCI_INTR_MIE) && (m_Registers.HcInterruptStatus & m_Registers.HcInterrupt)) { @@ -1278,7 +1281,7 @@ uint32_t OHCI::OHCI_GetFrameRemaining() } // Being in USB operational state guarantees that m_pEOFtimer and m_SOFtime were set already - ticks = GetTime_NS(m_pEOFtimer) - m_SOFtime; + ticks = get_now() - m_SOFtime; // Avoid Muldiv64 if possible if (ticks >= m_UsbFrameTime) { diff --git a/src/devices/usb/OHCI.h b/src/devices/usb/OHCI.h index 92119ba0f..3555667df 100644 --- a/src/devices/usb/OHCI.h +++ b/src/devices/usb/OHCI.h @@ -145,6 +145,10 @@ class OHCI uint32_t OHCI_ReadRegister(xbox::addr_xt Addr); // write a register void OHCI_WriteRegister(xbox::addr_xt Addr, uint32_t Value); + // calculates when the next EOF is due + uint64_t OHCI_next(uint64_t now); + // EOF callback function + void OHCI_FrameBoundaryWorker(); private: @@ -153,7 +157,7 @@ class OHCI // all the registers available in the OHCI standard OHCI_Registers m_Registers; // end-of-frame timer - TimerObject* m_pEOFtimer = nullptr; + bool m_pEOFtimer = false; // time at which a SOF was sent uint64_t m_SOFtime; // the duration of a usb frame @@ -173,10 +177,6 @@ class OHCI // indicates if there is a pending asynchronous packet to process int m_AsyncComplete = 0; - // EOF callback wrapper - static void OHCI_FrameBoundaryWrapper(void* pVoid); - // EOF callback function - void OHCI_FrameBoundaryWorker(); // inform the HCD that we got a problem here... void OHCI_FatalError(); // initialize packet struct @@ -189,8 +189,8 @@ class OHCI void OHCI_BusStart(); // stop sending SOF tokens across the usb bus void OHCI_BusStop(); - // generate a SOF event, and start a timer for EOF - void OHCI_SOF(bool bCreate); + // generate a SOF event + void OHCI_SOF(); // change interrupt status void OHCI_UpdateInterrupt(); // fire an interrupt diff --git a/src/devices/video/EmuNV2A_PRAMDAC.cpp b/src/devices/video/EmuNV2A_PRAMDAC.cpp index 768811e3a..c0e9db80f 100644 --- a/src/devices/video/EmuNV2A_PRAMDAC.cpp +++ b/src/devices/video/EmuNV2A_PRAMDAC.cpp @@ -82,6 +82,7 @@ DEVICE_WRITE32(PRAMDAC) } else { d->pramdac.core_clock_freq = (NV2A_CRYSTAL_FREQ * n) / (1 << p) / m; + d->ptimer_period = ((uint64_t(d->ptimer.alarm_time >> 5) * SCALE_S_IN_US) / d->pramdac.core_clock_freq); } break; diff --git a/src/devices/video/EmuNV2A_PTIMER.cpp b/src/devices/video/EmuNV2A_PTIMER.cpp index d6f6f42f5..5711537b8 100644 --- a/src/devices/video/EmuNV2A_PTIMER.cpp +++ b/src/devices/video/EmuNV2A_PTIMER.cpp @@ -35,17 +35,13 @@ #include "common\util\CxbxUtil.h" -#define NANOSECONDS_PER_SECOND 1000000000 /* PTIMER - time measurement and time-based alarms */ -static uint64_t ptimer_get_clock(NV2AState * d) +static uint64_t ptimer_get_clock(NV2AState *d) { - // Get time in nanoseconds - uint64_t time = std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); - - return Muldiv64(Muldiv64(time, + return Muldiv64(Muldiv64(get_now(), (uint32_t)d->pramdac.core_clock_freq, // TODO : Research how this can be updated to accept uint64_t - NANOSECONDS_PER_SECOND), // Was CLOCKS_PER_SEC + SCALE_S_IN_US), // Was CLOCKS_PER_SEC d->ptimer.denominator, d->ptimer.numerator); } @@ -91,6 +87,13 @@ DEVICE_WRITE32(PTIMER) break; case NV_PTIMER_INTR_EN_0: d->ptimer.enabled_interrupts = value; + if (d->ptimer.enabled_interrupts & NV_PTIMER_INTR_EN_0_ALARM) { + d->ptimer_last = get_now(); + d->ptimer_active = true; + } + else if ((d->ptimer.enabled_interrupts & NV_PTIMER_INTR_EN_0_ALARM) == 0) { + d->ptimer_active = false; + } update_irq(d); break; case NV_PTIMER_DENOMINATOR: @@ -101,6 +104,7 @@ DEVICE_WRITE32(PTIMER) break; case NV_PTIMER_ALARM_0: d->ptimer.alarm_time = value; + d->ptimer_period = ((uint64_t(d->ptimer.alarm_time >> 5) * SCALE_S_IN_US) / d->pramdac.core_clock_freq); break; default: //DEVICE_WRITE32_REG(ptimer); // Was : DEBUG_WRITE32_UNHANDLED(PTIMER); diff --git a/src/devices/video/nv2a.cpp b/src/devices/video/nv2a.cpp index 6c0e62aa0..a2a13397e 100644 --- a/src/devices/video/nv2a.cpp +++ b/src/devices/video/nv2a.cpp @@ -51,6 +51,7 @@ #include "core\kernel\init\CxbxKrnl.h" // For XBOX_MEMORY_SIZE, DWORD, etc #include "core\kernel\support\Emu.h" +#include "core\kernel\support\NativeHandle.h" #include "core\kernel\exports\EmuKrnl.h" #include #include @@ -58,6 +59,7 @@ #include "core\hle\Intercept.hpp" #include "common/win32/Threads.h" #include "Logging.h" +#include "Timer.h" #include "vga.h" #include "nv2a.h" // For NV2AState @@ -128,6 +130,14 @@ static void update_irq(NV2AState *d) d->pmc.pending_interrupts &= ~NV_PMC_INTR_0_PVIDEO; } + /* PTIMER */ + if (d->ptimer.pending_interrupts & d->ptimer.enabled_interrupts) { + d->pmc.pending_interrupts |= NV_PMC_INTR_0_PTIMER; + } + else { + d->pmc.pending_interrupts &= ~NV_PMC_INTR_0_PTIMER; + } + /* TODO : PBUS * / if (d->pbus.pending_interrupts & d->pbus.enabled_interrupts) { d->pmc.pending_interrupts |= NV_PMC_INTR_0_PBUS; @@ -319,8 +329,8 @@ const NV2ABlockInfo* EmuNV2A_Block(xbox::addr_xt addr) // HACK: Until we implement VGA/proper interrupt generation // we simulate VBLANK by calling the interrupt at 60Hz -std::thread vblank_thread; extern std::chrono::steady_clock::time_point GetNextVBlankTime(); +extern void hle_vblank(); void _check_gl_reset() { @@ -1097,25 +1107,27 @@ void NV2ADevice::UpdateHostDisplay(NV2AState *d) } // TODO: Fix this properly -static void nv2a_vblank_thread(NV2AState *d) +template +void nv2a_vblank_interrupt(void *opaque) { - g_AffinityPolicy->SetAffinityOther(); - CxbxSetThreadName("Cxbx NV2A VBLANK"); - auto nextVBlankTime = GetNextVBlankTime(); + NV2AState *d = static_cast(opaque); - while (!d->exiting) { - // Handle VBlank - if (std::chrono::steady_clock::now() > nextVBlankTime) { - d->pcrtc.pending_interrupts |= NV_PCRTC_INTR_0_VBLANK; - update_irq(d); - nextVBlankTime = GetNextVBlankTime(); + if (!d->exiting) [[likely]] { + d->pcrtc.pending_interrupts |= NV_PCRTC_INTR_0_VBLANK; + update_irq(d); - // TODO: We should swap here for the purposes of supporting overlays + direct framebuffer access - // But it causes crashes on AMD hardware for reasons currently unknown... - //NV2ADevice::UpdateHostDisplay(d); + // trigger the gpu interrupt if it was asserted in update_irq + if (g_bEnableAllInterrupts && HalSystemInterrupts[3].IsPending() && EmuInterruptList[3] && EmuInterruptList[3]->Connected) { + HalSystemInterrupts[3].Trigger(EmuInterruptList[3]); } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + // TODO: We should swap here for the purposes of supporting overlays + direct framebuffer access + // But it causes crashes on AMD hardware for reasons currently unknown... + //NV2ADevice::UpdateHostDisplay(d); + + if constexpr (should_update_hle) { + hle_vblank(); + } } } @@ -1189,8 +1201,8 @@ void NV2ADevice::Init() d->vram_ptr = (uint8_t*)PHYSICAL_MAP_BASE; d->vram_size = g_SystemMaxMemory; - d->pramdac.core_clock_coeff = 0x00011c01; /* 189MHz...? */ - d->pramdac.core_clock_freq = 189000000; + d->pramdac.core_clock_coeff = 0x00011C01; /* 233MHz...? */ + d->pramdac.core_clock_freq = 233333324; d->pramdac.memory_clock_coeff = 0; d->pramdac.video_clock_coeff = 0x0003C20D; /* 25182Khz...? */ @@ -1202,7 +1214,13 @@ void NV2ADevice::Init() pvideo_init(d); } - vblank_thread = std::thread(nv2a_vblank_thread, d); + d->vblank_last = get_now(); + if (bLLE_GPU) { + d->vblank_cb = nv2a_vblank_interrupt; + } + else { + d->vblank_cb = nv2a_vblank_interrupt; + } qemu_mutex_init(&d->pfifo.pfifo_lock); qemu_cond_init(&d->pfifo.puller_cond); @@ -1227,9 +1245,8 @@ void NV2ADevice::Reset() qemu_cond_broadcast(&d->pfifo.pusher_cond); d->pfifo.puller_thread.join(); d->pfifo.pusher_thread.join(); - qemu_mutex_destroy(&d->pfifo.pfifo_lock); // Cbxbx addition + qemu_mutex_destroy(&d->pfifo.pfifo_lock); // Cxbxr addition if (d->pgraph.opengl_enabled) { - vblank_thread.join(); pvideo_destroy(d); } @@ -1377,3 +1394,45 @@ int NV2ADevice::GetFrameWidth(NV2AState* d) return width; } + +uint64_t NV2ADevice::vblank_next(uint64_t now) +{ + // TODO: this should use a vblank period of 20ms when we are in 50Hz PAL mode + constexpr uint64_t vblank_period = 16.6666666667 * 1000; + uint64_t next = m_nv2a_state->vblank_last + vblank_period; + + if (now >= next) { + m_nv2a_state->vblank_cb(m_nv2a_state); + m_nv2a_state->vblank_last = get_now(); + return vblank_period; + } + + return m_nv2a_state->vblank_last + vblank_period - now; // time remaining until next vblank +} + +uint64_t NV2ADevice::ptimer_next(uint64_t now) +{ + // Test case: Dead or Alive Ultimate uses this when in PAL50 mode only + if (m_nv2a_state->ptimer_active) { + const uint64_t ptimer_period = m_nv2a_state->ptimer_period; + uint64_t next = m_nv2a_state->ptimer_last + ptimer_period; + + if (now >= next) { + if (!m_nv2a_state->exiting) [[likely]] { + m_nv2a_state->ptimer.pending_interrupts |= NV_PTIMER_INTR_0_ALARM; + update_irq(m_nv2a_state); + + // trigger the gpu interrupt if it was asserted in update_irq + if (g_bEnableAllInterrupts && HalSystemInterrupts[3].IsPending() && EmuInterruptList[3] && EmuInterruptList[3]->Connected) { + HalSystemInterrupts[3].Trigger(EmuInterruptList[3]); + } + } + m_nv2a_state->ptimer_last = get_now(); + return ptimer_period; + } + + return m_nv2a_state->ptimer_last + ptimer_period - now; // time remaining until next ptimer interrupt + } + + return -1; +} diff --git a/src/devices/video/nv2a.h b/src/devices/video/nv2a.h index 43b64c052..03fc3c81a 100644 --- a/src/devices/video/nv2a.h +++ b/src/devices/video/nv2a.h @@ -108,6 +108,10 @@ public: static int GetFrameWidth(NV2AState *d); static int GetFrameHeight(NV2AState *d); + + uint64_t vblank_next(uint64_t now); + uint64_t ptimer_next(uint64_t now); + private: NV2AState *m_nv2a_state; }; diff --git a/src/devices/video/nv2a_int.h b/src/devices/video/nv2a_int.h index 22630b146..ceef33c73 100644 --- a/src/devices/video/nv2a_int.h +++ b/src/devices/video/nv2a_int.h @@ -357,10 +357,15 @@ typedef struct OverlayState { } OverlayState; typedef struct NV2AState { + void(* vblank_cb)(void *); + uint64_t vblank_last; // PCIDevice dev; // qemu_irq irq; bool exiting; bool enable_overlay = false; + bool ptimer_active = false; + uint64_t ptimer_last; + uint64_t ptimer_period; // VGACommonState vga; // GraphicHwOps hw_ops; diff --git a/src/gui/WndMain.cpp b/src/gui/WndMain.cpp index 28f071c53..6f26b2628 100644 --- a/src/gui/WndMain.cpp +++ b/src/gui/WndMain.cpp @@ -394,6 +394,13 @@ LRESULT CALLBACK WndMain::WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lP }; break; // added per PVS suggestion. + case WM_TIMECHANGE: + { + ipc_send_kernel_update(IPC_UPDATE_KERNEL::CONFIG_CHANGE_TIME, 0, reinterpret_cast(m_hwndChild)); + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + break; + case WM_TIMER: { switch (wParam) diff --git a/src/gui/resource/ResCxbx.h b/src/gui/resource/ResCxbx.h index 927737d14..eff2e721a 100644 --- a/src/gui/resource/ResCxbx.h +++ b/src/gui/resource/ResCxbx.h @@ -353,6 +353,7 @@ #define ID_SETTINGS_EXPERIMENTAL 40113 #define ID_SETTINGS_IGNOREINVALIDXBESIG 40114 #define ID_SETTINGS_IGNOREINVALIDXBESEC 40115 +#define ID_SYNC_TIME_CHANGE 40116 #define IDC_STATIC -1 // Next default values for new objects @@ -360,7 +361,7 @@ #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 139 -#define _APS_NEXT_COMMAND_VALUE 40116 +#define _APS_NEXT_COMMAND_VALUE 40117 #define _APS_NEXT_CONTROL_VALUE 1308 #define _APS_NEXT_SYMED_VALUE 109 #endif