From 44f2e670783e63cfe1ed58052bceb5bb3df12e16 Mon Sep 17 00:00:00 2001 From: Justin Marshall Date: Thu, 14 May 2026 18:37:41 -0700 Subject: [PATCH] New editor tabs. --- neo/engine/doomdll.vcxproj.user | 4 +- neo/engine/framework/Licensee.h | 2 + neo/engine/idlib/precompiled.h | 3 +- neo/engine/opengl/DirectInputShim.cpp | 1633 ++++++++++++++++++++++ neo/engine/opengl/DirectInputShim.h | 28 + neo/engine/opengl/gl_d3d12shim.cpp | 86 +- neo/engine/opengl/opengl.h | 4 +- neo/engine/opengl/opengl.vcxproj | 2 + neo/engine/opengl/opengl.vcxproj.filters | 2 + neo/engine/sys/win32/win_input.cpp | 2 +- neo/engine/sys/win32/win_wndproc.cpp | 6 + neo/engine/tools/radiant/CamWnd.cpp | 2 +- neo/engine/tools/radiant/MainFrm.cpp | 191 ++- neo/engine/tools/radiant/MainFrm.h | 8 +- neo/engine/tools/radiant/QE3.H | 6 +- neo/engine/tools/radiant/XYWnd.cpp | 741 +++++++++- neo/engine/tools/radiant/XYWnd.h | 99 +- q4base/q4config.qe4 | 18 + 18 files changed, 2775 insertions(+), 62 deletions(-) create mode 100644 neo/engine/opengl/DirectInputShim.cpp create mode 100644 neo/engine/opengl/DirectInputShim.h create mode 100644 q4base/q4config.qe4 diff --git a/neo/engine/doomdll.vcxproj.user b/neo/engine/doomdll.vcxproj.user index 35d47df4..c6d187b5 100644 --- a/neo/engine/doomdll.vcxproj.user +++ b/neo/engine/doomdll.vcxproj.user @@ -15,9 +15,9 @@ D:\projects\Doom3\Doom3.exe WindowsLocalDebugger - +set r_fullscreen 1 +set r_mode 9 + +set r_fullscreen 0 +set r_mode 9 D:\projects\Doom3 - +set r_fullscreen 1 +set r_mode 1|+set r_fullscreen 0 +set r_mode 1|+set r_fullscreen 1 +set r_mode 0|+set r_fullscreen 1 +set r_mode 10|+set r_fullscreen 1 +set r_mode 9| + +set r_fullscreen 0 +set r_mode 1|+set r_fullscreen 1 +set r_mode 0|+set r_fullscreen 1 +set r_mode 10|+set r_fullscreen 1 +set r_mode 9|+set r_fullscreen 0 +set r_mode 9| D:\projects\Doom3\Quake4.exe diff --git a/neo/engine/framework/Licensee.h b/neo/engine/framework/Licensee.h index 824d6eb9..bc8da52c 100644 --- a/neo/engine/framework/Licensee.h +++ b/neo/engine/framework/Licensee.h @@ -63,6 +63,8 @@ If you have questions concerning this license or the applicable additional terms #ifdef PREY #define CONFIG_FILE "PreyConfig.cfg" +#elif defined(QUAKE4) +#define CONFIG_FILE "Q4Config.cfg" #else #define CONFIG_FILE "DoomConfig.cfg" #endif diff --git a/neo/engine/idlib/precompiled.h b/neo/engine/idlib/precompiled.h index f42eec5e..d3b7211e 100644 --- a/neo/engine/idlib/precompiled.h +++ b/neo/engine/idlib/precompiled.h @@ -96,7 +96,8 @@ const float MAX_BOUND_SIZE = 65536.0f; #define DIRECTSOUND_VERSION 0x0800 #include -#include +// #include +#include "../opengl/DirectInputShim.h" #endif /* !GAME_DLL */ #endif /* !_D3SDK */ diff --git a/neo/engine/opengl/DirectInputShim.cpp b/neo/engine/opengl/DirectInputShim.cpp new file mode 100644 index 00000000..fc611e21 --- /dev/null +++ b/neo/engine/opengl/DirectInputShim.cpp @@ -0,0 +1,1633 @@ +#ifndef DIRECTINPUT_VERSION +#define DIRECTINPUT_VERSION 0x0800 +#endif + +#include "DirectInputShim.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef ICE_DI_DEFAULT_BUFFER_SIZE +#define ICE_DI_DEFAULT_BUFFER_SIZE 256 +#endif + +#ifndef ICE_DI_CENTER_MOUSE_WHEN_ACTIVE +#define ICE_DI_CENTER_MOUSE_WHEN_ACTIVE 1 +#endif + +#ifndef ICE_DI_CENTER_WARP_IGNORE_MS +#define ICE_DI_CENTER_WARP_IGNORE_MS 120 +#endif + +#ifndef ICE_DI_CENTER_PIXEL_TOLERANCE +#define ICE_DI_CENTER_PIXEL_TOLERANCE 1 +#endif + +// Forward declarations +class IceDirectInputDevice8; +class IceDirectInput8; + +bool IceDirectInputShim_RegisterRawInput(HWND hwnd); +bool IceDirectInputShim_ForceMouseCenter(); +void IceDirectInputShim_SetMouseTargetWindow(HWND hwnd); +void IceDirectInputShim_SetMouseCenterLock(bool enabled); + +// Globals +static HWND g_rawInputHwnd = NULL; +static HWND g_keyboardTargetHwnd = NULL; +static HWND g_mouseTargetHwnd = NULL; +static IceDirectInputDevice8* g_keyboardDevice = NULL; +static IceDirectInputDevice8* g_mouseDevice = NULL; +static std::mutex g_deviceMutex; +static std::mutex g_windowMutex; +static std::atomic g_iceDI_GameMouseActive(true); +static std::atomic g_iceDI_MouseAcquired(false); +static std::atomic g_iceDI_CenterMouseWhenActive(ICE_DI_CENTER_MOUSE_WHEN_ACTIVE != 0); + +// The shim owns the clip only while the game mouse is active. +static bool g_mouseClipOwned = false; +static RECT g_lastClipRect = { 0, 0, 0, 0 }; + +// Used only to ignore absolute raw-mouse packets generated by our own recentering. +static POINT g_lastCenterWarpPoint = { 0, 0 }; +static DWORD g_lastCenterWarpTick = 0; + +// ------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------ + +static DWORD IceDI_GetTimeStamp() { + return GetTickCount(); +} + +static LONG IceDI_AbsLong(LONG v) { + return v < 0 ? -v : v; +} + +static bool IceDI_IsDInputProp(REFGUID rguidProp, DWORD propId) { + // DirectInput DIPROP_* values are not real GUID objects. + // They are encoded as invalid pointer-sized constants: + // #define MAKEDIPROP(prop) (*(const GUID *)(prop)) + // + // So do NOT compare DIPROP_* with operator== / IsEqualGUID / memcmp. + return (DWORD)(uintptr_t)&rguidProp == propId; +} + +static bool IceDI_IsPressedRawKeyboard(const RAWKEYBOARD& kb) { + return (kb.Flags & RI_KEY_BREAK) == 0; +} + +static bool IceDI_IsUsableWindow(HWND hwnd) { + return hwnd != NULL && ::IsWindow(hwnd) != FALSE; +} + +static bool IceDI_IsVisibleUsableWindow(HWND hwnd) { + return IceDI_IsUsableWindow(hwnd) && ::IsWindowVisible(hwnd) != FALSE; +} + +static bool IceDI_ForegroundOwnsWindow(HWND hwnd) { + if (!IceDI_IsUsableWindow(hwnd)) { + return false; + } + + HWND foreground = ::GetForegroundWindow(); + if (!IceDI_IsUsableWindow(foreground)) { + return false; + } + + if (foreground == hwnd) { + return true; + } + + // Embedded case: the Doom/game HWND can be a child of the editor's + // foreground top-level HWND. This is the case that needs live client-to- + // screen conversion instead of stale cached x/y/resolution. + if (::IsChild(foreground, hwnd)) { + return true; + } + + HWND root = ::GetAncestor(hwnd, GA_ROOT); + if (root != NULL && root == foreground) { + return true; + } + + // Rare, but allow it when a child of our target became foreground. + if (::IsChild(hwnd, foreground)) { + return true; + } + + return false; +} + +static bool IceDI_GetClientScreenRect(HWND hwnd, RECT* outRect) { + if (outRect == NULL || !IceDI_IsUsableWindow(hwnd)) { + return false; + } + + RECT client; + if (!::GetClientRect(hwnd, &client)) { + return false; + } + + if ((client.right - client.left) <= 0 || (client.bottom - client.top) <= 0) { + return false; + } + + POINT pts[2]; + pts[0].x = client.left; + pts[0].y = client.top; + pts[1].x = client.right; + pts[1].y = client.bottom; + + if (!::ClientToScreen(hwnd, &pts[0])) { + return false; + } + + if (!::ClientToScreen(hwnd, &pts[1])) { + return false; + } + + outRect->left = std::min(pts[0].x, pts[1].x); + outRect->top = std::min(pts[0].y, pts[1].y); + outRect->right = std::max(pts[0].x, pts[1].x); + outRect->bottom = std::max(pts[0].y, pts[1].y); + + return (outRect->right > outRect->left) && (outRect->bottom > outRect->top); +} + +static bool IceDI_RectEquals(const RECT& a, const RECT& b) { + return a.left == b.left && a.top == b.top && a.right == b.right && a.bottom == b.bottom; +} + +static HWND IceDI_GetFallbackRawInputWindow_NoLock() { + if (IceDI_IsUsableWindow(g_rawInputHwnd)) { + return g_rawInputHwnd; + } + + if (IceDI_IsUsableWindow(g_mouseTargetHwnd)) { + return g_mouseTargetHwnd; + } + + if (IceDI_IsUsableWindow(g_keyboardTargetHwnd)) { + return g_keyboardTargetHwnd; + } + + return NULL; +} + +static HWND IceDI_GetMouseTargetWindow() { + HWND target = NULL; + HWND fallback = NULL; + + { + std::lock_guard lock(g_windowMutex); + target = g_mouseTargetHwnd; + fallback = IceDI_GetFallbackRawInputWindow_NoLock(); + } + + if (IceDI_IsUsableWindow(target)) { + return target; + } + + if (IceDI_IsUsableWindow(fallback)) { + return fallback; + } + + // Last-ditch fallback for code paths that acquire before setting + // cooperative level. GetFocus only works reliably inside this thread, + // but it is better than retaining a stale hwnd. + HWND focus = ::GetFocus(); + if (IceDI_IsUsableWindow(focus)) { + return focus; + } + + return NULL; +} + +static bool IceDI_ShouldLockMouseToWindow(HWND hwnd) { + if (!g_iceDI_CenterMouseWhenActive.load()) { + return false; + } + + if (!g_iceDI_GameMouseActive.load()) { + return false; + } + + if (!g_iceDI_MouseAcquired.load()) { + return false; + } + + if (!IceDI_IsVisibleUsableWindow(hwnd)) { + return false; + } + + if (!IceDI_ForegroundOwnsWindow(hwnd)) { + return false; + } + + return true; +} + +static void IceDI_RememberCenterWarp(const POINT& center) { + std::lock_guard lock(g_windowMutex); + g_lastCenterWarpPoint = center; + g_lastCenterWarpTick = IceDI_GetTimeStamp(); +} + +static bool IceDI_IsRecentCenterWarpPoint(const POINT& pt) { + POINT lastPoint; + DWORD lastTick; + + { + std::lock_guard lock(g_windowMutex); + lastPoint = g_lastCenterWarpPoint; + lastTick = g_lastCenterWarpTick; + } + + if (lastTick == 0) { + return false; + } + + DWORD now = IceDI_GetTimeStamp(); + if ((DWORD)(now - lastTick) > ICE_DI_CENTER_WARP_IGNORE_MS) { + return false; + } + + return IceDI_AbsLong(pt.x - lastPoint.x) <= 2 && IceDI_AbsLong(pt.y - lastPoint.y) <= 2; +} + +static void IceDI_ClearMouseClipIfOwned() { + bool shouldClear = false; + + { + std::lock_guard lock(g_windowMutex); + shouldClear = g_mouseClipOwned; + g_mouseClipOwned = false; + g_lastClipRect.left = 0; + g_lastClipRect.top = 0; + g_lastClipRect.right = 0; + g_lastClipRect.bottom = 0; + } + + if (shouldClear) { + ::ClipCursor(NULL); + } +} + +static bool IceDI_ApplyMouseClip(const RECT& rect) { + bool needsClip = true; + + { + std::lock_guard lock(g_windowMutex); + needsClip = !g_mouseClipOwned || !IceDI_RectEquals(g_lastClipRect, rect); + g_mouseClipOwned = true; + g_lastClipRect = rect; + } + + // Calling ClipCursor repeatedly is okay, but avoid doing it every poll when + // the rect has not changed. If the editor moves/reparents/resizes the child + // window, the live rect changes and this is applied immediately. + if (needsClip) { + if (!::ClipCursor(&rect)) { + return false; + } + } + + return true; +} + +static bool IceDI_SkipCenter = false; + +void IceDI_ForceOverrideSkipCenter(bool shouldSkip) { + IceDI_SkipCenter = shouldSkip; +} + +static bool IceDI_ForceMouseCenterInternal(bool forceWarp) { + HWND hwnd = IceDI_GetMouseTargetWindow(); + + if (!IceDI_ShouldLockMouseToWindow(hwnd) || IceDI_SkipCenter) { + IceDI_ClearMouseClipIfOwned(); + return false; + } + + RECT clientScreenRect; + if (!IceDI_GetClientScreenRect(hwnd, &clientScreenRect)) { + IceDI_ClearMouseClipIfOwned(); + return false; + } + + POINT center; + center.x = clientScreenRect.left + ((clientScreenRect.right - clientScreenRect.left) / 2); + center.y = clientScreenRect.top + ((clientScreenRect.bottom - clientScreenRect.top) / 2); + + IceDI_ApplyMouseClip(clientScreenRect); + + POINT cursor; + bool haveCursor = (::GetCursorPos(&cursor) != FALSE); + bool outside = !haveCursor || !::PtInRect(&clientScreenRect, cursor); + bool offCenter = !haveCursor || + IceDI_AbsLong(cursor.x - center.x) > ICE_DI_CENTER_PIXEL_TOLERANCE || + IceDI_AbsLong(cursor.y - center.y) > ICE_DI_CENTER_PIXEL_TOLERANCE; + + if (forceWarp || outside || offCenter) { + IceDI_RememberCenterWarp(center); + ::SetCursorPos(center.x, center.y); + } + + return true; +} + +static bool IceDI_RawAbsoluteMouseToScreen(const RAWMOUSE& rm, POINT* outPoint) { + if (outPoint == NULL) { + return false; + } + + int left = 0; + int top = 0; + int width = ::GetSystemMetrics(SM_CXSCREEN); + int height = ::GetSystemMetrics(SM_CYSCREEN); + + if (rm.usFlags & MOUSE_VIRTUAL_DESKTOP) { + left = ::GetSystemMetrics(SM_XVIRTUALSCREEN); + top = ::GetSystemMetrics(SM_YVIRTUALSCREEN); + width = ::GetSystemMetrics(SM_CXVIRTUALSCREEN); + height = ::GetSystemMetrics(SM_CYVIRTUALSCREEN); + } + + if (width <= 0 || height <= 0) { + return false; + } + + // RAWMOUSE absolute packets are normalized to 0..65535 over the selected + // desktop rectangle. Treating these values as relative deltas is a classic + // cause of huge mouse jumps/skips. + outPoint->x = left + ::MulDiv(rm.lLastX, width - 1, 65535); + outPoint->y = top + ::MulDiv(rm.lLastY, height - 1, 65535); + return true; +} + +static DWORD IceDI_RawKeyboardToDIK(const RAWKEYBOARD& kb) { + USHORT makeCode = kb.MakeCode; + USHORT flags = kb.Flags; + USHORT vkey = kb.VKey; + + if (makeCode == 0) { + if (vkey == VK_SNAPSHOT) { + return DIK_SYSRQ; + } + if (vkey == VK_PAUSE) { + return DIK_PAUSE; + } + return 0; + } + + if (vkey == VK_PAUSE) { + return DIK_PAUSE; + } + + if (vkey == VK_SNAPSHOT) { + return DIK_SYSRQ; + } + + DWORD dik = makeCode & 0x7F; + + if (flags & RI_KEY_E0) { + dik |= 0x80; + } + + switch (vkey) { + case VK_RCONTROL: + return DIK_RCONTROL; + + case VK_RMENU: + return DIK_RMENU; + + case VK_LWIN: + return DIK_LWIN; + + case VK_RWIN: + return DIK_RWIN; + + case VK_APPS: + return DIK_APPS; + + default: + break; + } + + return dik; +} + +enum iceDIShimDeviceType { + ICE_DI_DEVICE_KEYBOARD, + ICE_DI_DEVICE_MOUSE +}; + +static bool IceDI_RegisterRawInputForCurrentTargets(HWND fallbackHwnd) { + HWND keyboardTarget = NULL; + HWND mouseTarget = NULL; + + { + std::lock_guard lock(g_windowMutex); + + if (IceDI_IsUsableWindow(g_keyboardTargetHwnd)) { + keyboardTarget = g_keyboardTargetHwnd; + } + + if (IceDI_IsUsableWindow(g_mouseTargetHwnd)) { + mouseTarget = g_mouseTargetHwnd; + } + + if (!IceDI_IsUsableWindow(keyboardTarget) && IceDI_IsUsableWindow(fallbackHwnd)) { + keyboardTarget = fallbackHwnd; + g_keyboardTargetHwnd = fallbackHwnd; + } + + if (!IceDI_IsUsableWindow(mouseTarget) && IceDI_IsUsableWindow(fallbackHwnd)) { + mouseTarget = fallbackHwnd; + g_mouseTargetHwnd = fallbackHwnd; + } + + if (IceDI_IsUsableWindow(fallbackHwnd)) { + g_rawInputHwnd = fallbackHwnd; + } + else { + g_rawInputHwnd = IceDI_GetFallbackRawInputWindow_NoLock(); + } + } + + if (!IceDI_IsUsableWindow(keyboardTarget) && !IceDI_IsUsableWindow(mouseTarget)) { + return false; + } + + RAWINPUTDEVICE rid[2]; + UINT count = 0; + memset(rid, 0, sizeof(rid)); + + if (IceDI_IsUsableWindow(keyboardTarget)) { + rid[count].usUsagePage = 0x01; + rid[count].usUsage = 0x06; + rid[count].dwFlags = 0; + rid[count].hwndTarget = keyboardTarget; + count++; + } + + if (IceDI_IsUsableWindow(mouseTarget)) { + rid[count].usUsagePage = 0x01; + rid[count].usUsage = 0x02; + rid[count].dwFlags = 0; + rid[count].hwndTarget = mouseTarget; + count++; + } + + if (count == 0) { + return false; + } + + if (!::RegisterRawInputDevices(rid, count, sizeof(RAWINPUTDEVICE))) { + OutputDebugStringA("IceDirectInputShim: RegisterRawInputDevices failed\n"); + return false; + } + + return true; +} + +static void IceDI_SetDeviceTargetWindow(iceDIShimDeviceType type, HWND hwnd) { + if (!IceDI_IsUsableWindow(hwnd)) { + return; + } + + { + std::lock_guard lock(g_windowMutex); + + if (type == ICE_DI_DEVICE_KEYBOARD) { + g_keyboardTargetHwnd = hwnd; + } + else { + g_mouseTargetHwnd = hwnd; + } + + g_rawInputHwnd = hwnd; + } + + IceDI_RegisterRawInputForCurrentTargets(hwnd); +} + +// ------------------------------------------------------------ +// IceDirectInputDevice8 +// ------------------------------------------------------------ + +class IceDirectInputDevice8 : public IDirectInputDevice8A { +public: + IceDirectInputDevice8(iceDIShimDeviceType type) : + refCount(1), + deviceType(type), + acquired(false), + bufferSize(ICE_DI_DEFAULT_BUFFER_SIZE), + coopFlags(0), + hwnd(NULL), + haveLastAbsoluteMouse(false) { + + memset(keyboardState, 0, sizeof(keyboardState)); + memset(&mouseState, 0, sizeof(mouseState)); + lastAbsoluteMouse.x = 0; + lastAbsoluteMouse.y = 0; + } + + virtual ~IceDirectInputDevice8() { + std::lock_guard lock(g_deviceMutex); + + if (g_keyboardDevice == this) { + g_keyboardDevice = NULL; + } + + if (g_mouseDevice == this) { + g_mouseDevice = NULL; + g_iceDI_MouseAcquired.store(false); + } + } + + // -------------------------------------------------------- + // IUnknown + // -------------------------------------------------------- + + STDMETHOD(QueryInterface)(REFIID riid, LPVOID* ppvObj) override { + if (ppvObj == NULL) { + return E_POINTER; + } + + *ppvObj = NULL; + + if (riid == IID_IUnknown || riid == IID_IDirectInputDevice8A) { + *ppvObj = static_cast(this); + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + + STDMETHOD_(ULONG, AddRef)() override { + return ++refCount; + } + + STDMETHOD_(ULONG, Release)() override { + ULONG r = --refCount; + + if (r == 0) { + delete this; + } + + return r; + } + + // -------------------------------------------------------- + // IDirectInputDevice8A + // -------------------------------------------------------- + + STDMETHOD(GetCapabilities)(LPDIDEVCAPS lpDIDevCaps) override { + if (lpDIDevCaps == NULL) { + return E_POINTER; + } + + if (lpDIDevCaps->dwSize < sizeof(DIDEVCAPS)) { + return DIERR_INVALIDPARAM; + } + + DWORD oldSize = lpDIDevCaps->dwSize; + memset(lpDIDevCaps, 0, oldSize); + lpDIDevCaps->dwSize = oldSize; + + if (deviceType == ICE_DI_DEVICE_KEYBOARD) { + lpDIDevCaps->dwDevType = DI8DEVTYPE_KEYBOARD; + lpDIDevCaps->dwButtons = 256; + } + else { + lpDIDevCaps->dwDevType = DI8DEVTYPE_MOUSE; + lpDIDevCaps->dwAxes = 3; + lpDIDevCaps->dwButtons = 8; + } + + return DI_OK; + } + + STDMETHOD(EnumObjects)(LPDIENUMDEVICEOBJECTSCALLBACKA lpCallback, LPVOID pvRef, DWORD dwFlags) override { + if (lpCallback == NULL) { + return DIERR_INVALIDPARAM; + } + + if (deviceType == ICE_DI_DEVICE_KEYBOARD) { + for (DWORD i = 0; i < 256; i++) { + DIDEVICEOBJECTINSTANCEA inst; + memset(&inst, 0, sizeof(inst)); + + inst.dwSize = sizeof(inst); + inst.dwType = DIDFT_BUTTON | DIDFT_MAKEINSTANCE(i); + inst.dwOfs = i; + sprintf_s(inst.tszName, "Key %u", i); + + if (lpCallback(&inst, pvRef) == DIENUM_STOP) { + break; + } + } + } + else { + static const DWORD mouseObjects[] = { + DIMOFS_X, + DIMOFS_Y, + DIMOFS_Z, + DIMOFS_BUTTON0, + DIMOFS_BUTTON1, + DIMOFS_BUTTON2, + DIMOFS_BUTTON3, + DIMOFS_BUTTON4, + DIMOFS_BUTTON5, + DIMOFS_BUTTON6, + DIMOFS_BUTTON7 + }; + + for (int i = 0; i < (int)(sizeof(mouseObjects) / sizeof(mouseObjects[0])); i++) { + DIDEVICEOBJECTINSTANCEA inst; + memset(&inst, 0, sizeof(inst)); + + inst.dwSize = sizeof(inst); + inst.dwOfs = mouseObjects[i]; + + if (mouseObjects[i] == DIMOFS_X || mouseObjects[i] == DIMOFS_Y || mouseObjects[i] == DIMOFS_Z) { + inst.dwType = DIDFT_AXIS | DIDFT_MAKEINSTANCE(i); + } + else { + inst.dwType = DIDFT_BUTTON | DIDFT_MAKEINSTANCE(i - 3); + } + + sprintf_s(inst.tszName, "Mouse Object %d", i); + + if (lpCallback(&inst, pvRef) == DIENUM_STOP) { + break; + } + } + } + + return DI_OK; + } + + STDMETHOD(GetProperty)(REFGUID rguidProp, LPDIPROPHEADER pdiph) override { + if (pdiph == NULL) { + return E_POINTER; + } + + if (IceDI_IsDInputProp(rguidProp, 1)) { // DIPROP_BUFFERSIZE + if (pdiph->dwSize < sizeof(DIPROPDWORD)) { + return DIERR_INVALIDPARAM; + } + + DIPROPDWORD* prop = reinterpret_cast(pdiph); + prop->dwData = bufferSize; + return DI_OK; + } + + return DIERR_UNSUPPORTED; + } + + STDMETHOD(SetProperty)(REFGUID rguidProp, LPCDIPROPHEADER pdiph) override { + if (pdiph == NULL) { + return E_POINTER; + } + + if (IceDI_IsDInputProp(rguidProp, 1)) { // DIPROP_BUFFERSIZE + if (pdiph->dwSize < sizeof(DIPROPDWORD)) { + return DIERR_INVALIDPARAM; + } + + const DIPROPDWORD* prop = reinterpret_cast(pdiph); + + std::lock_guard lock(mutex); + bufferSize = prop->dwData > 0 ? prop->dwData : ICE_DI_DEFAULT_BUFFER_SIZE; + TrimQueue_NoLock(); + + return DI_OK; + } + + // Ignore unsupported DirectInput properties instead of failing old code. + return DI_OK; + } + + STDMETHOD(Acquire)() override { + acquired = true; + + if (hwnd != NULL && ::IsWindow(hwnd)) { + IceDI_SetDeviceTargetWindow(deviceType, hwnd); + } + else { + HWND fallback = NULL; + { + std::lock_guard lock(g_windowMutex); + fallback = IceDI_GetFallbackRawInputWindow_NoLock(); + } + + if (fallback != NULL && ::IsWindow(fallback)) { + IceDI_RegisterRawInputForCurrentTargets(fallback); + } + } + + if (deviceType == ICE_DI_DEVICE_MOUSE) { + g_iceDI_MouseAcquired.store(true); + ResetMouseMotion(); + IceDI_ForceMouseCenterInternal(true); + } + + return DI_OK; + } + + STDMETHOD(Unacquire)() override { + acquired = false; + + if (deviceType == ICE_DI_DEVICE_MOUSE) { + g_iceDI_MouseAcquired.store(false); + ResetMouseMotion(); + IceDI_ClearMouseClipIfOwned(); + } + + return DI_OK; + } + + STDMETHOD(GetDeviceState)(DWORD cbData, LPVOID lpvData) override { + if (lpvData == NULL) { + return E_POINTER; + } + + if (!acquired) { + return DIERR_NOTACQUIRED; + } + + if (deviceType == ICE_DI_DEVICE_MOUSE) { + IceDI_ForceMouseCenterInternal(false); + } + + std::lock_guard lock(mutex); + + if (deviceType == ICE_DI_DEVICE_KEYBOARD) { + if (cbData < 256) { + return DIERR_INVALIDPARAM; + } + + memcpy(lpvData, keyboardState, 256); + return DI_OK; + } + + if (cbData >= sizeof(DIMOUSESTATE2)) { + memcpy(lpvData, &mouseState, sizeof(DIMOUSESTATE2)); + + // Relative mouse state is consumed per poll. + mouseState.lX = 0; + mouseState.lY = 0; + mouseState.lZ = 0; + + return DI_OK; + } + + if (cbData >= sizeof(DIMOUSESTATE)) { + DIMOUSESTATE out; + memset(&out, 0, sizeof(out)); + + out.lX = mouseState.lX; + out.lY = mouseState.lY; + out.lZ = mouseState.lZ; + memcpy(out.rgbButtons, mouseState.rgbButtons, 4); + + memcpy(lpvData, &out, sizeof(out)); + + mouseState.lX = 0; + mouseState.lY = 0; + mouseState.lZ = 0; + + return DI_OK; + } + + return DIERR_INVALIDPARAM; + } + + STDMETHOD(GetDeviceData)( + DWORD cbObjectData, + LPDIDEVICEOBJECTDATA rgdod, + LPDWORD pdwInOut, + DWORD dwFlags + ) override { + if (pdwInOut == NULL) { + return E_POINTER; + } + + if (cbObjectData < sizeof(DIDEVICEOBJECTDATA)) { + return DIERR_INVALIDPARAM; + } + + if (!acquired) { + return DIERR_NOTACQUIRED; + } + + if (deviceType == ICE_DI_DEVICE_MOUSE) { + IceDI_ForceMouseCenterInternal(false); + } + + std::lock_guard lock(mutex); + + DWORD requested = *pdwInOut; + + // Doom/idTech uses rgdod == NULL to clear buffered garbage. + if (rgdod == NULL) { + DWORD count = 0; + + if (requested == 0) { + count = (DWORD)queue.size(); + queue.clear(); + } + else { + count = std::min(requested, (DWORD)queue.size()); + + for (DWORD i = 0; i < count; i++) { + queue.pop_front(); + } + } + + *pdwInOut = count; + return DI_OK; + } + + DWORD count = std::min(requested, (DWORD)queue.size()); + + for (DWORD i = 0; i < count; i++) { + rgdod[i] = queue.front(); + queue.pop_front(); + } + + *pdwInOut = count; + return DI_OK; + } + + STDMETHOD(SetDataFormat)(LPCDIDATAFORMAT lpdf) override { + if (lpdf == NULL) { + return DIERR_INVALIDPARAM; + } + + // Existing Doom path passes c_dfDIKeyboard / c_dfDIMouse2. + // The shim always emits standard DirectInput offsets. + return DI_OK; + } + + STDMETHOD(SetEventNotification)(HANDLE hEvent) override { + return DI_OK; + } + + STDMETHOD(SetCooperativeLevel)(HWND hwndOwner, DWORD dwFlags) override { + hwnd = hwndOwner; + coopFlags = dwFlags; + + if (hwndOwner != NULL && ::IsWindow(hwndOwner)) { + IceDI_SetDeviceTargetWindow(deviceType, hwndOwner); + + if (deviceType == ICE_DI_DEVICE_MOUSE) { + IceDI_ForceMouseCenterInternal(true); + } + } + + return DI_OK; + } + + STDMETHOD(GetObjectInfo)(LPDIDEVICEOBJECTINSTANCEA pdidoi, DWORD dwObj, DWORD dwHow) override { + if (pdidoi == NULL) { + return E_POINTER; + } + + DWORD oldSize = pdidoi->dwSize; + memset(pdidoi, 0, oldSize); + pdidoi->dwSize = oldSize; + pdidoi->dwOfs = dwObj; + + sprintf_s(pdidoi->tszName, "Ice RawInput Object %u", dwObj); + + return DI_OK; + } + + STDMETHOD(GetDeviceInfo)(LPDIDEVICEINSTANCEA pdidi) override { + if (pdidi == NULL) { + return E_POINTER; + } + + DWORD oldSize = pdidi->dwSize; + memset(pdidi, 0, oldSize); + pdidi->dwSize = oldSize; + + if (deviceType == ICE_DI_DEVICE_KEYBOARD) { + pdidi->guidInstance = GUID_SysKeyboard; + pdidi->guidProduct = GUID_SysKeyboard; + pdidi->dwDevType = DI8DEVTYPE_KEYBOARD; + + strcpy_s(pdidi->tszInstanceName, "Ice RawInput Keyboard"); + strcpy_s(pdidi->tszProductName, "Ice RawInput Keyboard"); + } + else { + pdidi->guidInstance = GUID_SysMouse; + pdidi->guidProduct = GUID_SysMouse; + pdidi->dwDevType = DI8DEVTYPE_MOUSE; + + strcpy_s(pdidi->tszInstanceName, "Ice RawInput Mouse"); + strcpy_s(pdidi->tszProductName, "Ice RawInput Mouse"); + } + + return DI_OK; + } + + STDMETHOD(RunControlPanel)(HWND hwndOwner, DWORD dwFlags) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(Initialize)(HINSTANCE hinst, DWORD dwVersion, REFGUID rguid) override { + return DI_OK; + } + + STDMETHOD(CreateEffect)(REFGUID rguid, LPCDIEFFECT lpeff, LPDIRECTINPUTEFFECT* ppdeff, LPUNKNOWN punkOuter) override { + if (ppdeff != NULL) { + *ppdeff = NULL; + } + + return DIERR_UNSUPPORTED; + } + + STDMETHOD(EnumEffects)(LPDIENUMEFFECTSCALLBACKA lpCallback, LPVOID pvRef, DWORD dwEffType) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(GetEffectInfo)(LPDIEFFECTINFOA pdei, REFGUID rguid) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(GetForceFeedbackState)(LPDWORD pdwOut) override { + if (pdwOut != NULL) { + *pdwOut = 0; + } + + return DIERR_UNSUPPORTED; + } + + STDMETHOD(SendForceFeedbackCommand)(DWORD dwFlags) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(EnumCreatedEffectObjects)(LPDIENUMCREATEDEFFECTOBJECTSCALLBACK lpCallback, LPVOID pvRef, DWORD fl) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(Escape)(LPDIEFFESCAPE pesc) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(Poll)() override { + if (deviceType == ICE_DI_DEVICE_MOUSE && acquired) { + IceDI_ForceMouseCenterInternal(false); + } + + return DI_OK; + } + + STDMETHOD(SendDeviceData)(DWORD cbObjectData, LPCDIDEVICEOBJECTDATA rgdod, LPDWORD pdwInOut, DWORD fl) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(EnumEffectsInFile)(LPCSTR lpszFileName, LPDIENUMEFFECTSINFILECALLBACK pec, LPVOID pvRef, DWORD dwFlags) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(WriteEffectToFile)(LPCSTR lpszFileName, DWORD dwEntries, LPDIFILEEFFECT rgDiFileEft, DWORD dwFlags) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(BuildActionMap)(LPDIACTIONFORMATA lpdiaf, LPCSTR lpszUserName, DWORD dwFlags) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(SetActionMap)(LPDIACTIONFORMATA lpdiActionFormat, LPCSTR lptszUserName, DWORD dwFlags) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(GetImageInfo)(LPDIDEVICEIMAGEINFOHEADERA lpdiDevImageInfoHeader) override { + return DIERR_UNSUPPORTED; + } + + // -------------------------------------------------------- + // Raw Input entry points + // -------------------------------------------------------- + + void PushKeyboardRaw(const RAWKEYBOARD& kb) { + DWORD dik = IceDI_RawKeyboardToDIK(kb); + + if (dik == 0 || dik >= 256) { + return; + } + + const bool down = IceDI_IsPressedRawKeyboard(kb); + const BYTE newState = down ? 0x80 : 0x00; + + std::lock_guard lock(mutex); + + keyboardState[dik] = newState; + + DIDEVICEOBJECTDATA od; + memset(&od, 0, sizeof(od)); + + od.dwOfs = dik; + od.dwData = newState; + od.dwTimeStamp = IceDI_GetTimeStamp(); + od.dwSequence = 0; + od.uAppData = 0; + + PushEvent_NoLock(od); + } + + void PushMouseRaw(const RAWMOUSE& rm) { + std::lock_guard lock(mutex); + + const DWORD time = IceDI_GetTimeStamp(); + LONG dx = 0; + LONG dy = 0; + + if (rm.usFlags & MOUSE_MOVE_ABSOLUTE) { + POINT absolutePoint; + if (IceDI_RawAbsoluteMouseToScreen(rm, &absolutePoint)) { + // SetCursorPos can surface as an absolute packet on some mouse + // stacks / remote-desktop / wrapped-window setups. Reset the + // baseline instead of generating a giant negative/positive delta. + if (IceDI_IsRecentCenterWarpPoint(absolutePoint)) { + lastAbsoluteMouse = absolutePoint; + haveLastAbsoluteMouse = true; + } + else if (haveLastAbsoluteMouse) { + dx = absolutePoint.x - lastAbsoluteMouse.x; + dy = absolutePoint.y - lastAbsoluteMouse.y; + lastAbsoluteMouse = absolutePoint; + } + else { + lastAbsoluteMouse = absolutePoint; + haveLastAbsoluteMouse = true; + } + } + } + else { + haveLastAbsoluteMouse = false; + dx = rm.lLastX; + dy = rm.lLastY; + } + + // Raw Input relative deltas only. The absolute packet conversion above + // converts absolute devices into sane relative deltas; do not mix this + // with GetCursorPos/WM_MOUSEMOVE deltas. + if (dx != 0) { + mouseState.lX += dx; + PushMouseAxis_NoLock(DIMOFS_X, dx, time); + } + + if (dy != 0) { + mouseState.lY += dy; + PushMouseAxis_NoLock(DIMOFS_Y, dy, time); + } + + const USHORT flags = rm.usButtonFlags; + + if (flags & RI_MOUSE_WHEEL) { + SHORT wheel = (SHORT)rm.usButtonData; + mouseState.lZ += wheel; + PushMouseAxis_NoLock(DIMOFS_Z, wheel, time); + } + + if (flags & RI_MOUSE_BUTTON_1_DOWN) { + PushMouseButton_NoLock(0, true, time); + } + if (flags & RI_MOUSE_BUTTON_1_UP) { + PushMouseButton_NoLock(0, false, time); + } + + if (flags & RI_MOUSE_BUTTON_2_DOWN) { + PushMouseButton_NoLock(1, true, time); + } + if (flags & RI_MOUSE_BUTTON_2_UP) { + PushMouseButton_NoLock(1, false, time); + } + + if (flags & RI_MOUSE_BUTTON_3_DOWN) { + PushMouseButton_NoLock(2, true, time); + } + if (flags & RI_MOUSE_BUTTON_3_UP) { + PushMouseButton_NoLock(2, false, time); + } + + if (flags & RI_MOUSE_BUTTON_4_DOWN) { + PushMouseButton_NoLock(3, true, time); + } + if (flags & RI_MOUSE_BUTTON_4_UP) { + PushMouseButton_NoLock(3, false, time); + } + + if (flags & RI_MOUSE_BUTTON_5_DOWN) { + PushMouseButton_NoLock(4, true, time); + } + if (flags & RI_MOUSE_BUTTON_5_UP) { + PushMouseButton_NoLock(4, false, time); + } + } + + void ClearQueue() { + std::lock_guard lock(mutex); + queue.clear(); + } + + void ResetMouseMotion() { + if (deviceType != ICE_DI_DEVICE_MOUSE) { + return; + } + + std::lock_guard lock(mutex); + queue.clear(); + memset(&mouseState, 0, sizeof(mouseState)); + haveLastAbsoluteMouse = false; + lastAbsoluteMouse.x = 0; + lastAbsoluteMouse.y = 0; + } + +private: + void PushMouseAxis_NoLock(DWORD ofs, LONG value, DWORD time) { + DIDEVICEOBJECTDATA od; + memset(&od, 0, sizeof(od)); + + od.dwOfs = ofs; + od.dwData = (DWORD)value; + od.dwTimeStamp = time; + od.dwSequence = 0; + od.uAppData = 0; + + PushEvent_NoLock(od); + } + + void PushMouseButton_NoLock(int button, bool down, DWORD time) { + if (button < 0 || button >= 8) { + return; + } + + mouseState.rgbButtons[button] = down ? 0x80 : 0x00; + + DIDEVICEOBJECTDATA od; + memset(&od, 0, sizeof(od)); + + od.dwOfs = DIMOFS_BUTTON0 + button; + od.dwData = down ? 0x80 : 0x00; + od.dwTimeStamp = time; + od.dwSequence = 0; + od.uAppData = 0; + + PushEvent_NoLock(od); + } + + void PushEvent_NoLock(const DIDEVICEOBJECTDATA& od) { + queue.push_back(od); + TrimQueue_NoLock(); + } + + void TrimQueue_NoLock() { + while (queue.size() > bufferSize) { + queue.pop_front(); + } + } + +private: + std::atomic refCount; + iceDIShimDeviceType deviceType; + bool acquired; + DWORD bufferSize; + DWORD coopFlags; + HWND hwnd; + + std::mutex mutex; + std::deque queue; + + BYTE keyboardState[256]; + DIMOUSESTATE2 mouseState; + + bool haveLastAbsoluteMouse; + POINT lastAbsoluteMouse; +}; + +// ------------------------------------------------------------ +// IceDirectInput8 +// ------------------------------------------------------------ + +class IceDirectInput8 : public IDirectInput8A { +public: + IceDirectInput8() : refCount(1) { + } + + virtual ~IceDirectInput8() { + } + + STDMETHOD(QueryInterface)(REFIID riid, LPVOID* ppvObj) override { + if (ppvObj == NULL) { + return E_POINTER; + } + + *ppvObj = NULL; + + if (riid == IID_IUnknown || riid == IID_IDirectInput8A) { + *ppvObj = static_cast(this); + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + + STDMETHOD_(ULONG, AddRef)() override { + return ++refCount; + } + + STDMETHOD_(ULONG, Release)() override { + ULONG r = --refCount; + + if (r == 0) { + delete this; + } + + return r; + } + + STDMETHOD(CreateDevice)(REFGUID rguid, LPDIRECTINPUTDEVICE8A* lplpDirectInputDevice, LPUNKNOWN pUnkOuter) override { + if (lplpDirectInputDevice == NULL) { + return E_POINTER; + } + + *lplpDirectInputDevice = NULL; + + if (pUnkOuter != NULL) { + return CLASS_E_NOAGGREGATION; + } + + IceDirectInputDevice8* dev = NULL; + IceDirectInputDevice8* oldDev = NULL; + + if (rguid == GUID_SysKeyboard) { + dev = new IceDirectInputDevice8(ICE_DI_DEVICE_KEYBOARD); + + { + std::lock_guard lock(g_deviceMutex); + oldDev = g_keyboardDevice; + g_keyboardDevice = dev; + g_keyboardDevice->AddRef(); + } + } + else if (rguid == GUID_SysMouse) { + dev = new IceDirectInputDevice8(ICE_DI_DEVICE_MOUSE); + + { + std::lock_guard lock(g_deviceMutex); + oldDev = g_mouseDevice; + g_mouseDevice = dev; + g_mouseDevice->AddRef(); + } + } + else { + return DIERR_DEVICENOTREG; + } + + // Do not Release while holding g_deviceMutex. The device destructor also + // takes g_deviceMutex to clear the globals. + if (oldDev != NULL) { + oldDev->Release(); + } + + *lplpDirectInputDevice = dev; + return DI_OK; + } + + STDMETHOD(EnumDevices)(DWORD dwDevType, LPDIENUMDEVICESCALLBACKA lpCallback, LPVOID pvRef, DWORD dwFlags) override { + if (lpCallback == NULL) { + return DIERR_INVALIDPARAM; + } + + if (dwDevType == 0 || dwDevType == DI8DEVCLASS_KEYBOARD || dwDevType == DI8DEVTYPE_KEYBOARD) { + DIDEVICEINSTANCEA di; + memset(&di, 0, sizeof(di)); + + di.dwSize = sizeof(di); + di.guidInstance = GUID_SysKeyboard; + di.guidProduct = GUID_SysKeyboard; + di.dwDevType = DI8DEVTYPE_KEYBOARD; + + strcpy_s(di.tszInstanceName, "Ice RawInput Keyboard"); + strcpy_s(di.tszProductName, "Ice RawInput Keyboard"); + + if (lpCallback(&di, pvRef) == DIENUM_STOP) { + return DI_OK; + } + } + + if (dwDevType == 0 || dwDevType == DI8DEVCLASS_POINTER || dwDevType == DI8DEVTYPE_MOUSE) { + DIDEVICEINSTANCEA di; + memset(&di, 0, sizeof(di)); + + di.dwSize = sizeof(di); + di.guidInstance = GUID_SysMouse; + di.guidProduct = GUID_SysMouse; + di.dwDevType = DI8DEVTYPE_MOUSE; + + strcpy_s(di.tszInstanceName, "Ice RawInput Mouse"); + strcpy_s(di.tszProductName, "Ice RawInput Mouse"); + + lpCallback(&di, pvRef); + } + + return DI_OK; + } + + STDMETHOD(GetDeviceStatus)(REFGUID rguidInstance) override { + if (rguidInstance == GUID_SysKeyboard || rguidInstance == GUID_SysMouse) { + return DI_OK; + } + + return DIERR_NOTFOUND; + } + + STDMETHOD(RunControlPanel)(HWND hwndOwner, DWORD dwFlags) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(Initialize)(HINSTANCE hinst, DWORD dwVersion) override { + return DI_OK; + } + + STDMETHOD(FindDevice)(REFGUID rguidClass, LPCSTR ptszName, LPGUID pguidInstance) override { + if (pguidInstance == NULL) { + return E_POINTER; + } + + if (ptszName != NULL) { + if (_stricmp(ptszName, "keyboard") == 0) { + *pguidInstance = GUID_SysKeyboard; + return DI_OK; + } + + if (_stricmp(ptszName, "mouse") == 0) { + *pguidInstance = GUID_SysMouse; + return DI_OK; + } + } + + return DIERR_NOTFOUND; + } + + STDMETHOD(EnumDevicesBySemantics)( + LPCSTR ptszUserName, + LPDIACTIONFORMATA lpdiActionFormat, + LPDIENUMDEVICESBYSEMANTICSCBA lpCallback, + LPVOID pvRef, + DWORD dwFlags + ) override { + return DIERR_UNSUPPORTED; + } + + STDMETHOD(ConfigureDevices)( + LPDICONFIGUREDEVICESCALLBACK lpdiCallback, + LPDICONFIGUREDEVICESPARAMSA lpdiCDParams, + DWORD dwFlags, + LPVOID pvRefData + ) override { + return DIERR_UNSUPPORTED; + } + +private: + std::atomic refCount; +}; + +// ------------------------------------------------------------ +// Public API +// ------------------------------------------------------------ + +HRESULT WINAPI IceDirectInput8Create( + HINSTANCE hinst, + DWORD dwVersion, + REFIID riidltf, + LPVOID* ppvOut, + LPUNKNOWN punkOuter +) { + if (ppvOut == NULL) { + return E_POINTER; + } + + *ppvOut = NULL; + + if (punkOuter != NULL) { + return CLASS_E_NOAGGREGATION; + } + + IceDirectInput8* di = new IceDirectInput8(); + + HRESULT hr = di->QueryInterface(riidltf, ppvOut); + di->Release(); + + return hr; +} + +bool IceDirectInputShim_RegisterRawInput(HWND hwnd) { + if (hwnd == NULL || !::IsWindow(hwnd)) { + return false; + } + + { + std::lock_guard lock(g_windowMutex); + g_rawInputHwnd = hwnd; + g_keyboardTargetHwnd = hwnd; + g_mouseTargetHwnd = hwnd; + } + + bool ok = IceDI_RegisterRawInputForCurrentTargets(hwnd); + + if (ok) { + IceDI_ForceMouseCenterInternal(false); + } + + return ok; +} + +void IceDirectInputShim_SetMouseTargetWindow(HWND hwnd) { + if (hwnd == NULL || !::IsWindow(hwnd)) { + return; + } + + { + std::lock_guard lock(g_windowMutex); + g_mouseTargetHwnd = hwnd; + g_rawInputHwnd = hwnd; + } + + IceDI_RegisterRawInputForCurrentTargets(hwnd); + IceDI_ForceMouseCenterInternal(true); +} + +void IceDirectInputShim_SetMouseCenterLock(bool enabled) { + g_iceDI_CenterMouseWhenActive.store(enabled); + + if (!enabled) { + IceDI_ClearMouseClipIfOwned(); + } + else { + IceDI_ForceMouseCenterInternal(true); + } +} + +bool IceDirectInputShim_ForceMouseCenter() { + return IceDI_ForceMouseCenterInternal(true); +} + +bool IceDirectInputShim_HandleRawInput(LPARAM lParam) { + UINT size = 0; + + if (::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &size, sizeof(RAWINPUTHEADER)) != 0) { + return false; + } + + if (size == 0) { + return false; + } + + std::vector buffer; + buffer.resize(size); + + UINT result = ::GetRawInputData( + (HRAWINPUT)lParam, + RID_INPUT, + buffer.data(), + &size, + sizeof(RAWINPUTHEADER) + ); + + if (result == (UINT)-1 || result == 0) { + return false; + } + + RAWINPUT* raw = reinterpret_cast(buffer.data()); + + // RAWINPUTHEADER intentionally has no target HWND field. The target window + // is chosen when registering RAWINPUTDEVICE::hwndTarget, in SetCooperativeLevel, + // or through IceDirectInputShim_SetMouseTargetWindow(). Do not try to read + // the target HWND from the raw-input header; that member does not exist. + + if (raw->header.dwType == RIM_TYPEKEYBOARD) { + IceDirectInputDevice8* kbd = NULL; + + { + std::lock_guard lock(g_deviceMutex); + kbd = g_keyboardDevice; + + if (kbd != NULL) { + kbd->AddRef(); + } + } + + if (kbd != NULL) { + kbd->PushKeyboardRaw(raw->data.keyboard); + kbd->Release(); + return true; + } + } + + if (raw->header.dwType == RIM_TYPEMOUSE) { + if (!g_iceDI_GameMouseActive.load()) { + IceDI_ClearMouseClipIfOwned(); + return false; + } + + IceDirectInputDevice8* mouse = NULL; + + { + std::lock_guard lock(g_deviceMutex); + mouse = g_mouseDevice; + + if (mouse != NULL) { + mouse->AddRef(); + } + } + + if (mouse != NULL) { + mouse->PushMouseRaw(raw->data.mouse); + mouse->Release(); + IceDI_ForceMouseCenterInternal(false); + return true; + } + } + + return false; +} + +void IceDirectInputShim_SetGameMouseActive(bool active) { + g_iceDI_GameMouseActive.store(active); + + IceDirectInputDevice8* mouse = NULL; + + { + std::lock_guard lock(g_deviceMutex); + mouse = g_mouseDevice; + + if (mouse != NULL) { + mouse->AddRef(); + } + } + + if (mouse != NULL) { + mouse->ResetMouseMotion(); + mouse->Release(); + } + + if (active) { + IceDI_ForceMouseCenterInternal(true); + } + else { + IceDI_ClearMouseClipIfOwned(); + } +} + +void IceDirectInputShim_Shutdown() { + IceDirectInputDevice8* keyboard = NULL; + IceDirectInputDevice8* mouse = NULL; + + { + std::lock_guard lock(g_deviceMutex); + keyboard = g_keyboardDevice; + mouse = g_mouseDevice; + g_keyboardDevice = NULL; + g_mouseDevice = NULL; + } + + if (keyboard != NULL) { + keyboard->Release(); + } + + if (mouse != NULL) { + mouse->Release(); + } + + { + std::lock_guard lock(g_windowMutex); + g_rawInputHwnd = NULL; + g_keyboardTargetHwnd = NULL; + g_mouseTargetHwnd = NULL; + g_lastCenterWarpTick = 0; + } + + g_iceDI_MouseAcquired.store(false); + g_iceDI_GameMouseActive.store(false); + IceDI_ClearMouseClipIfOwned(); +} diff --git a/neo/engine/opengl/DirectInputShim.h b/neo/engine/opengl/DirectInputShim.h new file mode 100644 index 00000000..0690c1d0 --- /dev/null +++ b/neo/engine/opengl/DirectInputShim.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#ifndef DIRECTINPUT_VERSION +#define DIRECTINPUT_VERSION 0x0800 +#endif + +// Call this instead of DirectInput8Create. +HRESULT WINAPI IceDirectInput8Create( + HINSTANCE hinst, + DWORD dwVersion, + REFIID riidltf, + LPVOID* ppvOut, + LPUNKNOWN punkOuter +); + +// Call from your main window proc on WM_INPUT. +bool IceDirectInputShim_HandleRawInput(LPARAM lParam); + +// Optional: call when your hwnd changes/recreates. +bool IceDirectInputShim_RegisterRawInput(HWND hwnd); + +// Optional shutdown helper. +void IceDirectInputShim_Shutdown(); + +void IceDI_ForceOverrideSkipCenter(bool shouldSkip); \ No newline at end of file diff --git a/neo/engine/opengl/gl_d3d12shim.cpp b/neo/engine/opengl/gl_d3d12shim.cpp index a338ceb4..c08fd0cd 100644 --- a/neo/engine/opengl/gl_d3d12shim.cpp +++ b/neo/engine/opengl/gl_d3d12shim.cpp @@ -188,6 +188,12 @@ using Microsoft::WRL::ComPtr; #ifndef GL_QD3D12_TONEMAP_BRIGHTNESS #define GL_QD3D12_TONEMAP_BRIGHTNESS 0x600A #endif +#ifndef GL_QD3D12_TAA_ENABLED +#define GL_QD3D12_TAA_ENABLED 0x600B +#endif +#ifndef GL_QD3D12_TAA +#define GL_QD3D12_TAA GL_QD3D12_TAA_ENABLED +#endif #ifndef GL_QD3D12_TONE_MAP_BRIGHTNESS #define GL_QD3D12_TONE_MAP_BRIGHTNESS GL_QD3D12_TONEMAP_BRIGHTNESS #endif @@ -314,6 +320,12 @@ void QD3D12_EnableDLAA(int enabled); int QD3D12_IsDLAAEnabled(void); void APIENTRY glDLAAQD3D12(GLboolean enable); void APIENTRY glEnableDLAAQD3D12(GLboolean enable); +void QD3D12_EnableTAA(int enabled); +int QD3D12_IsTAAEnabled(void); +void APIENTRY glTAAQD3D12(GLboolean enable); +void APIENTRY glEnableTAAQD3D12(GLboolean enable); +void APIENTRY glTemporalAAQD3D12(GLboolean enable); +void APIENTRY glEnableTemporalAAQD3D12(GLboolean enable); void QD3D12_SetPathTracingQuality(uint32_t samplesPerPixel, uint32_t maxBounces); void QD3D12_SetPathTracingFallbackSamples(uint32_t samplesPerPixel); void QD3D12_SetCameraInfo( @@ -661,7 +673,7 @@ struct GLBufferObject const char* vendor = "Justin Marshall"; const char* renderer = "Quake D3D12 Wrapper"; const char* version = "1.1-quake-d3d12"; -const char* extensions = "GL_SGIS_multitexture GL_ARB_multitexture GL_EXT_texture_env_add GL_ARB_texture_env_combine GL_ARB_texture_compression GL_EXT_texture_compression_s3tc GL_ARB_vertex_program GL_ARB_fragment_program GL_EXT_texture_cube_map GL_EXT_depth_bounds_test GL_EXT_stencil_two_side GL_ATI_separate_stencil GL_QD3D12_normal_map GL_QD3D12_glow_map GL_QD3D12_specular_map GL_QD3D12_glass_material GL_QD3D12_volumetric_light GL_QD3D12_dlaa GL_QD3D12_tonemap_brightness"; +const char* extensions = "GL_SGIS_multitexture GL_ARB_multitexture GL_EXT_texture_env_add GL_ARB_texture_env_combine GL_ARB_texture_compression GL_EXT_texture_compression_s3tc GL_ARB_vertex_program GL_ARB_fragment_program GL_EXT_texture_cube_map GL_EXT_depth_bounds_test GL_EXT_stencil_two_side GL_ATI_separate_stencil GL_QD3D12_normal_map GL_QD3D12_glow_map GL_QD3D12_specular_map GL_QD3D12_glass_material GL_QD3D12_volumetric_light GL_QD3D12_dlaa GL_QD3D12_taa GL_QD3D12_tonemap_brightness"; enum TexEnvModeShader { @@ -1207,6 +1219,7 @@ struct GLState QD3D12UpscalerBackend upscalerBackend = QD3D12_UPSCALER_DLSS; QD3D12UpscalerQuality upscalerQuality = QD3D12_QUALITY_PERFORMANCE; + bool enableInternalTAA = true; bool enableRayAIDenoise = false; bool enableDLSSRayReconstruction = true; bool enableFSRRayRegeneration = false; @@ -5874,7 +5887,9 @@ static bool QD3D12_ShouldRunInternalTAA(const QD3D12Window& w) { // DLSS/DLSS-RR/FSR are already temporal. The internal TAA pass replaces MSAA // for the native/no-upscaler path and runs after lighting but before 2D. - return !w.isPbuffer && !QD3D12_ExternalTemporalUpscalerActive(w); + return g_gl.enableInternalTAA && + !w.isPbuffer && + !QD3D12_ExternalTemporalUpscalerActive(w); } static bool QD3D12_FinalBlitToBackBuffer( @@ -10348,6 +10363,9 @@ void APIENTRY glEnable(GLenum cap) case GL_FRAGMENT_PROGRAM_ARB: QD3D12ARB_SetEnabled(cap, true); break; + case GL_QD3D12_TAA: + QD3D12_EnableTAA(1); + break; case GL_TEXTURE_2D: #ifdef GL_TEXTURE_RECTANGLE_ARB case GL_TEXTURE_RECTANGLE_ARB: @@ -10385,6 +10403,9 @@ void APIENTRY glDisable(GLenum cap) case GL_FRAGMENT_PROGRAM_ARB: QD3D12ARB_SetEnabled(cap, false); break; + case GL_QD3D12_TAA: + QD3D12_EnableTAA(0); + break; case GL_TEXTURE_2D: #ifdef GL_TEXTURE_RECTANGLE_ARB case GL_TEXTURE_RECTANGLE_ARB: @@ -10954,6 +10975,10 @@ void APIENTRY glGetIntegerv(GLenum pname, GLint* params) *params = (GLint)QD3D12_IsDLAAEnabled(); break; + case GL_QD3D12_TAA_ENABLED: + *params = (GLint)QD3D12_IsTAAEnabled(); + break; + #ifdef GL_ACTIVE_STENCIL_FACE_EXT case GL_ACTIVE_STENCIL_FACE_EXT: *params = (GLint)g_gl.activeStencilFace; @@ -13561,6 +13586,12 @@ PROC WINAPI qd3d12_wglGetProcAddress(LPCSTR name) { { "QD3D12_IsDLAAEnabled", (PROC)QD3D12_IsDLAAEnabled }, { "glDLAAQD3D12", (PROC)glDLAAQD3D12 }, { "glEnableDLAAQD3D12", (PROC)glEnableDLAAQD3D12 }, + { "QD3D12_EnableTAA", (PROC)QD3D12_EnableTAA }, + { "QD3D12_IsTAAEnabled", (PROC)QD3D12_IsTAAEnabled }, + { "glTAAQD3D12", (PROC)glTAAQD3D12 }, + { "glEnableTAAQD3D12", (PROC)glEnableTAAQD3D12 }, + { "glTemporalAAQD3D12", (PROC)glTemporalAAQD3D12 }, + { "glEnableTemporalAAQD3D12", (PROC)glEnableTemporalAAQD3D12 }, { "QD3D12_SetPathTracingQuality", (PROC)QD3D12_SetPathTracingQuality }, { "QD3D12_SetPathTracingFallbackSamples", (PROC)QD3D12_SetPathTracingFallbackSamples }, { "glSetTopLevelAccelStructureVisible", (PROC)glSetTopLevelAccelStructureVisible }, @@ -14503,6 +14534,56 @@ void APIENTRY glEnableDLAAQD3D12(GLboolean enable) glDLAAQD3D12(enable); } +static void QD3D12_ResetInternalTAAHistoryForWindow(QD3D12Window* window) +{ + if (!window) + return; + + for (UINT i = 0; i < QD3D12_FrameCount; ++i) + window->taaHistoryValid[i] = false; +} + +void QD3D12_EnableTAA(int enabled) +{ + const bool newValue = enabled ? true : false; + if (g_gl.enableInternalTAA == newValue) + return; + + g_gl.enableInternalTAA = newValue; + g_gl.motionHistoryReset = true; + + // TAA history is only valid for the previous TAA mode. Drop it so re-enabling + // starts from the current frame instead of blending against stale history. + QD3D12_ResetInternalTAAHistoryForWindow(g_currentWindow); + for (auto& kv : g_windows) + QD3D12_ResetInternalTAAHistoryForWindow(&kv.second); +} + +int QD3D12_IsTAAEnabled(void) +{ + return g_gl.enableInternalTAA ? 1 : 0; +} + +void APIENTRY glTAAQD3D12(GLboolean enable) +{ + QD3D12_EnableTAA(enable != GL_FALSE ? 1 : 0); +} + +void APIENTRY glEnableTAAQD3D12(GLboolean enable) +{ + glTAAQD3D12(enable); +} + +void APIENTRY glTemporalAAQD3D12(GLboolean enable) +{ + glTAAQD3D12(enable); +} + +void APIENTRY glEnableTemporalAAQD3D12(GLboolean enable) +{ + glTAAQD3D12(enable); +} + void QD3D12_SetUpscalerSharpness(float sharpness) { g_gl.upscalerSharpness = ClampValue(sharpness, 0.0f, 1.0f); @@ -15679,6 +15760,7 @@ GLboolean APIENTRY glIsEnabled(GLenum cap) case GL_STENCIL_TEST_TWO_SIDE_EXT: return g_gl.stencilTwoSide ? GL_TRUE : GL_FALSE; #endif case GL_TEXTURE_2D: return g_gl.texture2D[g_gl.activeTextureUnit] ? GL_TRUE : GL_FALSE; + case GL_QD3D12_TAA: return QD3D12_IsTAAEnabled() ? GL_TRUE : GL_FALSE; case GL_LIGHTING: return g_glState.lightingEnabled; diff --git a/neo/engine/opengl/opengl.h b/neo/engine/opengl/opengl.h index 6a4f20ec..1268284f 100644 --- a/neo/engine/opengl/opengl.h +++ b/neo/engine/opengl/opengl.h @@ -2325,4 +2325,6 @@ void APIENTRY glShowAllTopLevelAccelStructures( glRaytracingSceneHandle_t scene); void QD3D12_SetUpscalerSharpness(float sharpness); -void QD3D12_SetToneMapBrightness(float brightness); \ No newline at end of file +void QD3D12_SetToneMapBrightness(float brightness); + +void QD3D12_EnableTAA(int enabled); \ No newline at end of file diff --git a/neo/engine/opengl/opengl.vcxproj b/neo/engine/opengl/opengl.vcxproj index d8c3f02f..0f4d2838 100644 --- a/neo/engine/opengl/opengl.vcxproj +++ b/neo/engine/opengl/opengl.vcxproj @@ -35,6 +35,7 @@ + Disabled @@ -178,6 +179,7 @@ + diff --git a/neo/engine/opengl/opengl.vcxproj.filters b/neo/engine/opengl/opengl.vcxproj.filters index 459f815a..d6e5bc58 100644 --- a/neo/engine/opengl/opengl.vcxproj.filters +++ b/neo/engine/opengl/opengl.vcxproj.filters @@ -42,6 +42,7 @@ tess + @@ -94,6 +95,7 @@ tess + diff --git a/neo/engine/sys/win32/win_input.cpp b/neo/engine/sys/win32/win_input.cpp index d8aa6997..4c73741d 100644 --- a/neo/engine/sys/win32/win_input.cpp +++ b/neo/engine/sys/win32/win_input.cpp @@ -512,7 +512,7 @@ void IN_InitDirectInput( void ) { // Register with the DirectInput subsystem and get a pointer // to a IDirectInput interface we can use. // Create the base DirectInput object - if ( FAILED( hr = DirectInput8Create( GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&win32.g_pdi, NULL ) ) ) { + if ( FAILED( hr = IceDirectInput8Create( GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&win32.g_pdi, NULL ) ) ) { common->Printf ("DirectInputCreate failed\n"); } } diff --git a/neo/engine/sys/win32/win_wndproc.cpp b/neo/engine/sys/win32/win_wndproc.cpp index 7e774f2f..5f8b563e 100644 --- a/neo/engine/sys/win32/win_wndproc.cpp +++ b/neo/engine/sys/win32/win_wndproc.cpp @@ -276,6 +276,10 @@ main window procedure LONG WINAPI MainWndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { int key; switch( uMsg ) { + case WM_INPUT: + IceDirectInputShim_HandleRawInput(lParam); + return 0; + case WM_WINDOWPOSCHANGED: if (glConfig.isInitialized) { RECT rect; @@ -334,6 +338,8 @@ LONG WINAPI MainWndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { if ( fActive == WA_INACTIVE ) { win32.movingWindow = false; + Sys_GrabMouseCursor(false); + idKeyInput::ClearStates(); } // start playing the game sound world diff --git a/neo/engine/tools/radiant/CamWnd.cpp b/neo/engine/tools/radiant/CamWnd.cpp index 87d01e05..72c03cd3 100644 --- a/neo/engine/tools/radiant/CamWnd.cpp +++ b/neo/engine/tools/radiant/CamWnd.cpp @@ -1829,7 +1829,7 @@ void CCamWnd::OnPaint() { CamWnd_EnsureMenuBar(this); CamWnd_LayoutMenuBar(this); - idGraphicsDeviceContextHelper context(dc.m_hDC, hglrc); + idGraphicsDeviceContextHelper context(dc.m_hDC, hglrc, true); g_pSplitList = NULL; if (g_bClipMode) { diff --git a/neo/engine/tools/radiant/MainFrm.cpp b/neo/engine/tools/radiant/MainFrm.cpp index b0f9bf4e..fb6696e4 100644 --- a/neo/engine/tools/radiant/MainFrm.cpp +++ b/neo/engine/tools/radiant/MainFrm.cpp @@ -278,7 +278,6 @@ SCommandInfo g_Commands[] = { { "Find_Entity", VK_F3, RAD_CONTROL, ID_MISC_FINDORREPLACEENTITY}, { "Find_NextEntity", VK_F3,RAD_SHIFT, ID_MISC_FINDNEXTENT}, - { "_ShowDOOM", VK_F2, 0, ID_SHOW_DOOM }, { "Rotate_MouseRotate", 'R', 0, ID_SELECT_MOUSEROTATE }, { "Rotate_ToggleFlatRotation", 'R', RAD_CONTROL, ID_VIEW_CAMERAUPDATE }, @@ -1360,12 +1359,71 @@ BOOL CMainFrame::CreateEmbeddedMainToolBar(UINT nID) { } m_wndToolBar.SetOwner(this); + RemoveDoomToolbarButton(); if (m_pXYDockWnd) { m_pXYDockWnd->SetEmbeddedToolBar(&m_wndToolBar); } return TRUE; } +/* + ======================================================================================================================= + ======================================================================================================================= + */ +static BOOL RemoveMenuCommandRecursive(HMENU hMenu, UINT nCommandID) { + if (!hMenu) { + return FALSE; + } + + BOOL removed = FALSE; + for (int i = ::GetMenuItemCount(hMenu) - 1; i >= 0; --i) { + MENUITEMINFO info; + memset(&info, 0, sizeof(info)); + info.cbSize = sizeof(info); + info.fMask = MIIM_ID | MIIM_SUBMENU; + + if (!::GetMenuItemInfo(hMenu, i, TRUE, &info)) { + continue; + } + + if (info.hSubMenu) { + removed |= RemoveMenuCommandRecursive(info.hSubMenu, nCommandID); + if (::GetMenuItemCount(info.hSubMenu) == 0) { + ::DeleteMenu(hMenu, i, MF_BYPOSITION); + removed = TRUE; + } + } + + if (!info.hSubMenu && info.wID == nCommandID) { + ::DeleteMenu(hMenu, i, MF_BYPOSITION); + removed = TRUE; + } + } + + return removed; +} + +void CMainFrame::RemoveDoomMenuItems(HMENU hMenu) { + if (hMenu) { + RemoveMenuCommandRecursive(hMenu, ID_SHOW_DOOM); + } +} + +void CMainFrame::RemoveDoomToolbarButton() { + if (!m_wndToolBar.GetSafeHwnd()) { + return; + } + + CToolBarCtrl &toolBarCtrl = m_wndToolBar.GetToolBarCtrl(); + for (int i = toolBarCtrl.GetButtonCount() - 1; i >= 0; --i) { + TBBUTTON button; + memset(&button, 0, sizeof(button)); + if (toolBarCtrl.GetButton(i, &button) && button.idCommand == ID_SHOW_DOOM) { + toolBarCtrl.DeleteButton(i); + } + } +} + /* ======================================================================================================================= ======================================================================================================================= @@ -1384,6 +1442,7 @@ void CMainFrame::MoveFrameMenuIntoXYWnd() { } m_hMovedMenu = hMenu; + RemoveDoomMenuItems(hMenu); m_pXYDockWnd->SetMenuHandle(hMenu); if (::GetMenu(GetSafeHwnd()) == hMenu) { @@ -1409,6 +1468,62 @@ void CMainFrame::RecalcMainLayout() { RecalcXYDockLayout(); } +void CMainFrame::AttachDoomWindowToGameTab() { + if (!m_pXYDockWnd || !m_pXYDockWnd->GetSafeHwnd()) { + return; + } + if (!win32.hWnd || !::IsWindow(win32.hWnd)) { + return; + } + + if (m_pXYDockWnd->GetDockedGameWindow() != win32.hWnd) { + m_pXYDockWnd->AttachGameWindow(win32.hWnd); + RecalcXYDockLayout(); + } else { + m_pXYDockWnd->AttachGameWindow(win32.hWnd); + } +} + +bool CMainFrame::IsDockedGameInputActive() const { + return (m_pXYDockWnd && + m_pXYDockWnd->GetSafeHwnd() && + m_pXYDockWnd->IsGameTabActive() && + m_pXYDockWnd->IsGameWindowDocked()); +} + +BOOL CMainFrame::ForwardDockedGameKey(UINT message, WPARAM wParam, LPARAM lParam) { + if (!IsDockedGameInputActive()) { + return FALSE; + } + + HWND hGameWnd = m_pXYDockWnd->GetDockedGameWindow(); + if (!hGameWnd || !::IsWindow(hGameWnd)) { + return FALSE; + } + + HWND hFocus = ::GetFocus(); + if (hFocus != hGameWnd && !::IsChild(hGameWnd, hFocus)) { + ::SetFocus(hGameWnd); + } + + ::PostMessage(hGameWnd, message, wParam, lParam); + return TRUE; +} + +void CMainFrame::ApplyActiveTabInputMode() { + AttachDoomWindowToGameTab(); + + if (IsDockedGameInputActive()) { + common->ActivateTool(false); + m_pXYDockWnd->FocusGameWindow(); + } else { + common->ActivateTool(true); + if (soundSystem) { + soundSystem->SetPlayingSoundWorld(g_qeglobals.sw); + } + } +} + /* ======================================================================================================================= ======================================================================================================================= @@ -1971,6 +2086,12 @@ void CMainFrame::CreateQEChildren() { ======================================================================================================================= */ BOOL CMainFrame::OnCommand(WPARAM wParam, LPARAM lParam) { + if (IsDockedGameInputActive()) { + const UINT commandID = LOWORD(wParam); + if (commandID != ID_SHOW_DOOM) { + return FALSE; + } + } return CFrameWnd::OnCommand(wParam, lParam); } @@ -2049,6 +2170,11 @@ bool MouseDown() { void CMainFrame::OnTimer(UINT_PTR nIDEvent) { static bool autoSavePending = false; + if (nIDEvent == QE_TIMER0) { + AttachDoomWindowToGameTab(); + ApplyActiveTabInputMode(); + } + if ( nIDEvent == QE_TIMER0 && !MouseDown() ) { QE_CountBrushesAndUpdateStatusBar(); } @@ -2118,6 +2244,10 @@ void CMainFrame::OnDestroy() { SaveWindowPlacement(GetSafeHwnd(), "radiant_MainWindowPlace"); + if (m_pXYDockWnd && m_pXYDockWnd->GetSafeHwnd()) { + m_pXYDockWnd->DetachGameWindow(TRUE); + } + if (m_pXYDockWnd && m_pXYDockWnd->GetSafeHwnd()) { SaveWindowPlacement(m_pXYDockWnd->GetSafeHwnd(), "radiant_xywindow"); } else if (m_pXYWnd && m_pXYWnd->GetSafeHwnd()) { @@ -2241,6 +2371,11 @@ void CMainFrame::OnClose() { ======================================================================================================================= */ void CMainFrame::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags) { + if (IsDockedGameInputActive()) { + ForwardDockedGameKey(WM_KEYUP, nChar, MAKELPARAM(nRepCnt, nFlags)); + return; + } + // run through our list to see if we have a handler for nChar for (int i = 0; i < g_nCommandCount; i++) { @@ -2312,6 +2447,11 @@ bool CamOK(unsigned int nKey) { ======================================================================================================================= */ void CMainFrame::OnSysKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { + if (IsDockedGameInputActive()) { + ForwardDockedGameKey(WM_SYSKEYDOWN, nChar, MAKELPARAM(nRepCnt, nFlags)); + return; + } + // OnKeyDown(nChar, nRepCnt, nFlags); if (nChar == VK_DOWN) { OnKeyDown(nChar, nRepCnt, nFlags); @@ -2326,6 +2466,11 @@ void CMainFrame::OnSysKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { */ void CMainFrame::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { + if (IsDockedGameInputActive()) { + ForwardDockedGameKey(WM_KEYDOWN, nChar, MAKELPARAM(nRepCnt, nFlags)); + return; + } + for (int i = 0; i < g_nCommandCount; i++) { if (g_Commands[i].m_nKey == nChar) { // find a match? // check modifiers @@ -2422,6 +2567,8 @@ BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext *pContext) { RecalcMainLayout(); CreateQEChildren(); + AttachDoomWindowToGameTab(); + ApplyActiveTabInputMode(); if (m_pXYWnd) { m_pXYWnd->SetActive(true); @@ -5903,6 +6050,24 @@ void CMainFrame::NudgeSelection(int nDirection, float fAmount) { ======================================================================================================================= */ BOOL CMainFrame::PreTranslateMessage(MSG *pMsg) { + if (pMsg && IsDockedGameInputActive()) { + const UINT message = pMsg->message; + const BOOL keyMessage = (message >= WM_KEYFIRST && message <= WM_KEYLAST); + + if (keyMessage) { + HWND hGameWnd = m_pXYDockWnd->GetDockedGameWindow(); + if (hGameWnd && ::IsWindow(hGameWnd)) { + if (pMsg->hwnd == hGameWnd || ::IsChild(hGameWnd, pMsg->hwnd)) { + return FALSE; + } + + ::SetFocus(hGameWnd); + ::PostMessage(hGameWnd, pMsg->message, pMsg->wParam, pMsg->lParam); + return TRUE; + } + } + } + if (pMsg && m_pXYDockWnd && m_pXYDockWnd->GetSafeHwnd()) { BOOL menuKey = FALSE; @@ -7004,13 +7169,14 @@ void CMainFrame::OnActivate(UINT nState, CWnd *pWndOther, BOOL bMinimized) { CFrameWnd::OnActivate(nState, pWndOther, bMinimized); if ( nState != WA_INACTIVE ) { - common->ActivateTool( true ); - if (::IsWindowVisible(win32.hWnd)) { - ::ShowWindow(win32.hWnd, SW_HIDE); - } + AttachDoomWindowToGameTab(); + ApplyActiveTabInputMode(); - // start playing the editor sound world - soundSystem->SetPlayingSoundWorld( g_qeglobals.sw ); + if (win32.hWnd && ::IsWindow(win32.hWnd) && ::IsWindowVisible(win32.hWnd)) { + if (!m_pXYDockWnd || !m_pXYDockWnd->IsGameWindowDocked()) { + ::ShowWindow(win32.hWnd, SW_HIDE); + } + } } else { //com_editorActive = false; @@ -7266,11 +7432,16 @@ void CMainFrame::OnPatchCombine() { void CMainFrame::OnShowDoom() { - int show = ::IsWindowVisible(win32.hWnd) ? SW_HIDE : SW_NORMAL; - if (show == SW_NORMAL) { + // Legacy command path. The menu/toolbar entry is removed, but if an old + // accelerator or external command still reaches this handler, switch to the + // always-attached Game tab instead of showing a popup. + AttachDoomWindowToGameTab(); + if (m_pXYDockWnd && m_pXYDockWnd->GetSafeHwnd()) { g_Inspectors->SetMode(W_TEXTURE); + m_pXYDockWnd->SelectGameTab(); + ApplyActiveTabInputMode(); + RecalcXYDockLayout(); } - ::ShowWindow(win32.hWnd, show); } void CMainFrame::OnViewRendermode() diff --git a/neo/engine/tools/radiant/MainFrm.h b/neo/engine/tools/radiant/MainFrm.h index 7c4deae1..76f27d1a 100644 --- a/neo/engine/tools/radiant/MainFrm.h +++ b/neo/engine/tools/radiant/MainFrm.h @@ -117,7 +117,7 @@ protected: // CMainFrameLayoutWnd // // Main workspace split: -// [ Camera/Inspector stack ] [ splitter ] [ XYDock: menu, toolbar, Z | XY ] +// [ Camera/Inspector stack ] [ splitter ] [ XYDock: XY tab | Game tab ] // // XYDock already owns the Z/XY splitter. This wrapper supplies the missing // MainFrm-level split and gives startup proportions like the reference image. @@ -231,6 +231,10 @@ public: CMenu* GetMenu() const; void RecalcMainLayout(); void RecalcXYDockLayout(); + void AttachDoomWindowToGameTab(); + bool IsDockedGameInputActive() const; + BOOL ForwardDockedGameKey(UINT message, WPARAM wParam, LPARAM lParam); + void ApplyActiveTabInputMode(); void SetActiveXY(CXYWnd* p) { @@ -275,6 +279,8 @@ protected: void CreateQEChildren(); BOOL CreateEmbeddedMainToolBar(UINT nID); void MoveFrameMenuIntoXYWnd(); + void RemoveDoomMenuItems(HMENU hMenu); + void RemoveDoomToolbarButton(); void LoadCommandMap(); void SaveCommandMap(); void ShowMenuItemKeyBindings(CMenu *pMenu); diff --git a/neo/engine/tools/radiant/QE3.H b/neo/engine/tools/radiant/QE3.H index d23f97a2..ca0d5a9a 100644 --- a/neo/engine/tools/radiant/QE3.H +++ b/neo/engine/tools/radiant/QE3.H @@ -84,21 +84,25 @@ extern std::mutex g_graphicsDeviceContextMutex; class idGraphicsDeviceContextHelper { public: - idGraphicsDeviceContextHelper(HDC inDC, HGLRC inRC) + idGraphicsDeviceContextHelper(HDC inDC, HGLRC inRC, bool tsaaEnabled = false) : dc(inDC) , rc(inRC) { + this->tsaaEnabled = tsaaEnabled; g_graphicsDeviceContextMutex.lock(); wglMakeCurrent(dc, rc); } ~idGraphicsDeviceContextHelper() { + QD3D12_EnableTAA(tsaaEnabled); SwapBuffers(dc); + QD3D12_EnableTAA(true); g_graphicsDeviceContextMutex.unlock(); } private: + bool tsaaEnabled = false; HDC dc = nullptr; HGLRC rc = nullptr; }; diff --git a/neo/engine/tools/radiant/XYWnd.cpp b/neo/engine/tools/radiant/XYWnd.cpp index deed3060..78ef6e90 100644 --- a/neo/engine/tools/radiant/XYWnd.cpp +++ b/neo/engine/tools/radiant/XYWnd.cpp @@ -42,6 +42,16 @@ If you have questions concerning this license or the applicable additional terms #define WM_IDLEUPDATECMDUI 0x0363 #endif +#define ID_XYDOCK_TABS 0x7A00 +#define ID_XYDOCK_XYPAGE 0x7A03 +#define ID_XYDOCK_GAMEPAGE 0x7A04 + +#define ID_GAME_HOST 0x7A30 +#define ID_GAME_FILL_FIT 0x7A40 +#define ID_GAME_FILL_100 0x7A41 +#define ID_GAME_FILL_75 0x7A42 +#define ID_GAME_FILL_50 0x7A43 + #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE @@ -586,6 +596,16 @@ int CXYMDIContainerWnd::GetFixedZWidth() const { return GetZWidth(); } +void CXYMDIContainerWnd::FocusXYWindow() { + if (m_pXYWnd && m_pXYWnd->GetSafeHwnd()) { + m_pXYWnd->SetFocus(); + } +} + +CWnd *CXYMDIContainerWnd::GetXYWindow() const { + return m_pXYWnd; +} + bool CXYMDIContainerWnd::HitTestSplitter(const CPoint &point) const { return !m_rcSplitter.IsRectEmpty() && m_rcSplitter.PtInRect(point); } @@ -756,6 +776,421 @@ BOOL CXYMDIContainerWnd::OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHAND return FALSE; } +//============================================================================= +// CGameDockWnd +//============================================================================= +IMPLEMENT_DYNAMIC(CGameDockWnd, CWnd) + +BEGIN_MESSAGE_MAP(CGameDockWnd, CWnd) + ON_WM_CREATE() + ON_WM_SIZE() + ON_WM_ERASEBKGND() + ON_WM_DESTROY() + ON_WM_SETFOCUS() + ON_BN_CLICKED(ID_GAME_FILL_FIT, OnFillFit) + ON_BN_CLICKED(ID_GAME_FILL_100, OnFill100) + ON_BN_CLICKED(ID_GAME_FILL_75, OnFill75) + ON_BN_CLICKED(ID_GAME_FILL_50, OnFill50) +END_MESSAGE_MAP() + +CGameDockWnd::CGameDockWnd() { + m_hGameWnd = NULL; + m_hOldParent = NULL; + m_dwOldStyle = 0; + m_dwOldExStyle = 0; + m_nFillPercent = 0; + m_bActive = FALSE; +} + +CGameDockWnd::~CGameDockWnd() { +} + +BOOL CGameDockWnd::Create(CWnd *pParent, UINT nID) { + CString className = AfxRegisterWndClass( + CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, + ::LoadCursor(NULL, IDC_ARROW), + (HBRUSH)::GetStockObject(BLACK_BRUSH), + NULL + ); + + return CWnd::CreateEx( + 0, + className, + "RadiantGameDockWnd", + WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, + CRect(0, 0, 0, 0), + pParent, + nID + ); +} + +void CGameDockWnd::SendGameActivate(BOOL bActive) { + if (!m_hGameWnd || !::IsWindow(m_hGameWnd)) { + return; + } + + // + // Doom 3's MainWndProc expects: + // + // LOWORD(wParam) = WA_ACTIVE / WA_INACTIVE + // HIWORD(wParam) = minimized flag + // + // We are not minimizing it when changing tabs, so HIWORD is FALSE. + // + const WPARAM activateParam = MAKEWPARAM(bActive ? WA_ACTIVE : WA_INACTIVE, FALSE); + + // + // lParam is the other window involved in activation. + // For our embedded case, use the host when activating/deactivating. + // + HWND hOther = m_wndHost.GetSafeHwnd(); + + ::SendMessage(m_hGameWnd, WM_ACTIVATE, activateParam, reinterpret_cast(hOther)); + + common->ActivateTool(!bActive); + + if (bActive) { + // + // WM_ACTIVATE updates win32.activeApp / mouse grabbing. + // SetFocus makes keyboard input go directly to the game HWND. + // + ::SetFocus(m_hGameWnd); + ::SendMessage(m_hGameWnd, WM_SETFOCUS, reinterpret_cast(hOther), 0); + } + else { + ::SendMessage(m_hGameWnd, WM_KILLFOCUS, reinterpret_cast(hOther), 0); + } +} + +int CGameDockWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { + if (CWnd::OnCreate(lpCreateStruct) == -1) { + return -1; + } + + CString hostClass = AfxRegisterWndClass( + CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, + ::LoadCursor(NULL, IDC_ARROW), + (HBRUSH)::GetStockObject(BLACK_BRUSH), + NULL + ); + + m_wndTitle.Create("GAME", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); + + m_btnFit.Create("FIT", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_GAME_FILL_FIT); + m_btn100.Create("100%", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_GAME_FILL_100); + m_btn75.Create("75%", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_GAME_FILL_75); + m_btn50.Create("50%", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_GAME_FILL_50); + + if (!m_wndHost.CreateEx( + WS_EX_CLIENTEDGE, + hostClass, + "RadiantGameRenderHost", + WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, + CRect(0, 0, 0, 0), + this, + ID_GAME_HOST + )) { + return -1; + } + + return 0; +} + +void CGameDockWnd::OnDestroy() { + DetachGameWindow(TRUE); + CWnd::OnDestroy(); +} + +void CGameDockWnd::OnSetFocus(CWnd *pOldWnd) { + CWnd::OnSetFocus(pOldWnd); + FocusGameWindow(); +} + +void CGameDockWnd::SetActive(BOOL bActive) { + const BOOL wasActive = m_bActive; + m_bActive = bActive; + + if (m_hGameWnd && ::IsWindow(m_hGameWnd)) { + ::ShowWindow(m_hGameWnd, bActive ? SW_SHOW : SW_HIDE); + + if (bActive) { + LayoutGameWindow(); + ::InvalidateRect(m_hGameWnd, NULL, FALSE); + } + + // + // Only synthesize WM_ACTIVATE on real active-state transitions. + // LayoutChildren() calls ShowActiveTab(), so without this guard the + // game would get repeated WA_ACTIVE messages every layout pass. + // + if (wasActive != m_bActive) { + SendGameActivate(m_bActive); + } + + if (bActive) { + FocusGameWindow(); + } + } + + // + // Keep the engine/editor global mode in sync even if the HWND does not + // exist yet. This way attaching the game later starts in the correct mode. + // + if (bActive) { + common->ActivateTool(false); + } + else { + common->ActivateTool(true); + } +} + +BOOL CGameDockWnd::IsGameWindowDocked() const { + return (m_hGameWnd && ::IsWindow(m_hGameWnd)) ? TRUE : FALSE; +} + +HWND CGameDockWnd::GetGameWindow() const { + return (m_hGameWnd && ::IsWindow(m_hGameWnd)) ? m_hGameWnd : NULL; +} + +void CGameDockWnd::FocusGameWindow() { + if (m_bActive && m_hGameWnd && ::IsWindow(m_hGameWnd)) { + ::SetFocus(m_hGameWnd); + } +} + +void CGameDockWnd::AttachGameWindow(HWND hGameWnd) { + if (!hGameWnd || !::IsWindow(hGameWnd) || !m_wndHost.GetSafeHwnd()) { + return; + } + + if (m_hGameWnd == hGameWnd) { + LayoutGameWindow(); + return; + } + + DetachGameWindow(TRUE); + + m_hGameWnd = hGameWnd; + m_hOldParent = ::GetParent(hGameWnd); + m_dwOldStyle = ::GetWindowLong(hGameWnd, GWL_STYLE); + m_dwOldExStyle = ::GetWindowLong(hGameWnd, GWL_EXSTYLE); + + ::SetParent(hGameWnd, m_wndHost.GetSafeHwnd()); + + LONG style = m_dwOldStyle; + style &= ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX); + style |= WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS; + ::SetWindowLong(hGameWnd, GWL_STYLE, style); + + LONG exStyle = m_dwOldExStyle; + exStyle &= ~(WS_EX_APPWINDOW | WS_EX_TOOLWINDOW | WS_EX_WINDOWEDGE | WS_EX_DLGMODALFRAME); + exStyle |= WS_EX_CONTROLPARENT; + ::SetWindowLong(hGameWnd, GWL_EXSTYLE, exStyle); + + ::SetWindowPos( + hGameWnd, + NULL, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW + ); + + LayoutGameWindow(); + + ::ShowWindow(hGameWnd, m_bActive ? SW_SHOW : SW_HIDE); + + Sys_GrabMouseCursor(m_bActive); + + IceDI_ForceOverrideSkipCenter(!m_bActive); + + if (m_bActive) { + // + // If the Game tab was already active before the HWND got attached, + // SetActive(TRUE) would not see a false->true transition, so force the + // initial activation here. + // + SendGameActivate(TRUE); + FocusGameWindow(); + } +} + +void CGameDockWnd::DetachGameWindow(BOOL bRestore) { + if (!m_hGameWnd || !::IsWindow(m_hGameWnd)) { + m_hGameWnd = NULL; + m_hOldParent = NULL; + m_dwOldStyle = 0; + m_dwOldExStyle = 0; + return; + } + + HWND hGameWnd = m_hGameWnd; + m_hGameWnd = NULL; + + ::ShowWindow(hGameWnd, SW_HIDE); + + if (bRestore) { + ::SetParent(hGameWnd, m_hOldParent); + ::SetWindowLong(hGameWnd, GWL_STYLE, m_dwOldStyle); + ::SetWindowLong(hGameWnd, GWL_EXSTYLE, m_dwOldExStyle); + ::SetWindowPos( + hGameWnd, + NULL, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED + ); + } + + m_hOldParent = NULL; + m_dwOldStyle = 0; + m_dwOldExStyle = 0; +} + +void CGameDockWnd::SetFillPercent(int percent) { + m_nFillPercent = percent; + LayoutGameWindow(); +} + +CRect CGameDockWnd::ComputeGameRect(const CRect &hostClient) const { + CRect rc = hostClient; + + if (hostClient.Width() <= 0 || hostClient.Height() <= 0) { + return rc; + } + + int targetW = hostClient.Width(); + int targetH = hostClient.Height(); + + if (m_nFillPercent == 0) { + // Fit to a 16:9 frame while preserving aspect ratio. + targetW = hostClient.Width(); + targetH = (targetW * 9) / 16; + + if (targetH > hostClient.Height()) { + targetH = hostClient.Height(); + targetW = (targetH * 16) / 9; + } + } else { + targetW = (hostClient.Width() * m_nFillPercent) / 100; + targetH = (hostClient.Height() * m_nFillPercent) / 100; + } + + if (targetW < 1) { + targetW = 1; + } + if (targetH < 1) { + targetH = 1; + } + + int x = hostClient.left + (hostClient.Width() - targetW) / 2; + int y = hostClient.top + (hostClient.Height() - targetH) / 2; + + rc.SetRect(x, y, x + targetW, y + targetH); + return rc; +} + +void CGameDockWnd::LayoutGameWindow() { + if (!m_hGameWnd || !::IsWindow(m_hGameWnd) || !m_wndHost.GetSafeHwnd()) { + return; + } + + CRect hostClient; + m_wndHost.GetClientRect(hostClient); + + CRect gameRect = ComputeGameRect(hostClient); + + ::MoveWindow( + m_hGameWnd, + gameRect.left, + gameRect.top, + gameRect.Width(), + gameRect.Height(), + TRUE + ); + + ::InvalidateRect(m_hGameWnd, NULL, FALSE); +} + +void CGameDockWnd::LayoutChildren() { + if (!GetSafeHwnd()) { + return; + } + + CRect client; + GetClientRect(client); + + const int stripH = 34; + const int gap = 6; + int x = client.left + 8; + int y = client.top + 4; + + if (m_wndTitle.GetSafeHwnd()) { + m_wndTitle.MoveWindow(x, y, 90, 24, TRUE); + x += 100; + } + + CWnd *buttons[] = { &m_btnFit, &m_btn100, &m_btn75, &m_btn50 }; + for (int i = 0; i < 4; i++) { + if (buttons[i]->GetSafeHwnd()) { + buttons[i]->MoveWindow(x, y, 54, 24, TRUE); + x += 54 + gap; + } + } + + CRect hostRect( + client.left + 4, + client.top + stripH + 4, + client.right - 4, + client.bottom - 4 + ); + + if (hostRect.Width() < 1 || hostRect.Height() < 1) { + return; + } + + if (m_wndHost.GetSafeHwnd()) { + m_wndHost.MoveWindow(hostRect, TRUE); + } + + LayoutGameWindow(); +} + +void CGameDockWnd::OnSize(UINT nType, int cx, int cy) { + CWnd::OnSize(nType, cx, cy); + LayoutChildren(); +} + +BOOL CGameDockWnd::OnEraseBkgnd(CDC *pDC) { + CRect client; + GetClientRect(client); + pDC->FillSolidRect(client, RGB(18, 18, 18)); + return TRUE; +} + +void CGameDockWnd::OnFillFit() { + SetFillPercent(0); + FocusGameWindow(); +} + +void CGameDockWnd::OnFill100() { + SetFillPercent(100); + FocusGameWindow(); +} + +void CGameDockWnd::OnFill75() { + SetFillPercent(75); + FocusGameWindow(); +} + +void CGameDockWnd::OnFill50() { + SetFillPercent(50); + FocusGameWindow(); +} + //============================================================================= // CXYDockWnd //============================================================================= @@ -766,11 +1201,13 @@ BEGIN_MESSAGE_MAP(CXYDockWnd, CWnd) ON_WM_SIZE() ON_WM_ERASEBKGND() ON_MESSAGE(WM_IDLEUPDATECMDUI, OnIdleUpdateCmdUI) + ON_NOTIFY(TCN_SELCHANGE, ID_XYDOCK_TABS, OnTabSelChange) END_MESSAGE_MAP() CXYDockWnd::CXYDockWnd() { m_pToolBar = NULL; m_bInLayout = false; + m_nActiveTab = XYDOCK_TAB_XY; } CXYDockWnd::~CXYDockWnd() { @@ -803,15 +1240,69 @@ int CXYDockWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) { return -1; } - if (!m_wndMenuBar.Create(this, 0x7A01)) { + + if (!m_wndTabs.Create( + WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | TCS_TABS | TCS_SINGLELINE, + CRect(0, 0, 0, 0), + this, + ID_XYDOCK_TABS + )) { return -1; } - if (!m_wndMDIContainer.Create(this, 0x7A02)) { + + TCITEM item; + memset(&item, 0, sizeof(item)); + item.mask = TCIF_TEXT; + + item.pszText = "XY"; + m_wndTabs.InsertItem(XYDOCK_TAB_XY, &item); + + item.pszText = "Game"; + m_wndTabs.InsertItem(XYDOCK_TAB_GAME, &item); + + CString pageClass = AfxRegisterWndClass( + CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, + ::LoadCursor(NULL, IDC_ARROW), + (HBRUSH)(COLOR_BTNFACE + 1), + NULL + ); + + if (!m_wndXYPage.CreateEx( + 0, + pageClass, + "RadiantXYTabPage", + WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, + CRect(0, 0, 0, 0), + &m_wndTabs, + ID_XYDOCK_XYPAGE + )) { return -1; } + + if (!m_wndGamePage.Create(&m_wndTabs, ID_XYDOCK_GAMEPAGE)) { + return -1; + } + m_wndGamePage.ShowWindow(SW_HIDE); + + if (!m_wndMenuBar.Create(&m_wndXYPage, 0x7A01)) { + return -1; + } + + if (!m_wndMDIContainer.Create(&m_wndXYPage, 0x7A02)) { + return -1; + } + + m_wndTabs.SetCurSel(XYDOCK_TAB_XY); return 0; } +CWnd *CXYDockWnd::GetXYPageParent() const { + if (m_wndXYPage.GetSafeHwnd()) { + return const_cast(&m_wndXYPage); + } + return const_cast(this); +} + void CXYDockWnd::SetChildWindows(CXYWnd *pXYWnd, CZWnd *pZWnd) { m_wndMDIContainer.SetChildWindows(pXYWnd, pZWnd); LayoutChildren(); @@ -825,15 +1316,14 @@ void CXYDockWnd::SetEmbeddedWindows(CWnd *pZWnd, CWnd *pXYWnd) { void CXYDockWnd::SetToolBar(CToolBar *pToolBar) { m_pToolBar = pToolBar; if (m_pToolBar && m_pToolBar->GetSafeHwnd()) { - CWnd *commandTarget = GetCommandTarget(); - if (!commandTarget || !commandTarget->GetSafeHwnd()) { - commandTarget = this; - } - - m_pToolBar->SetParent(this); + CWnd *toolbarParent = GetXYPageParent(); + m_pToolBar->SetParent(toolbarParent); m_pToolBar->SetOwner(this); m_pToolBar->SetDlgCtrlID(AFX_IDW_TOOLBAR); - m_pToolBar->ModifyStyle(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN); + m_pToolBar->ModifyStyle( + WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU, + WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN + ); } LayoutChildren(); } @@ -858,6 +1348,10 @@ CWnd *CXYDockWnd::GetCommandTarget() const { } void CXYDockWnd::UpdateToolBarCmdUI(BOOL bDisableIfNoHndler) { + if (m_nActiveTab != XYDOCK_TAB_XY) { + return; + } + if (!m_pToolBar || !m_pToolBar->GetSafeHwnd() || !m_pToolBar->IsWindowVisible()) { return; } @@ -923,6 +1417,121 @@ int CXYDockWnd::GetZPercent() const { return GetZDockPercent(); } +void CXYDockWnd::ShowActiveTab() { + const BOOL xyActive = (m_nActiveTab == XYDOCK_TAB_XY); + const BOOL gameActive = (m_nActiveTab == XYDOCK_TAB_GAME); + + if (m_wndXYPage.GetSafeHwnd()) { + m_wndXYPage.ShowWindow(xyActive ? SW_SHOW : SW_HIDE); + if (xyActive) { + m_wndXYPage.SetWindowPos(&wndTop, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } + } + + if (m_wndGamePage.GetSafeHwnd()) { + m_wndGamePage.ShowWindow(gameActive ? SW_SHOW : SW_HIDE); + if (gameActive) { + m_wndGamePage.SetWindowPos(&wndTop, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } + m_wndGamePage.SetActive(gameActive); + } +} + +void CXYDockWnd::SetActiveTab(int nTab) { + if (nTab != XYDOCK_TAB_XY && nTab != XYDOCK_TAB_GAME) { + nTab = XYDOCK_TAB_XY; + } + + const BOOL changed = (m_nActiveTab != nTab); + m_nActiveTab = nTab; + + if (m_wndTabs.GetSafeHwnd()) { + m_wndTabs.SetCurSel(nTab); + } + + LayoutChildren(); + + if (changed) { + if (m_nActiveTab == XYDOCK_TAB_GAME) { + // + // Game tab selected: + // - editor tool input off + // - game HWND receives WM_ACTIVATE/WA_ACTIVE through SetActive(TRUE) + // - keyboard/mouse focus moves to game + // + common->ActivateTool(false); + + if (m_wndGamePage.GetSafeHwnd()) { + m_wndGamePage.SetActive(TRUE); + } + + FocusGameWindow(); + } + else { + // + // XY tab selected: + // - game HWND receives WM_ACTIVATE/WA_INACTIVE through SetActive(FALSE) + // - editor tool input comes back + // - focus returns to XY + // + if (m_wndGamePage.GetSafeHwnd()) { + m_wndGamePage.SetActive(FALSE); + } + + common->ActivateTool(true); + FocusXYWindow(); + } + } +} + +void CXYDockWnd::OnTabSelChange(NMHDR *pNMHDR, LRESULT *pResult) { + int sel = m_wndTabs.GetCurSel(); + SetActiveTab(sel); + + if (pResult) { + *pResult = 0; + } +} + +void CXYDockWnd::SelectXYTab() { + SetActiveTab(XYDOCK_TAB_XY); +} + +void CXYDockWnd::SelectGameTab() { + SetActiveTab(XYDOCK_TAB_GAME); +} + +BOOL CXYDockWnd::IsGameTabActive() const { + return (m_nActiveTab == XYDOCK_TAB_GAME) ? TRUE : FALSE; +} + +void CXYDockWnd::AttachGameWindow(HWND hGameWnd) { + m_wndGamePage.AttachGameWindow(hGameWnd); + m_wndGamePage.SetActive(IsGameTabActive()); +} + +void CXYDockWnd::DetachGameWindow(BOOL bRestore) { + m_wndGamePage.DetachGameWindow(bRestore); +} + +BOOL CXYDockWnd::IsGameWindowDocked() const { + return m_wndGamePage.IsGameWindowDocked(); +} + +HWND CXYDockWnd::GetDockedGameWindow() const { + return m_wndGamePage.GetGameWindow(); +} + +void CXYDockWnd::FocusGameWindow() { + m_wndGamePage.FocusGameWindow(); +} + +void CXYDockWnd::FocusXYWindow() { + m_wndMDIContainer.FocusXYWindow(); +} + void CXYDockWnd::LayoutChildren() { if (!GetSafeHwnd() || m_bInLayout) { return; @@ -933,41 +1542,77 @@ void CXYDockWnd::LayoutChildren() { CRect client; GetClientRect(client); - int y = client.top; - const int width = client.Width(); - - if (m_wndMenuBar.GetSafeHwnd()) { - if (m_wndMenuBar.GetMenuHandle()) { - m_wndMenuBar.ShowWindow(SW_SHOW); - const int menuHeight = m_wndMenuBar.PreferredHeight(); - m_wndMenuBar.MoveWindow(client.left, y, width, menuHeight, TRUE); - y += menuHeight; - } else { - m_wndMenuBar.ShowWindow(SW_HIDE); - } - } - - if (m_pToolBar && m_pToolBar->GetSafeHwnd()) { - if (m_pToolBar->IsWindowVisible()) { - CSize toolbarSize = m_pToolBar->CalcFixedLayout(FALSE, TRUE); - int toolbarHeight = toolbarSize.cy; - if (toolbarHeight < 24) { - toolbarHeight = 24; - } - m_pToolBar->MoveWindow(client.left, y, width, toolbarHeight, TRUE); - y += toolbarHeight; - } - } - - CRect content(client.left, y, client.right, client.bottom); - if (content.Width() < 1 || content.Height() < 1) { + if (client.Width() < 1 || client.Height() < 1) { m_bInLayout = false; return; } - if (m_wndMDIContainer.GetSafeHwnd()) { - m_wndMDIContainer.MoveWindow(content, TRUE); - m_wndMDIContainer.LayoutChildren(); + CRect pageRect(0, 0, client.Width(), client.Height()); + + if (m_wndTabs.GetSafeHwnd()) { + m_wndTabs.MoveWindow(client, TRUE); + + // The tab pages are children of the tab control, so use tab-client + // coordinates here. Creating/moving them as siblings of the tab control + // can leave them behind the tab control's client area, which makes both + // tabs look empty even though their child windows exist. + m_wndTabs.GetClientRect(&pageRect); + m_wndTabs.AdjustRect(FALSE, &pageRect); + pageRect.DeflateRect(2, 2); + } + + if (m_wndXYPage.GetSafeHwnd()) { + m_wndXYPage.MoveWindow(pageRect, TRUE); + } + + if (m_wndGamePage.GetSafeHwnd()) { + m_wndGamePage.MoveWindow(pageRect, TRUE); + m_wndGamePage.LayoutChildren(); + } + + ShowActiveTab(); + + if (m_pToolBar && m_pToolBar->GetSafeHwnd() && m_wndXYPage.GetSafeHwnd()) { + if (::GetParent(m_pToolBar->GetSafeHwnd()) != m_wndXYPage.GetSafeHwnd()) { + m_pToolBar->SetParent(&m_wndXYPage); + } + } + + if (m_wndXYPage.GetSafeHwnd()) { + CRect xyClient; + m_wndXYPage.GetClientRect(xyClient); + + int y = xyClient.top; + const int width = xyClient.Width(); + + if (m_wndMenuBar.GetSafeHwnd()) { + if (m_wndMenuBar.GetMenuHandle()) { + m_wndMenuBar.ShowWindow(SW_SHOW); + const int menuHeight = m_wndMenuBar.PreferredHeight(); + m_wndMenuBar.MoveWindow(xyClient.left, y, width, menuHeight, TRUE); + y += menuHeight; + } else { + m_wndMenuBar.ShowWindow(SW_HIDE); + } + } + + if (m_pToolBar && m_pToolBar->GetSafeHwnd()) { + if (m_pToolBar->IsWindowVisible()) { + CSize toolbarSize = m_pToolBar->CalcFixedLayout(FALSE, TRUE); + int toolbarHeight = toolbarSize.cy; + if (toolbarHeight < 24) { + toolbarHeight = 24; + } + m_pToolBar->MoveWindow(xyClient.left, y, width, toolbarHeight, TRUE); + y += toolbarHeight; + } + } + + CRect content(xyClient.left, y, xyClient.right, xyClient.bottom); + if (content.Width() >= 1 && content.Height() >= 1 && m_wndMDIContainer.GetSafeHwnd()) { + m_wndMDIContainer.MoveWindow(content, TRUE); + m_wndMDIContainer.LayoutChildren(); + } } m_bInLayout = false; @@ -978,6 +1623,10 @@ void CXYDockWnd::RecalcLayout() { } BOOL CXYDockWnd::TrackMenuMnemonic(UINT nChar) { + if (m_nActiveTab != XYDOCK_TAB_XY) { + return FALSE; + } + if (m_wndMenuBar.GetSafeHwnd()) { return m_wndMenuBar.TrackMnemonic(nChar); } @@ -999,6 +1648,10 @@ LRESULT CXYDockWnd::OnIdleUpdateCmdUI(WPARAM wParam, LPARAM lParam) { } BOOL CXYDockWnd::OnCommand(WPARAM wParam, LPARAM lParam) { + if (m_nActiveTab != XYDOCK_TAB_XY) { + return CWnd::OnCommand(wParam, lParam); + } + CWnd *commandTarget = GetCommandTarget(); if (commandTarget && commandTarget->GetSafeHwnd() && commandTarget != this) { return static_cast(commandTarget->SendMessage(WM_COMMAND, wParam, lParam)); @@ -1007,6 +1660,10 @@ BOOL CXYDockWnd::OnCommand(WPARAM wParam, LPARAM lParam) { } BOOL CXYDockWnd::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT *pResult) { + if (wParam == ID_XYDOCK_TABS) { + return CWnd::OnNotify(wParam, lParam, pResult); + } + CWnd *commandTarget = GetCommandTarget(); if (commandTarget && commandTarget->GetSafeHwnd() && commandTarget != this) { LRESULT result = commandTarget->SendMessage(WM_NOTIFY, wParam, lParam); @@ -1026,6 +1683,10 @@ BOOL CXYDockWnd::OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHANDLERINFO return TRUE; } + if (m_nActiveTab != XYDOCK_TAB_XY) { + return FALSE; + } + CWnd *commandTarget = GetCommandTarget(); if (commandTarget && commandTarget->GetSafeHwnd() && commandTarget != this) { return commandTarget->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); diff --git a/neo/engine/tools/radiant/XYWnd.h b/neo/engine/tools/radiant/XYWnd.h index 11af5920..db08b643 100644 --- a/neo/engine/tools/radiant/XYWnd.h +++ b/neo/engine/tools/radiant/XYWnd.h @@ -39,6 +39,7 @@ If you have questions concerning this license or the applicable additional terms #include "qe3.h" #include "CamWnd.h" +#include class CXYWnd; @@ -109,6 +110,8 @@ public: int GetZDockPercent() const; int GetZWidth() const; int GetFixedZWidth() const; + void FocusXYWindow(); + CWnd *GetXYWindow() const; protected: CWnd *m_pXYWnd; @@ -143,16 +146,83 @@ protected: DECLARE_MESSAGE_MAP() }; +//============================================================================= +// CGameDockWnd +// +// Docked game preview page used by CXYDockWnd's Game tab. This class owns a +// small command strip and a black child host window. The real game HWND is +// reparented into the host so the existing renderer/context can keep using the +// same window instead of a popup. +//============================================================================= +class CGameDockWnd : public CWnd +{ + DECLARE_DYNAMIC(CGameDockWnd) + +public: + CGameDockWnd(); + virtual ~CGameDockWnd(); + + BOOL Create(CWnd *pParent, UINT nID); + void AttachGameWindow(HWND hGameWnd); + void DetachGameWindow(BOOL bRestore = TRUE); + BOOL IsGameWindowDocked() const; + HWND GetGameWindow() const; + void FocusGameWindow(); + + void SetActive(BOOL bActive); + void LayoutChildren(); + void SendGameActivate(BOOL bActive); +protected: + CStatic m_wndTitle; + CButton m_btnFit; + CButton m_btn100; + CButton m_btn75; + CButton m_btn50; + CWnd m_wndHost; + + HWND m_hGameWnd; + HWND m_hOldParent; + LONG m_dwOldStyle; + LONG m_dwOldExStyle; + + int m_nFillPercent; // 0 = fit 16:9, otherwise percentage of host area + BOOL m_bActive; + + void SetFillPercent(int percent); + void LayoutGameWindow(); + CRect ComputeGameRect(const CRect &hostClient) const; + + afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg BOOL OnEraseBkgnd(CDC *pDC); + afx_msg void OnDestroy(); + afx_msg void OnSetFocus(CWnd *pOldWnd); + + afx_msg void OnFillFit(); + afx_msg void OnFill100(); + afx_msg void OnFill75(); + afx_msg void OnFill50(); + + DECLARE_MESSAGE_MAP() +}; + //============================================================================= // CXYDockWnd // -// Owns the embedded XY editor layout: +// Owns the embedded XY editor layout and the new docked game preview: +// [ tabs: XY | Game ] +// +// XY tab: // [ menu bar ] // [ toolbar ] // [ MDI client: Z window | splitter | XY top render window ] // +// Game tab: +// [ game command strip / fill controls ] +// [ hosted game HWND ] +// // The docked Z window starts at 5% of the MDI container width. The divider in -// the MDI client can be dragged to resize the Z and XY top panes. +// the MDI client can be dragged to resize the Z and XY top panes interactively. //============================================================================= class CXYDockWnd : public CWnd { @@ -187,16 +257,41 @@ public: int GetZDockPercent() const; int GetZPercent() const; + void AttachGameWindow(HWND hGameWnd); + void DetachGameWindow(BOOL bRestore = TRUE); + BOOL IsGameWindowDocked() const; + HWND GetDockedGameWindow() const; + BOOL IsGameTabActive() const; + void SelectXYTab(); + void SelectGameTab(); + void FocusGameWindow(); + void FocusXYWindow(); + protected: + enum { + XYDOCK_TAB_XY = 0, + XYDOCK_TAB_GAME = 1 + }; + + CTabCtrl m_wndTabs; + CWnd m_wndXYPage; + CGameDockWnd m_wndGamePage; + CXYMenuBar m_wndMenuBar; CXYMDIContainerWnd m_wndMDIContainer; CToolBar *m_pToolBar; bool m_bInLayout; + int m_nActiveTab; + + void SetActiveTab(int nTab); + void ShowActiveTab(); + CWnd *GetXYPageParent() const; afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg BOOL OnEraseBkgnd(CDC *pDC); afx_msg LRESULT OnIdleUpdateCmdUI(WPARAM wParam, LPARAM lParam); + afx_msg void OnTabSelChange(NMHDR *pNMHDR, LRESULT *pResult); virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam); virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT *pResult); virtual BOOL OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHANDLERINFO *pHandlerInfo); diff --git a/q4base/q4config.qe4 b/q4base/q4config.qe4 new file mode 100644 index 00000000..c17fccb5 --- /dev/null +++ b/q4base/q4config.qe4 @@ -0,0 +1,18 @@ +{ +"mapspath" "w:\doom\base\maps" +"bsp" "" +"bsp noflood" "" +"bsp shadowOpt 2" "" +"bsp noaas" "" +"bspext" "" +"bspext noflood" "" +"autosave" "c:\autosave.map" +"autosave1" "c:\autosave1.map" +"autosave2" "c:\autosave2.map" +"texturepath" "w:\doom\base\textures" +"entitypath" "w:\doom\base\scripts\*.def" +"remotebasepath" "w:\doom\base" +"rshcmd" "" +"basepath" "w:\doom\base" +"brush_primit" "1" +}