From ec0ada5614012f9d71c3b1054a3b5504c2ebfa0f Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Sat, 16 May 2026 19:16:30 +0800 Subject: [PATCH 01/21] Refactor ddraw render backend into software surface + directx12 swapchain --- .gitignore | 1 + Phobos.props | 2 +- Phobos.vcxproj | 14 + YRpp | 2 +- src/Render/Functions.cpp | 556 +++++++++++++++ src/Render/Functions.h | 25 + src/Render/Hooks.Mouse.cpp | 60 ++ src/Render/Hooks.Options.cpp | 15 + src/Render/Hooks.Surface.cpp | 14 + src/Render/Hooks.cpp | 217 ++++++ src/Render/Mouse.cpp | 332 +++++++++ src/Render/Mouse.h | 106 +++ src/Render/Options.cpp | 0 src/Render/Options.h | 15 + src/Render/Renderer.cpp | 1231 ++++++++++++++++++++++++++++++++++ src/Render/Renderer.h | 142 ++++ src/Render/Surface.cpp | 286 ++++++++ src/Render/Surface.h | 99 +++ 18 files changed, 3115 insertions(+), 2 deletions(-) create mode 100644 src/Render/Functions.cpp create mode 100644 src/Render/Functions.h create mode 100644 src/Render/Hooks.Mouse.cpp create mode 100644 src/Render/Hooks.Options.cpp create mode 100644 src/Render/Hooks.Surface.cpp create mode 100644 src/Render/Hooks.cpp create mode 100644 src/Render/Mouse.cpp create mode 100644 src/Render/Mouse.h create mode 100644 src/Render/Options.cpp create mode 100644 src/Render/Options.h create mode 100644 src/Render/Renderer.cpp create mode 100644 src/Render/Renderer.h create mode 100644 src/Render/Surface.cpp create mode 100644 src/Render/Surface.h diff --git a/.gitignore b/.gitignore index ea89168d2d..913d5a2244 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ out .venv/ debug.log +Phobos.log \ No newline at end of file diff --git a/Phobos.props b/Phobos.props index bee83b6da5..e24e1ac0b2 100644 --- a/Phobos.props +++ b/Phobos.props @@ -30,7 +30,7 @@ $(Configuration)\ $(Configuration)\IntDir\ - dbghelp.lib;onecore.lib + dbghelp.lib;onecore.lib;imm32.lib diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 5d2d15eca0..3ea12b25cf 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -265,6 +265,15 @@ + + + + + + + + + @@ -388,6 +397,11 @@ + + + + + diff --git a/YRpp b/YRpp index 613922aa10..171250122a 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit 613922aa10f47d547baa00ab4b67e04b6b5de4b7 +Subproject commit 171250122ad6bc5b461b2b896f96eb6ec2d0ba16 diff --git a/src/Render/Functions.cpp b/src/Render/Functions.cpp new file mode 100644 index 0000000000..da3c6cb5ad --- /dev/null +++ b/src/Render/Functions.cpp @@ -0,0 +1,556 @@ +#include "Functions.h" + +#include + +#include "Surface.h" +#include "Renderer.h" +#include "Mouse.h" +#include "Options.h" + +#include +#include +#include +#include + +#include + +#include +#include + +bool __fastcall RenderDX::AllocateSurfaces(const RectangleStruct& hidden_rect, const RectangleStruct& composite_rect, const RectangleStruct& tile_rect, const RectangleStruct& sidebar_rect, bool hidden_first) { + Debug::Log("[RenderDX] Allocating new surfaces\n"); + + if (DSurface::Alternate) { + Debug::Log("[RenderDX] Deleting AlternateSurface\n"); + GameDelete(DSurface::Alternate); + DSurface::Alternate = nullptr; + } + + if (DSurface::Hidden) { + Debug::Log("[RenderDX] Deleting HiddenSurface\n"); + GameDelete(DSurface::Hidden); + DSurface::Hidden = nullptr; + } + + if (DSurface::Composite) { + Debug::Log("[RenderDX] Deleting CompositeSurface\n"); + GameDelete(DSurface::Composite); + DSurface::Composite = nullptr; + } + + if (DSurface::Tile) { + Debug::Log("[RenderDX] Deleting TileSurface\n"); + GameDelete(DSurface::Tile); + DSurface::Tile = nullptr; + } + + if (DSurface::Sidebar) { + Debug::Log("[RenderDX] Deleting SidebarSurface\n"); + GameDelete(DSurface::Sidebar); + DSurface::Sidebar = nullptr; + } + + if (hidden_first && hidden_rect.Width > 0 && hidden_rect.Height > 0) { + DSurface::Hidden = GameCreate(hidden_rect.Width, hidden_rect.Height); + DSurface::Hidden->Fill(0); + Debug::Log("[RenderDX] HiddenSurface (%dx%d)\n", hidden_rect.Width, hidden_rect.Height); + } + + if (composite_rect.Width > 0 && composite_rect.Height > 0) { + DSurface::Composite = GameCreate(composite_rect.Width, composite_rect.Height); + DSurface::Composite->Fill(0); + Debug::Log("[RenderDX] CompositeSurface (%dx%d)\n", composite_rect.Width, composite_rect.Height); + } + + if (tile_rect.Width > 0 && tile_rect.Height > 0) { + DSurface::Tile = GameCreate(tile_rect.Width, tile_rect.Height); + DSurface::Tile->Fill(0); + Debug::Log("[RenderDX] TileSurface (%dx%d)\n", tile_rect.Width, tile_rect.Height); + } + + if (sidebar_rect.Width > 0 && sidebar_rect.Height > 0) { + DSurface::Sidebar = GameCreate(sidebar_rect.Width, sidebar_rect.Height); + DSurface::Sidebar->Fill(0); + Debug::Log("[RenderDX] SidebarSurface (%dx%d)\n", sidebar_rect.Width, sidebar_rect.Height); + } + + if (!hidden_first && hidden_rect.Width > 0 && hidden_rect.Height > 0) { + DSurface::Hidden = GameCreate(hidden_rect.Width, hidden_rect.Height); + DSurface::Hidden->Fill(0); + Debug::Log("[RenderDX] HiddenSurface (%dx%d)\n", hidden_rect.Width, hidden_rect.Height); + } + + if (hidden_rect.Width > 0 && hidden_rect.Height > 0) { + DSurface::Alternate = GameCreate(hidden_rect.Width, hidden_rect.Height); + DSurface::Alternate->Fill(0); + Debug::Log("[RenderDX] AlternateSurface (%dx%d)\n", hidden_rect.Width, hidden_rect.Height); + } + + return true; +} + +bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bits_per_pixel) { + Debug::Log("[RenderDX] Setting video mode to %dx%d@%d\n", width, height, bits_per_pixel); + + if (!DXRenderer::Instance().IsRendererReady()) { + Debug::Log("[RenderDX] Renderer is not ready\n"); + return false; + } + + ResetVideoMode(); + if (!DXRenderer::Instance().CreateRenderer(width, height, bits_per_pixel)) { + Debug::Log("[RenderDX] Failed to create renderer\n"); + return false; + } + + Drawing::RenderWidth = width; + Drawing::RenderHeight = height; + Drawing::RenderBitsPerPixel = bits_per_pixel; + + RenderDX::UpdateScale(); + + return true; +} + +void __fastcall RenderDX::ResetVideoMode() { + Debug::Log("[RenderDX] Resetting video mode\n"); + + DXRenderer::Instance().DestroyRenderer(); + + Drawing::RenderWidth = 0; + Drawing::RenderHeight = 0; + Drawing::RenderBitsPerPixel = 0; + + RenderDX::ResetScale(); +} + +static bool WindowResizeInProgress = false; +static bool DeferredWindowResize = false; +static int DeferredWindowWidth = 0; +static int DeferredWindowHeight = 0; + +static void RecalcMouseWindowRegion(bool rebuildCursor) { + if (!DXMouse::Instance) + return; + + DXMouse::Instance->Recalc_Capture_Region(); + if (rebuildCursor) + DXMouse::Instance->Rebuild_Cursor_Image(); +} + +static void ApplyWindowResize(int width, int height) { + DXRenderer::Instance().ResizeWindow(width, height); + RecalcMouseWindowRegion(true); +} + +static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + switch (msg) { + case WM_MOUSEMOVE: + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_LBUTTONDBLCLK: + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + case WM_RBUTTONDBLCLK: + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + case WM_MBUTTONDBLCLK: + case WM_MOUSEWHEEL: + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: + { + // Scale mouse inputs before they are processed by SDL or the game. + if (RenderDX::ShouldScale()) { + int x = GET_X_LPARAM(lparam); + int y = GET_Y_LPARAM(lparam); + + x = RenderDX::ClientToRenderX(x); + y = RenderDX::ClientToRenderY(y); + + lparam = MAKELPARAM(x, y); + } + break; + } + + case WM_MOVE: + { + if (DXMouse::Instance) { + DXMouse::Instance->Recalc_Capture_Region(); + } + return 0; // handled + } + + case WM_ENTERSIZEMOVE: + { + WindowResizeInProgress = true; + DeferredWindowResize = false; + DeferredWindowWidth = 0; + DeferredWindowHeight = 0; + return 0; // handled + } + + case WM_EXITSIZEMOVE: + { + WindowResizeInProgress = false; + + if (DeferredWindowResize) { + int width = DeferredWindowWidth; + int height = DeferredWindowHeight; + + RECT clientRect {}; + if (::GetClientRect(hwnd, &clientRect)) { + width = clientRect.right - clientRect.left; + height = clientRect.bottom - clientRect.top; + } + + if (width > 0 && height > 0) + ApplyWindowResize(width, height); + + DeferredWindowResize = false; + DeferredWindowWidth = 0; + DeferredWindowHeight = 0; + } + + return 0; // handled + } + + case WM_SIZE: + { + const int width = LOWORD(lparam); + const int height = HIWORD(lparam); + if (wparam == SIZE_MINIMIZED || width == 0 || height == 0) { + DeferredWindowResize = false; + RecalcMouseWindowRegion(false); + } + else if (WindowResizeInProgress) { + DeferredWindowResize = true; + DeferredWindowWidth = width; + DeferredWindowHeight = height; + RecalcMouseWindowRegion(false); + } + else { + ApplyWindowResize(width, height); + } + + return 0; // handled + } + + case WM_SYSKEYDOWN: + { + // Handle Alt+Enter for fullscreen toggle + if (wparam == VK_RETURN && (lparam & (1 << 29))) { + DXRenderer::Instance().ToggleFullscreen(); + if (DXMouse::Instance) { + DXMouse::Instance->Recalc_Capture_Region(); + DXMouse::Instance->Rebuild_Cursor_Image(); + } + return 0; // handled + } + break; + } + + case WM_SETCURSOR: + { + // Prevent the system from setting the cursor when it's over our window, since we handle it ourselves. + if (LOWORD(lparam) == HTCLIENT) { + if (DXMouse::Instance) + DXMouse::Instance->Set_Cached_Cursor(); + return TRUE; // handled + } + break; + } + + case WM_ACTIVATEAPP: + { + if (DXRenderOptions::Config().PauseGameWhenLoseFocus) + break; // goto the original window procedure to allow the game to pause when losing focus + if (hwnd == Game::hWnd) { + Unsorted::ScenarioInit = true; // game is always active + if (wparam) { + Debug::Log("[RenderDX] Game window activated\n"); + if (DXMouse::Instance) + DXMouse::Instance->Capture_Mouse(); + } + else { + Debug::Log("[RenderDX] Game window deactivated\n"); + if (DXMouse::Instance) + DXMouse::Instance->Release_Mouse(); + } + } + return 0; // handled - prevent the game from pausing when the window is deactivated + } + } + + // Call original window procedure for default processing + return reinterpret_cast(0x7775C0)(hwnd, msg, wparam, lparam); +} + +void __fastcall RenderDX::CreateMainWindow(HINSTANCE instance, int cmd_show, int width, int height) { + Debug::Log("[RenderDX] Creating main window\n"); + if (!DXRenderer::Instance().CreateMainWindow(instance, cmd_show, width, height, MainWindowProc)) { + Debug::Log("[RenderDX] Failed to create main window\n"); + ::MessageBoxA(nullptr, "Failed to create main window", "Error", MB_ICONERROR); + ::ExitProcess(0xC0DEBEEF); + } +} + +void __fastcall RenderDX::DestroyMainWindow() { + Debug::Log("[RenderDX] Destroying main window\n"); + DXRenderer::Instance().DestroyMainWindow(); +} + +bool __fastcall RenderDX::UpdateScreen(Surface* surface) { + if (!surface) { + Debug::Log("[RenderDX] UpdateScreen called with null surface\n"); + return false; + } + + const bool shouldScale = ShouldScale(); + DXRenderer::Instance().SetRenderScale(shouldScale); + + // Retrieve the game surface data + if (void* pixels = surface->Lock(0, 0)) { + if (!DXRenderer::Instance().UploadSurfaceToTexture(pixels, surface->GetPitch())) { + Debug::Log("[RenderDX] Failed to upload surface to texture\n"); + surface->Unlock(); + return false; + } + surface->Unlock(); + } + + static bool scaled = ShouldScale(); + + // Extra process on scaling change + if (scaled != shouldScale) { + scaled = shouldScale; + if (DXMouse::Instance) + DXMouse::Instance->Rebuild_Cursor_Image(); + } + + DXRenderer::Instance().Present(); + + return true; +} + +bool __fastcall RenderDX::ShouldScale() { + return Unsorted::SpecialDialog == 0 && Unsorted::WSDialogCount == 0; +} + +static void RebuildDisplayState(const RectangleStruct& view_rect) { + auto temp = view_rect; + temp.X = GameOptionsClass::Instance.SidebarMode ? 0 : 168; + temp.Y = 16; + temp.Width -= 168; + temp.Height -= 16; + + DSurface::ViewBounds = view_rect; + Drawing::RenderWidth = view_rect.Width; + Drawing::RenderHeight = view_rect.Height; + + DSurface::Primary = DXSurface::CreatePrimary(); + + RenderDX::AllocateSurfaces( + view_rect, + RectangleStruct { 0,0,temp.Width,view_rect.Height }, + RectangleStruct { 0,0,temp.Width,view_rect.Height }, + RectangleStruct { 0,0,168,view_rect.Height }, + false + ); + DSurface::Temp = DSurface::Hidden; + + if (DXMouse::Instance) { + DXMouse::Instance->Rebuild_Cursor_Image(); + } + + SidebarClass::Instance.Set_View_Dimensions(temp); + SidebarClass::Instance.Init_IO(); + SidebarClass::Instance.Activate(1); + SidebarClass::Instance.InitGUI(); + SidebarClass::Instance.MarkNeedsRedraw(2); // REDRAW_ALL + DXMouse::Instance->Show_Mouse(); +} + +bool __fastcall RenderDX::ChangeDisplayMode(int width, int height) { + Debug::Log("[RenderDX] Changing display mode to %dx%d\n", width, height); + + // Save current window position + RectangleStruct old_rect = DSurface::ViewBounds; + if (old_rect.Width <= 0 || old_rect.Height <= 0) { + if (Drawing::RenderWidth > 0 && Drawing::RenderHeight > 0) { + Debug::Log("[RenderDX] Current view bounds are invalid, using RenderWidth/RenderHeight\n"); + old_rect = RectangleStruct { 0, 0, Drawing::RenderWidth, Drawing::RenderHeight }; + } + } + + const int old_render_width = Drawing::RenderWidth; + const int old_render_height = Drawing::RenderHeight; + const int old_render_bpp = Drawing::RenderBitsPerPixel; + + int old_window_x = 0; + int old_window_y = 0; + int old_window_width = DXRenderer::Instance().GetWindowWidth(); + int old_window_height = DXRenderer::Instance().GetWindowHeight(); + + DXMouse::Instance->Hide_Mouse(); + + // Delete the old primary surface + if (DSurface::Primary) { + Debug::Log("[RenderDX] Deleting old primary surface\n"); + GameDelete(DSurface::Primary); + DSurface::Primary = nullptr; + } + + if (DXRenderer::Instance().IsWindowed()) { + int window_width = width; + int window_height = height; + + RECT temp; + ::GetWindowRect(Game::hWnd, &temp); + old_window_x = temp.left; + old_window_y = temp.top; + old_window_width = temp.right - temp.left; + old_window_height = temp.bottom - temp.top; + + int center_x = old_window_x + old_window_width / 2; + int center_y = old_window_y + old_window_height / 2; + + int new_x = center_x - window_width / 2; + int new_y = center_y - window_height / 2; + + DXRenderer::Instance().MoveWindow(new_x, new_y, window_width, window_height); + + Debug::Log("[RenderDX] Moved window to (%d, %d) with size %dx%d\n", new_x, new_y, window_width, window_height); + } + + // Recreate all intermediates + if (!SetVideoMode(Game::hWnd, width, height, 16)) { + if (DXRenderer::Instance().IsWindowed()) { + DXRenderer::Instance().MoveWindow(old_window_x, old_window_y, old_window_width, old_window_height); + Debug::Log("[RenderDX] Restore window to (%d, %d) with size %dx%d\n", old_window_x, old_window_y, old_window_width, old_window_height); + } + + if (old_rect.X > 0 && old_rect.Y > 0 && old_render_width > 0 && old_render_height > 0) { + Debug::Log("[RenderDX] Restoring old display mode.\n"); + if (!SetVideoMode(Game::hWnd, old_render_width, old_render_height, old_render_bpp)) { + Debug::Log("[RenderDX] Failed to restore old display mode.\n"); + DXMouse::Instance->Show_Mouse(); + return false; + } + RebuildDisplayState(old_rect); + } + else { + Debug::Log("[RenderDX] Old view bounds are invalid, cannot restore\n"); + } + + DXMouse::Instance->Show_Mouse(); + return false; + } + + RectangleStruct new_view_rect = { 0, 0, width, height }; + RebuildDisplayState(new_view_rect); + Debug::Log("[RenderDX]: ViewBounds: %dx%d\n", width, height); + Debug::Log("[RenderDX] Mode change complete.\n"); + + return true; +} + +static float ScaleX = 1.0f; +static float ScaleY = 1.0f; +static float ViewportX = 0.0f; +static float ViewportY = 0.0f; + +float __fastcall RenderDX::GetXScale() { + return ScaleX; +} + +float __fastcall RenderDX::GetYScale() { + return ScaleY; +} + +int __fastcall RenderDX::ClientToRenderX(int x) { + if (Drawing::RenderWidth <= 0) + return x; + + return std::clamp(static_cast((x - ViewportX) * ScaleX), 0, Drawing::RenderWidth - 1); +} + +int __fastcall RenderDX::ClientToRenderY(int y) { + if (Drawing::RenderHeight <= 0) + return y; + + return std::clamp(static_cast((y - ViewportY) * ScaleY), 0, Drawing::RenderHeight - 1); +} + +void __fastcall RenderDX::UpdateScale() { + const float viewportWidth = DXRenderer::Instance().GetViewportWidth(); + const float viewportHeight = DXRenderer::Instance().GetViewportHeight(); + ViewportX = DXRenderer::Instance().GetViewportX(); + ViewportY = DXRenderer::Instance().GetViewportY(); + + if (Drawing::RenderWidth <= 0 || Drawing::RenderHeight <= 0 || viewportWidth <= 0.0f || viewportHeight <= 0.0f) { + ResetScale(); + return; + } + + ScaleX = static_cast(Drawing::RenderWidth) / viewportWidth; + ScaleY = static_cast(Drawing::RenderHeight) / viewportHeight; +} + +void __fastcall RenderDX::ResetScale() { + ScaleX = 1.0f; + ScaleY = 1.0f; + ViewportX = 0.0f; + ViewportY = 0.0f; +} + +int* __fastcall RenderDX::EnumDisplayModes(DWORD minw, DWORD minh, DWORD maxw, DWORD maxh, DWORD bitdepth) { + std::vector> modes; + DEVMODE devmode{}; + DWORD mode_index = 0; + + while (::EnumDisplaySettingsA(nullptr, mode_index++, &devmode)) { + const DWORD w = devmode.dmPelsWidth; + const DWORD h = devmode.dmPelsHeight; + const DWORD bpp = devmode.dmBitsPerPel; + + if (w >= minw && h >= minh && w <= maxw && h <= maxh && bpp == bitdepth) { + modes.emplace_back(static_cast(w), static_cast(h)); + } + } + + if (modes.empty()) { + return nullptr; + } + + std::sort(modes.begin(), modes.end()); + modes.erase(std::unique(modes.begin(), modes.end()), modes.end()); + + const size_t count = modes.size(); + const size_t bytes = sizeof(int) * (count * 2 + 1); + + int* list = static_cast(YRMemory::Allocate(bytes)); + std::memset(list, 0, bytes); + + int* ptr = list; + for (const auto& mode : modes) { + *ptr++ = mode.first; + *ptr++ = mode.second; + } + + return list; +} + +void __fastcall RenderDX::MainProcHandlePaint() { + if (DXMouse::Instance && DSurface::Primary && DSurface::Hidden && DSurface::Composite) { + if (Unsorted::ScenarioStarted) { + GScreenClass::UpdatePrimarySurface(DXMouse::Instance->Is_Captured(), DSurface::Composite, nullptr); + SidebarClass::Instance.BlitSidebar(true); + } + else if (Game::IsMoviePlaying()) { + Game::BlitMovie(); + } + else { + GScreenClass::UpdatePrimarySurface(DXMouse::Instance->Is_Captured(), DSurface::Hidden, nullptr); + } + } +} diff --git a/src/Render/Functions.h b/src/Render/Functions.h new file mode 100644 index 0000000000..8f97794811 --- /dev/null +++ b/src/Render/Functions.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +class Surface; + +class RenderDX { +public: + static bool __fastcall AllocateSurfaces(const RectangleStruct& hidden_rect, const RectangleStruct& composite_rect, const RectangleStruct& tile_rect, const RectangleStruct& sidebar_rect, bool hidden_first); + static bool __fastcall SetVideoMode(HWND, int width, int height, int bits_per_pixel); + static void __fastcall ResetVideoMode(); + static void __fastcall CreateMainWindow(HINSTANCE instance, int cmd_show, int width, int height); + static void __fastcall DestroyMainWindow(); + static bool __fastcall UpdateScreen(Surface* surface); + static bool __fastcall ShouldScale(); + static bool __fastcall ChangeDisplayMode(int width, int height); + static float __fastcall GetXScale(); + static float __fastcall GetYScale(); + static int __fastcall ClientToRenderX(int x); + static int __fastcall ClientToRenderY(int y); + static void __fastcall UpdateScale(); + static void __fastcall ResetScale(); + static int* __fastcall EnumDisplayModes(DWORD minw, DWORD minh, DWORD maxw, DWORD maxh, DWORD bitdepth); + static void __fastcall MainProcHandlePaint(); +}; diff --git a/src/Render/Hooks.Mouse.cpp b/src/Render/Hooks.Mouse.cpp new file mode 100644 index 0000000000..c60153197c --- /dev/null +++ b/src/Render/Hooks.Mouse.cpp @@ -0,0 +1,60 @@ +#include "Mouse.h" + +#include +#include + +#include +#include +#include + +#include + +DEFINE_HOOK(0x6BDEF9, WinMain_CreateWWMouse, 0x5) { + DXMouse::Instance = GameCreate(DSurface::Primary, Game::hWnd); + R->EAX(DXMouse::Instance); + return 0x6BDF25; +} + +static HANDLE MouseThread; +static std::binary_semaphore MouseThreadSemaphore { 0 }; + +static DWORD WINAPI _MouseThread(LPVOID) { + while (!MouseThreadSemaphore.try_acquire_for(std::chrono::milliseconds(10))) { + if (DXMouse::Instance) + DXMouse::Instance->Process_Mouse(); + } + return 0; +} + +static void __fastcall _DXMouse_StartMouseThread() { + MouseThread = ::CreateThread(nullptr, 0, _MouseThread, nullptr, 0, nullptr); + if (!MouseThread) { + MouseThreadSemaphore.release(); + return; + } + ::SetThreadPriority(MouseThread, THREAD_PRIORITY_TIME_CRITICAL); +} +DEFINE_FUNCTION_JUMP(LJMP, 0x7B84F0, _DXMouse_StartMouseThread); + +static void __fastcall _DXMouse_EndMouseThread() { + MouseThreadSemaphore.release(); + if (MouseThread) { + ::WaitForSingleObject(MouseThread, INFINITE); + ::CloseHandle(MouseThread); + MouseThread = nullptr; + } +} +DEFINE_FUNCTION_JUMP(LJMP, 0x7B86B0, _DXMouse_EndMouseThread); + +static void __fastcall _DXMouse_ProcessMouse(DXMouse* This) { + This->Process_Mouse(); +} +DEFINE_FUNCTION_JUMP(LJMP, 0x7BA090, _DXMouse_ProcessMouse); + +DEFINE_HOOK_AGAIN(0x72429E, DXMouse_TooltipManager_GetMousePosition, 0xA); +DEFINE_HOOK(0x724359, DXMouse_TooltipManager_GetMousePosition, 0xA) { + GET(ToolTipManager*, pThis, ESI); + pThis->CurrentMousePosition = DXMouse::Instance->Get_Mouse_Point(); + R->EBX(&pThis->CurrentMousePosition); + return R->Origin() + 0x15; +} diff --git a/src/Render/Hooks.Options.cpp b/src/Render/Hooks.Options.cpp new file mode 100644 index 0000000000..e25f51c332 --- /dev/null +++ b/src/Render/Hooks.Options.cpp @@ -0,0 +1,15 @@ +#include + +#include "Options.h" + +#include + +DEFINE_HOOK(0x6BC141, DXRender_LoadConfigFromRA2MD, 0x7) +{ + auto& config = DXRenderOptions::Config(); + config.PreserveAspectRatio = CCINIClass::INI_RA2MD.ReadBool("DXRender", "PreserveAspectRatio", config.PreserveAspectRatio); + config.WindowedBorder = CCINIClass::INI_RA2MD.ReadBool("DXRender", "WindowedBorder", config.WindowedBorder); + config.StartFullscreen = CCINIClass::INI_RA2MD.ReadBool("DXRender", "StartFullscreen", config.StartFullscreen); + config.PauseGameWhenLoseFocus = CCINIClass::INI_RA2MD.ReadBool("DXRender", "PauseGameWhenLoseFocus", config.PauseGameWhenLoseFocus); + return 0; +} diff --git a/src/Render/Hooks.Surface.cpp b/src/Render/Hooks.Surface.cpp new file mode 100644 index 0000000000..068cba82ac --- /dev/null +++ b/src/Render/Hooks.Surface.cpp @@ -0,0 +1,14 @@ +#include "Surface.h" + +#include +#include + +DEFINE_FUNCTION_JUMP(LJMP, 0x4BA770, DXSurface::CreatePrimary); + +DEFINE_JUMP(LJMP, 0x77747A, 0x777575); // Skip Restore_Check + +static DXSurface* __fastcall _DXSurface_CTOR(DXSurface* surface, void*, int width, int height, bool system_mem, bool enable_3d) { + return new(surface) DXSurface(width, height); +} +DEFINE_FUNCTION_JUMP(LJMP, 0x4BA5A0, _DXSurface_CTOR); + diff --git a/src/Render/Hooks.cpp b/src/Render/Hooks.cpp new file mode 100644 index 0000000000..5697bb3de8 --- /dev/null +++ b/src/Render/Hooks.cpp @@ -0,0 +1,217 @@ +#include "Functions.h" + +#include +#include + +#include +#include +#include +#include + +#ifdef CALL +#undef CALL +#endif + +// Main window creation +DEFINE_FUNCTION_JUMP(LJMP, 0x777C30, RenderDX::CreateMainWindow); + +static BOOL WINAPI _ClientToScreen(HWND hWnd, LPPOINT lpPoint) { + return TRUE; +} + +// Disable ClientToScreen +DEFINE_PATCH_TYPED(void*, 0x7E14B8, _ClientToScreen); + +// But these 3 need to use the real ClientToScreen so that dialogs are where they should be. +static void __fastcall CenterWindowIn(HWND window, HWND parent) { + RECT rcl; + ::GetClientRect(parent, &rcl); + + if (parent == Game::hWnd) { + rcl.right = Drawing::RenderWidth; + rcl.bottom = Drawing::RenderHeight; + } + + ::ClientToScreen(parent, reinterpret_cast(&rcl)); + ::ClientToScreen(parent, reinterpret_cast(&rcl.right)); + rcl.right -= rcl.left; + rcl.bottom -= rcl.top; + + RECT rect; + ::GetClientRect(window, &rect); + ::ClientToScreen(window, reinterpret_cast(&rect)); + ::ClientToScreen(window, reinterpret_cast(&rect.right)); + rect.right -= rect.left; + rect.bottom -= rect.top; + int x = (rcl.right - rect.right + 1) / 2; + int y = (rcl.bottom - rect.bottom + 1) / 2; + + x = std::max(x, 0); + y = std::max(y, 0); + + ::SetWindowPos(window, nullptr, x, y, -1, -1, SWP_NOSIZE | SWP_NOZORDER); +} +DEFINE_FUNCTION_JUMP(LJMP, 0x777080, CenterWindowIn); + +static BOOL __fastcall ODMoveDialog(HWND window, int x, int y) { + int xpos; + int ypos; + + RECT rect1; + rect1.left = 0; + rect1.top = 0; + rect1.right = *reinterpret_cast(0x8A00A4); + rect1.bottom = *reinterpret_cast(0x8A00A8); + + ::ClientToScreen(Game::hWnd, reinterpret_cast(&rect1)); + ::ClientToScreen(Game::hWnd, reinterpret_cast(&rect1.right)); + + RECT rect2; + ::GetWindowRect(window, &rect2); + + rect2.right -= rect2.left; + rect2.bottom -= rect2.top; + + if (x == -1) { + xpos = rect2.left - rect1.left; + } + else { + xpos = x; + } + rect2.left = xpos; + + if (y == -1) { + ypos = rect2.top - rect1.top; + } + else { + ypos = y; + } + rect2.top = ypos; + + return ::MoveWindow(window, rect2.left, rect2.top, rect2.right, rect2.bottom, FALSE); +} +DEFINE_FUNCTION_JUMP(LJMP, 0x623170, ODMoveDialog); + +static BOOL __fastcall WinDialog_GetRectangle(HWND hWnd, LPRECT rect) { + BOOL result = ::GetWindowRect(hWnd, rect); + if (result) { + RECT client; + ::GetClientRect(Game::hWnd, &client); + ::ClientToScreen(Game::hWnd, reinterpret_cast(&client)); + rect->left -= client.left; + rect->right -= client.left; + rect->top -= client.top; + rect->bottom -= client.top; + } + return result; +} +DEFINE_FUNCTION_JUMP(LJMP, 0x775690, WinDialog_GetRectangle); + +// However this WinDialog_GetRectangle is used for drawing offset, so we need to use the original window rect +static BOOL __fastcall _GetWindowRect(HWND hWnd, LPRECT rect) { + return ::GetWindowRect(hWnd, rect); +} +DEFINE_FUNCTION_JUMP(CALL, 0x610E77, _GetWindowRect); + +// All controls inside the window is repositioned by this function, fix it up too +static BOOL __fastcall OD_MoveIngameWindowControls(HWND hWnd) { + if (!SessionClass::Instance.CurrentlyInGame) + return FALSE; + + auto parent = ::GetParent(hWnd); + + RECT rect; + RECT parentRect; + if (!parent || !::GetWindowRect(hWnd, &rect) || !::GetWindowRect(parent, &parentRect)) + return FALSE; + + int x = rect.left - parentRect.left + (parentRect.right - parentRect.left - 800) / 2; + int y = rect.top - parentRect.top + (parentRect.bottom - parentRect.top - 600) / 2; + if (x < 0) + x = 0; + if (y < 0) + y = 0; + + return ::MoveWindow(hWnd, x, y, rect.right - rect.left, rect.bottom - rect.top, FALSE); +} +DEFINE_FUNCTION_JUMP(LJMP, 0x60B7A0, OD_MoveIngameWindowControls); + +DEFINE_JUMP(LJMP, 0x4A4830, 0x4A4848); // Skip Wait_Blit + +DEFINE_JUMP(LJMP, 0x4A4780, 0x4A4825); // Skip Set_DD_Palette + +// Rendering preps. +DEFINE_FUNCTION_JUMP(LJMP, 0x533FD0, RenderDX::AllocateSurfaces); +DEFINE_FUNCTION_JUMP(LJMP, 0x4A42F0, RenderDX::SetVideoMode); +DEFINE_FUNCTION_JUMP(LJMP, 0x4A44F0, RenderDX::ResetVideoMode); +DEFINE_FUNCTION_JUMP(LJMP, 0x560BF0, RenderDX::ChangeDisplayMode); + +// Update the window surface when the game updates its PrimarySurface +DEFINE_HOOK(0x4F4B7E, DXRender_UpdateScreen_GScreenClass_Blit, 0x5) { + RenderDX::UpdateScreen(DSurface::Primary); + return 0; +} + +DEFINE_HOOK(0x5D233A, DXRender_UpdateScreen_MSEngine_Blit_Rects, 0x5) { + const auto eflags = R->EFLAGS(); + // not JLE + const auto zf = eflags & 0x40; + const auto sf = eflags & 0x80; + const auto of = eflags & 0x800; + if (!(zf || sf != of)) { + RenderDX::UpdateScreen(DSurface::Primary); + } + return 0; +} + +DEFINE_HOOK(0x5D1F15, DXRender_UpdateScreen_MSEngine_Frame_Update, 0x5) { + RenderDX::UpdateScreen(DSurface::Primary); + return 0; +} + +DEFINE_HOOK(0x690228, DXRender_UpdateScreen_ScoreClass_Call_Back_Delay, 0x6) { + RenderDX::UpdateScreen(DSurface::Primary); + return 0; +} + +DEFINE_NAKED_HOOK(0x5C0477, DXRender_UpdateScreen_Movie_Blit_To_Screen) { + __asm { + call dword ptr[edx + 8] + mov ecx, dword ptr ds:[0x887308] + call RenderDX::UpdateScreen + pop edi + pop esi + pop ebx + add esp, 0x20 + ret + } +} + +// Windows controls +static LRESULT CALLBACK OwnerDraw_Window_Procedure_(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + auto result = reinterpret_cast(0x610CA0)(hwnd, msg, wparam, lparam); + if (msg == WM_PAINT) { + RenderDX::UpdateScreen(DSurface::Primary); + } + return result; +} +DEFINE_PATCH_TYPED(void*, 0x60FF06, OwnerDraw_Window_Procedure_); + +DEFINE_HOOK_AGAIN(0x611FB0, DXRender_UpdateScreen_OwnerDraw_Window, 0x6); +DEFINE_HOOK(0x61187D, DXRender_UpdateScreen_OwnerDraw_Window, 0xA) { + RenderDX::UpdateScreen(DSurface::Primary); + return 0; +} + +DEFINE_HOOK(0x7776B5, MainWindowProc_WMPAINT, 0x6) { + RenderDX::MainProcHandlePaint(); + return 0x7779B5; +} + +// Call Set_Video_Mode even when windowed. +DEFINE_JUMP(LJMP, 0x6BD9D9, 0x6BDA61); +DEFINE_JUMP(LJMP, 0x6BDB16, 0x6BDB6D); + +// Disable DirectDraw. +DEFINE_JUMP(LJMP, 0x4A3FD0, 0x4A4019); // Skip Prep_Direct_Draw +DEFINE_FUNCTION_JUMP(LJMP, 0x4A4900, RenderDX::EnumDisplayModes); diff --git a/src/Render/Mouse.cpp b/src/Render/Mouse.cpp new file mode 100644 index 0000000000..5e20eacebf --- /dev/null +++ b/src/Render/Mouse.cpp @@ -0,0 +1,332 @@ +#include "Mouse.h" + +#include +#include +#include + +#include "Functions.h" + +#include + +#include + +DXMouse::DXMouse(Surface* surface, HWND hwnd) {} + +DXMouse::~DXMouse() { + Delete_Cursor_Image(); + if (Cursor) { + ::DestroyCursor(Cursor); + Cursor = nullptr; + } +} + +void DXMouse::Set_Cursor(Point2D const& hotspot, SHPStruct const* cursor, int shape) { + if (cursor == nullptr || shape < 0 || shape >= cursor->Frames) { + Delete_Cursor_Image(); + Set_System_Cursor(); + return; + } + + if (MouseShape == cursor && ShapeNumber == shape) { + return; // No change needed + } + + if (cursor != MouseShape) { + Delete_Cursor_Image(); + Convert_Custor_Image(cursor); + } + + MouseShape = cursor; + ShapeNumber = shape; + + const auto& info = CursorInfo[shape]; + + Hotspot = hotspot; + Point2D scaled_hotspot; + scaled_hotspot.X = std::clamp(Hotspot.X * Get_Cursor_Scale(), 0, info.Width - 1); + scaled_hotspot.Y = std::clamp(Hotspot.Y * Get_Cursor_Scale(), 0, info.Height - 1); + + Replace_Cursor(Build_Cursor(info, scaled_hotspot.X, scaled_hotspot.Y)); +} + +bool DXMouse::Is_Hidden() const { + return !IsVisible; +} + +void DXMouse::Hide_Mouse() { + Debug::Log("Hiding mouse cursor\n"); + + if (!IsVisible) + return; + + ::SetCursor(nullptr); + IsVisible = false; +} + +void DXMouse::Show_Mouse() { + Debug::Log("Showing mouse cursor\n"); + + if (IsVisible) + return; + + ::SetCursor(Cursor); + IsVisible = true; +} + +void DXMouse::Release_Mouse() { + if (!IsCaptured) + return; + + ::ClipCursor(nullptr); + IsCaptured = false; +} + +void DXMouse::Capture_Mouse() { + if (IsCaptured) + return; + + RECT client_rect; + ::GetClientRect(Game::hWnd, &client_rect); + ::MapWindowPoints(Game::hWnd, nullptr, reinterpret_cast(&client_rect), 2); + ::ClipCursor(&client_rect); + + IsCaptured = true; +} + +bool DXMouse::Is_Captured() const { + return IsCaptured; +} + +void DXMouse::Conditional_Hide_Mouse(RectangleStruct region) { + Hide_Mouse(); +} + +void DXMouse::Conditional_Show_Mouse() { + Show_Mouse(); +} + +int DXMouse::Get_Mouse_State() const { + return IsVisible ? 0 : -1; +} + +int DXMouse::Get_Mouse_X() const { + return MouseX; +} + +int DXMouse::Get_Mouse_Y() const { + return MouseY; +} + +Point2D DXMouse::Get_Mouse_Point() const { + return Point2D { MouseX, MouseY }; +} + +void DXMouse::Set_Mouse_Point(int x, int y) { + MouseX = x; + MouseY = y; +} + +// Hardware cursor drawing is handled by the OS, so these functions are no-ops. +void DXMouse::Draw_Mouse(Surface* scr, bool issidebarsurface) {} + +void DXMouse::Erase_Mouse(Surface* scr, bool issidebarsurface) {} + +// Coordinate conversion is not needed when using hardware cursor, so this is a no-op. +void DXMouse::Convert_Coordinate(int& x, int& y) const {} + +void DXMouse::Process_Mouse() { + // Update mouse position via GetCursorPos and ScreenToClient + if (!Unsorted::GameInFocus) + return; + + POINT pt; + if (!::GetCursorPos(&pt)) { + return; + } + + if (!::ScreenToClient(Game::hWnd, &pt)) { + return; + } + + if (RenderDX::ShouldScale()) { + MouseX = RenderDX::ClientToRenderX(pt.x); + MouseY = RenderDX::ClientToRenderY(pt.y); + } + else { + MouseX = pt.x; + MouseY = pt.y; + } + +} + +void DXMouse::Recalc_Capture_Region() { + if (Is_Captured()) { + Release_Mouse(); + Capture_Mouse(); + } +} + +void DXMouse::Set_Cached_Cursor() { + if (IsVisible) + ::SetCursor(Cursor); + else + ::SetCursor(nullptr); +} + +void DXMouse::Rebuild_Cursor_Image() { + SHPStruct const* shape = MouseShape; + int number = ShapeNumber; + + Delete_Cursor_Image(); + Set_Cursor(Hotspot, shape, number); +} + +void DXMouse::Delete_Cursor_Image() { + CursorInfo.clear(); + + MouseShape = nullptr; + ShapeNumber = 0; +} + +void DXMouse::Convert_Custor_Image(SHPStruct const* cursor) { + if (!cursor || cursor->Frames <= 0) + return; + + for (int i = 0; i < 256; ++i) { + const auto color = static_cast(FileSystem::MOUSE_PAL->PaletteData)[i]; + auto clr = ColorStruct { color }; + MousePalette[i] = ((i == 0 ? 0 : 255) << 24) | (clr.R << 16) | (clr.G << 8) | clr.B; + } + + CursorInfo.resize(cursor->Frames); + for (int i = 0; i < cursor->Frames; ++i) + Shape_To_Cursor(cursor, i, CursorInfo[i]); +} + +void DXMouse::Shape_To_Cursor(SHPStruct const* cursor, int frame, CursorData& result) { + int width = cursor->Width; + int height = cursor->Height; + + std::vector original_colors; + original_colors.resize(width * height); + + int scaled_width = static_cast(width * Get_Cursor_Scale()); + int scaled_height = static_cast(height * Get_Cursor_Scale()); + + BITMAPV5HEADER bi {}; + bi.bV5Size = sizeof(BITMAPV5HEADER); + bi.bV5Width = scaled_width; + bi.bV5Height = -scaled_height; // Negative height for top-down bitmap + bi.bV5Planes = 1; + bi.bV5BitCount = 32; + bi.bV5Compression = BI_BITFIELDS; + bi.bV5RedMask = 0x00FF0000; + bi.bV5GreenMask = 0x0000FF00; + bi.bV5BlueMask = 0x000000FF; + bi.bV5AlphaMask = 0xFF000000; + + HDC hdc = ::GetDC(nullptr); + void* dst = nullptr; + HBITMAP bitmap = ::CreateDIBSection(hdc, reinterpret_cast(&bi), DIB_RGB_COLORS, &dst, nullptr, 0); + ::ReleaseDC(nullptr, hdc); + + if (!dst || !bitmap) { + return; + } + + const auto* src = static_cast(cursor->GetPixels(frame)); + const auto r = cursor->GetFrameBounds(frame); + + if (cursor->HasCompression(frame)) { + const uint8_t* psrc = src; + for (int y = 0; y < r.Height; ++y) { + uint32_t* dst_row = original_colors.data() + (r.Y + y) * width + r.X; + int len = psrc[0] | (psrc[1] << 8); + int pos = 0; + for (int k = 2; k < len; ++k) { + uint8_t b = psrc[k]; + if (b == 0) { + uint8_t count = psrc[++k]; + for (int i = 0; i < count; ++i) { + dst_row[pos++] = MousePalette[0]; + } + } + else + dst_row[pos++] = MousePalette[b]; + } + psrc += len; + } + } + else { + for (int y = 0; y < r.Height; ++y) { + uint32_t* dst_row = original_colors.data() + (r.Y + y) * width + r.X; + const uint8_t* src_row = src + y * r.Width; + for (int x = 0; x < r.Width; ++x) { + const auto color = MousePalette[src_row[x]]; + dst_row[x] = color; + } + } + } + + Scale_Bitmap_Image(original_colors.data(), width, height, static_cast(dst), scaled_width, scaled_height); + + HBITMAP mask = ::CreateBitmap(scaled_width, scaled_height, 1, 1, nullptr); + + result.Width = scaled_width; + result.Height = scaled_height; + result.Color = bitmap; + result.Mask = mask; +} + +void DXMouse::Scale_Bitmap_Image(const uint32_t* src_ptr, int src_w, int src_h, uint32_t* dst, int dst_w, int dst_h) { + // Using nearest neighbor scaling for simplicity + const uint64_t inc_y = (static_cast(src_h) << 16) / dst_h; + const uint64_t inc_x = (static_cast(src_w) << 16) / dst_w; + + uint64_t pos_y = inc_y / 2; + + for (int y = 0; y < dst_h; ++y) { + const uint64_t src_y = pos_y >> 16; + const uint32_t* src_row = src_ptr + src_y * src_w; + + pos_y += inc_y; + + uint64_t pos_x = inc_x / 2; + + for (int x = 0; x < dst_w; ++x) { + const uint64_t src_x = pos_x >> 16; + pos_x += inc_x; + *dst++ = src_row[src_x]; + } + } +} + + +void DXMouse::Replace_Cursor(HCURSOR cursor) { + auto old_cursor = std::exchange(Cursor, cursor); + ::SetCursor(Cursor); + if (old_cursor) { + ::DestroyCursor(old_cursor); + } +} + +void DXMouse::Set_System_Cursor() { Replace_Cursor(::LoadCursorA(nullptr, IDC_ARROW)); } + +HCURSOR DXMouse::Build_Cursor(const CursorData& data, int hotspot_x, int hotspot_y) { + ICONINFO ii {}; + ii.fIcon = FALSE; + ii.xHotspot = static_cast(hotspot_x); + ii.yHotspot = static_cast(hotspot_y); + ii.hbmColor = data.Color; + ii.hbmMask = data.Mask; + + return static_cast(::CreateIconIndirect(&ii)); +} + +int DXMouse::Get_Cursor_Scale() { + if (!RenderDX::ShouldScale()) { + return 1; + } + + return std::max(1, static_cast(std::round(1.0f / RenderDX::GetYScale()))); +} diff --git a/src/Render/Mouse.h b/src/Render/Mouse.h new file mode 100644 index 0000000000..26a508134f --- /dev/null +++ b/src/Render/Mouse.h @@ -0,0 +1,106 @@ +#pragma once + +#include + +#include + +#include + +struct SHPStruct; +class Surface; + +class Mouse { +public: + virtual ~Mouse() {} + virtual void Set_Cursor(Point2D const& hotspot, SHPStruct const* cursor, int shape) = 0; + virtual bool Is_Hidden() const = 0; + virtual void Hide_Mouse() = 0; + virtual void Show_Mouse() = 0; + virtual void Release_Mouse() = 0; + virtual void Capture_Mouse() = 0; + virtual bool Is_Captured() const = 0; + virtual void Conditional_Hide_Mouse(RectangleStruct region) = 0; + virtual void Conditional_Show_Mouse() = 0; + virtual int Get_Mouse_State() const = 0; + virtual int Get_Mouse_X() const = 0; + virtual int Get_Mouse_Y() const = 0; + virtual Point2D Get_Mouse_Point() const = 0; + virtual void Set_Mouse_Point(int x, int y) = 0; + virtual void Draw_Mouse(Surface* scr, bool issidebarsurface = false) = 0; + virtual void Erase_Mouse(Surface* scr, bool issidebarsurface = false) = 0; + virtual void Convert_Coordinate(int& x, int& y) const = 0; +}; + +class DXMouse : public Mouse { +public: + DEFINE_REFERENCE(DXMouse*, Instance, 0x887640u) + + DXMouse(Surface* surface, HWND hwnd); + + virtual ~DXMouse() override; + virtual void Set_Cursor(Point2D const& hotspot, SHPStruct const* cursor, int shape) override; + virtual bool Is_Hidden() const override; + virtual void Hide_Mouse() override; + virtual void Show_Mouse() override; + virtual void Release_Mouse() override; + virtual void Capture_Mouse() override; + virtual bool Is_Captured() const override; + virtual void Conditional_Hide_Mouse(RectangleStruct region) override; + virtual void Conditional_Show_Mouse() override; + virtual int Get_Mouse_State() const override; + virtual int Get_Mouse_X() const override; + virtual int Get_Mouse_Y() const override; + virtual Point2D Get_Mouse_Point() const override; + virtual void Set_Mouse_Point(int x, int y) override; + virtual void Draw_Mouse(Surface* scr, bool issidebarsurface = false) override; + virtual void Erase_Mouse(Surface* scr, bool issidebarsurface = false) override; + virtual void Convert_Coordinate(int& x, int& y) const override; + + void Process_Mouse(); + void Recalc_Capture_Region(); + void Set_Cached_Cursor(); + + void Rebuild_Cursor_Image(); +private: + SHPStruct const* MouseShape { nullptr }; + int ShapeNumber { 0 }; + + DWORD MousePalette[256] { 0 }; + + struct CursorData { + ~CursorData() { + if (Color) { + ::DeleteObject(Color); + } + if (Mask) { + ::DeleteObject(Mask); + } + } + + int Width { 0 }; + int Height { 0 }; + HBITMAP Color { nullptr }; + HBITMAP Mask { nullptr }; + }; + std::vector CursorInfo; + + Point2D Hotspot { 0,0 }; + HCURSOR Cursor { nullptr }; + + bool IsCaptured { false }; + bool IsVisible { true }; + + int MouseX { 0 }; + int MouseY { 0 }; + + void Delete_Cursor_Image(); + void Convert_Custor_Image(SHPStruct const* cursor); + void Shape_To_Cursor(SHPStruct const* cursor, int frame, CursorData& result); + void Scale_Bitmap_Image(const uint32_t* src_ptr, int src_w, int src_h, uint32_t* dst, int dst_w, int dst_h); + void Replace_Cursor(HCURSOR cursor); + void Set_System_Cursor(); + HCURSOR Build_Cursor(const CursorData& data, int hotspot_x, int hotspot_y); + + static int Get_Cursor_Scale(); + +}; diff --git a/src/Render/Options.cpp b/src/Render/Options.cpp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Render/Options.h b/src/Render/Options.h new file mode 100644 index 0000000000..e46ac1cfbb --- /dev/null +++ b/src/Render/Options.h @@ -0,0 +1,15 @@ +#pragma once + +struct DXRenderOptions +{ + static DXRenderOptions& Config() + { + static DXRenderOptions instance; + return instance; + } + + bool PreserveAspectRatio { true }; + bool WindowedBorder { true }; + bool StartFullscreen { true }; + bool PauseGameWhenLoseFocus { true }; +}; diff --git a/src/Render/Renderer.cpp b/src/Render/Renderer.cpp new file mode 100644 index 0000000000..0508521930 --- /dev/null +++ b/src/Render/Renderer.cpp @@ -0,0 +1,1231 @@ +#include "Renderer.h" + +#include + +#include + +#include + +#include "Functions.h" +#include "Options.h" + +#include + +DXRenderer& DXRenderer::Instance() { + static DXRenderer instance; + return instance; +} + +static LONG_PTR GetConfiguredWindowedStyle(LONG_PTR style, bool visible) { + if (DXRenderOptions::Config().WindowedBorder) + style = (style & ~WS_POPUP) | WS_OVERLAPPEDWINDOW; + else + style = (style & ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU)) | WS_POPUP; + + if (visible) + style |= WS_VISIBLE; + else + style &= ~WS_VISIBLE; + + return style; +} + +static LONG_PTR GetConfiguredWindowedExStyle(LONG_PTR exStyle) { + if (DXRenderOptions::Config().WindowedBorder) + return exStyle; + + return exStyle & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE); +} + +static bool GetMonitorRect(HMONITOR monitor, RECT& monitorRect) { + if (!monitor) + return false; + + MONITORINFO monitorInfo {}; + monitorInfo.cbSize = sizeof(monitorInfo); + if (!::GetMonitorInfoA(monitor, &monitorInfo)) + return false; + + monitorRect = monitorInfo.rcMonitor; + return true; +} + +static bool GetPrimaryMonitorRect(RECT& monitorRect) { + POINT point { 0, 0 }; + return GetMonitorRect(::MonitorFromPoint(point, MONITOR_DEFAULTTOPRIMARY), monitorRect); +} + +static bool GetNearestMonitorRect(HWND hwnd, RECT& monitorRect) { + return GetMonitorRect(::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), monitorRect); +} + +static void CenterRectInMonitor(RECT& rect, const RECT& monitorRect) { + const int width = rect.right - rect.left; + const int height = rect.bottom - rect.top; + + rect.left = monitorRect.left + (monitorRect.right - monitorRect.left - width) / 2; + rect.top = monitorRect.top + (monitorRect.bottom - monitorRect.top - height) / 2; + rect.right = rect.left + width; + rect.bottom = rect.top + height; +} + +bool DXRenderer::CreateMainWindow(HINSTANCE instance, int cmd_show, int width, int height, WNDPROC proc) { + ::InitCommonControls(); + + WNDCLASSA wc {}; + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = proc; + wc.hInstance = instance; + wc.hIcon = ::LoadIconA(instance, MAKEINTRESOURCEA(93)); + wc.hCursor = ::LoadCursorA(nullptr, IDC_ARROW); + wc.lpszClassName = reinterpret_cast(0x849F48); + if (!::RegisterClassA(&wc)) { + Debug::Log("[RenderDX] Failed to register window class\n"); + return false; + } + + LONG_PTR style = GetConfiguredWindowedStyle(WS_OVERLAPPEDWINDOW, false); + LONG_PTR exStyle = GetConfiguredWindowedExStyle(0); + RECT rect = { 0, 0, width, height }; + ::AdjustWindowRectEx(&rect, static_cast(style), FALSE, static_cast(exStyle)); + bool centerWindow = false; + RECT monitorRect {}; + if (GetPrimaryMonitorRect(monitorRect)) { + CenterRectInMonitor(rect, monitorRect); + centerWindow = true; + } + + int window_width = rect.right - rect.left; + int window_height = rect.bottom - rect.top; + int window_x = centerWindow ? rect.left : CW_USEDEFAULT; + int window_y = centerWindow ? rect.top : CW_USEDEFAULT; + + Game::hWnd = ::CreateWindowExA(static_cast(exStyle), wc.lpszClassName, wc.lpszClassName, static_cast(style), window_x, window_y, window_width, window_height, nullptr, nullptr, instance, nullptr); + + if (!Game::hWnd) { + Debug::Log("[RenderDX] Failed to create main window\n"); + return false; + } + + // Disable clipping because we draw Win32 child windows as part of the main window + style = GetWindowLongPtrA(Game::hWnd, GWL_STYLE); + style &= ~(WS_CLIPSIBLINGS | WS_CLIPCHILDREN); + SetWindowLongPtrA(Game::hWnd, GWL_STYLE, style); + + Hwnd = Game::hWnd; + WindowWidth = window_width; + WindowHeight = window_height; + + if (DXRenderOptions::Config().StartFullscreen) + ToggleFullscreen(); + + ::ShowWindow(Game::hWnd, cmd_show); + ::UpdateWindow(Game::hWnd); + + Game::hIMC = ::ImmGetContext(Game::hWnd); + ::ImmAssociateContext(Game::hWnd, nullptr); + + ::RegisterHotKey(Game::hWnd, 1, MOD_ALT | MOD_CONTROL | MOD_SHIFT, 'M'); + + // Gain focus for the game window to ensure it receives input + ::SetForegroundWindow(Game::hWnd); + Unsorted::GameInFocus = true; + + if (!DXRenderer::Instance().LoadImports()) { + Debug::Log("[RenderDX] Failed to load required libraries\n"); + return false; + } + + return true; +} + +void DXRenderer::DestroyMainWindow() { + if (!Hwnd) + return; + + ::DestroyWindow(Hwnd); + Hwnd = nullptr; + + DXRenderer::Instance().UnloadImports(); +} + +bool DXRenderer::IsRendererReady() { + return true; +} + +bool DXRenderer::CreateRenderer(int width, int height, int bits_per_pixel) { + if (bits_per_pixel != 16) { + Debug::Log("[RenderDX] Unsupported bits per pixel: %d\n", bits_per_pixel); + return false; + } + + RenderWidth = width; + RenderHeight = height; + if (WindowWidth <= 0) + WindowWidth = width; + if (WindowHeight <= 0) + WindowHeight = height; + + UpdateViewportAndScissor(); + + if (!CreateDevice()) { + Debug::Log("[RenderDX] Failed to create D3D12 device\n"); + return false; + } + + if (!CreateCommandQueue()) { + Debug::Log("[RenderDX] Failed to create command queue\n"); + return false; + } + + if (!CreateSwapChain()) { + Debug::Log("[RenderDX] Failed to create swap chain\n"); + return false; + } + + if (!CreateRtvHeap()) { + Debug::Log("[RenderDX] Failed to create RTV descriptor heap\n"); + return false; + } + + if (!CreateRenderTargetViews()) { + Debug::Log("[RenderDX] Failed to create render target views\n"); + return false; + } + + if (!CreateSrvHeap()) { + Debug::Log("[RenderDX] Failed to create SRV descriptor heap\n"); + return false; + } + + if (!CreateSurfacePipeline()) { + Debug::Log("[RenderDX] Failed to create surface pipeline\n"); + return false; + } + + if (!CreateCommandObjects()) { + Debug::Log("[RenderDX] Failed to create command objects\n"); + return false; + } + + if (!CreateFenceObjects()) { + Debug::Log("[RenderDX] Failed to create fence objects\n"); + return false; + } + + if (!CreateFixedSurfaceGpuResources()) { + Debug::Log("[RenderDX] Failed to create fixed surface GPU resources\n"); + return false; + } + + return true; +} + +void DXRenderer::DestroyRenderer() { + if (SurfaceTexture) { + SurfaceTexture.Reset(); + } + + for (UINT i = 0; i < kFrameCount; ++i) { + if (SurfaceUploadBuffers[i] && SurfaceUploadMapped[i]) { + SurfaceUploadBuffers[i]->Unmap(0, nullptr); + SurfaceUploadMapped[i] = nullptr; + } + } + + for (UINT i = 0; i < kFrameCount; ++i) { + if (SurfaceUploadBuffers[i]) { + SurfaceUploadBuffers[i].Reset(); + } + } + + if (Fence) { + Fence.Reset(); + } + if (FenceEvent) { + ::CloseHandle(FenceEvent); + FenceEvent = nullptr; + } + + if (CommandList) { + CommandList.Reset(); + } + for (UINT i = 0; i < kFrameCount; ++i) { + if (CommandAllocators[i]) { + CommandAllocators[i].Reset(); + } + } + + if (PipelineState) { + PipelineState.Reset(); + } + if (RootSignature) { + RootSignature.Reset(); + } + + if (SrvHeap) { + SrvHeap.Reset(); + } + + for (UINT i = 0; i < kFrameCount; ++i) { + if (RenderTargets[i]) + RenderTargets[i].Reset(); + } + + if (RtvHeap) { + RtvHeap.Reset(); + } + RtvDescriptorSize = 0; + + if (SwapChain) { + BOOL fullscreenState = FALSE; + Microsoft::WRL::ComPtr pTarget; + if (SUCCEEDED(SwapChain->GetFullscreenState(&fullscreenState, &pTarget)) && fullscreenState) + SwapChain->SetFullscreenState(FALSE, nullptr); + + SwapChain.Reset(); + } + FrameIndex = 0; + + if (CommandQueue) { + CommandQueue.Reset(); + } + + if (Device) { + Device.Reset(); + } + if (Factory) { + Factory.Reset(); + } +} + +bool DXRenderer::ResizeWindow(int width, int height) { + WindowWidth = width; + WindowHeight = height; + + UpdateViewportAndScissor(); + RenderDX::UpdateScale(); + + if (!Device || !SwapChain) { + return true; // No swap chain to resize, not an error. + } + + if (!WaitForGpu()) { + Debug::Log("[RenderDX] Failed to wait for GPU before resizing swap chain.\n"); + return false; + } + + for (auto& target : RenderTargets) { + target.Reset(); + } + + if (FAILED(SwapChain->ResizeBuffers(kFrameCount, WindowWidth, WindowHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0))) { + Debug::Log("[RenderDX] Failed to resize swap chain buffers.\n"); + return false; + } + + FrameIndex = SwapChain->GetCurrentBackBufferIndex(); + if (!CreateRenderTargetViews()) + return false; + + const UINT64 newFenceValue = Fence->GetCompletedValue() + 1; + FenceValues.fill(newFenceValue); + + Debug::Log("[RenderDX] Swap chain resized successfully to %ux%u.\n", WindowWidth, WindowHeight); + + return true; +} + +void DXRenderer::ToggleFullscreen() { + Debug::Log("[RenderDX] Toggling fullscreen mode.\n"); + + if (!Hwnd) + return; + + if (!Windowed) { + if (!HasWindowedState) { + Debug::Log("[RenderDX] Cannot restore windowed mode, no saved window state.\n"); + return; + } + + Windowed = true; + + ::SetWindowLongPtrA(Hwnd, GWL_STYLE, GetConfiguredWindowedStyle(WindowedStyle, true)); + ::SetWindowLongPtrA(Hwnd, GWL_EXSTYLE, GetConfiguredWindowedExStyle(WindowedExStyle)); + + const int width = WindowedRect.right - WindowedRect.left; + const int height = WindowedRect.bottom - WindowedRect.top; + int windowX = WindowedRect.left; + int windowY = WindowedRect.top; + + if (!DXRenderOptions::Config().WindowedBorder) { + RECT monitorRect {}; + if (GetNearestMonitorRect(Hwnd, monitorRect)) { + RECT centeredRect = { 0, 0, width, height }; + CenterRectInMonitor(centeredRect, monitorRect); + WindowedRect = centeredRect; + windowX = centeredRect.left; + windowY = centeredRect.top; + } + } + + ::SetWindowPos(Hwnd, nullptr, windowX, windowY, width, height, SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW); + + Debug::Log("[RenderDX] Borderless fullscreen disabled.\n"); + return; + } + + if (!::GetWindowRect(Hwnd, &WindowedRect)) { + Debug::Log("[RenderDX] Failed to save window rectangle before entering borderless fullscreen.\n"); + return; + } + + WindowedStyle = GetConfiguredWindowedStyle(::GetWindowLongPtrA(Hwnd, GWL_STYLE), true); + WindowedExStyle = GetConfiguredWindowedExStyle(::GetWindowLongPtrA(Hwnd, GWL_EXSTYLE)); + HasWindowedState = true; + + RECT monitorRect {}; + if (!GetNearestMonitorRect(Hwnd, monitorRect)) { + Debug::Log("[RenderDX] Failed to get monitor rectangle for borderless fullscreen.\n"); + return; + } + + const LONG_PTR borderlessStyle = (WindowedStyle & ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU)) | WS_POPUP | WS_VISIBLE; + const LONG_PTR borderlessExStyle = WindowedExStyle & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE); + + Windowed = false; + + ::SetWindowLongPtrA(Hwnd, GWL_STYLE, borderlessStyle); + ::SetWindowLongPtrA(Hwnd, GWL_EXSTYLE, borderlessExStyle); + + const int width = monitorRect.right - monitorRect.left; + const int height = monitorRect.bottom - monitorRect.top; + ::SetWindowPos(Hwnd, HWND_TOP, monitorRect.left, monitorRect.top, width, height, SWP_NOOWNERZORDER | SWP_FRAMECHANGED); + + Debug::Log("[RenderDX] Borderless fullscreen enabled.\n"); +} + +bool DXRenderer::UploadSurfaceToTexture(void* surface_data, int source_pitch) { + const int sourceRowBytes = RenderWidth * static_cast(sizeof(std::uint16_t)); + if (source_pitch < sourceRowBytes) { + Debug::Log("[RenderDX] Source pitch %d is smaller than required row bytes %d.\n", source_pitch, sourceRowBytes); + return false; + } + + if (!PopulateCommandListForCPUSurface(surface_data, source_pitch)) + return false; + + ID3D12CommandList* list[] = { CommandList.Get() }; + CommandQueue->ExecuteCommandLists(1, list); + return true; +} + +void DXRenderer::SetRenderScale(bool scale) { + if (ScaleRender == scale) + return; + + ScaleRender = scale; + UpdateViewportAndScissor(); + RenderDX::UpdateScale(); +} + +bool DXRenderer::Present() { + if (FAILED(SwapChain->Present(0, 0))) { + Debug::Log("[RenderDX] Failed to present swap chain.\n"); + return false; + } + + return MoveToNextFrame(); +} + +void DXRenderer::MoveWindow(int x, int y, int width, int height) { + if (Windowed && !DXRenderOptions::Config().WindowedBorder) { + RECT monitorRect {}; + if (GetNearestMonitorRect(Hwnd, monitorRect)) { + RECT centeredRect = { 0, 0, width, height }; + CenterRectInMonitor(centeredRect, monitorRect); + x = centeredRect.left; + y = centeredRect.top; + } + } + + ::MoveWindow(Hwnd, x, y, width, height, TRUE); + WindowWidth = width; + WindowHeight = height; + UpdateViewportAndScissor(); + RenderDX::UpdateScale(); +} + +bool DXRenderer::IsWindowed() const { + return Windowed; +} + +int DXRenderer::GetWindowWidth() const { + return WindowWidth; +} + +int DXRenderer::GetWindowHeight() const { + return WindowHeight; +} + +float DXRenderer::GetViewportX() const { + return RenderViewportX; +} + +float DXRenderer::GetViewportY() const { + return RenderViewportY; +} + +float DXRenderer::GetViewportWidth() const { + return RenderViewportWidth; +} + +float DXRenderer::GetViewportHeight() const { + return RenderViewportHeight; +} + +DXRenderer::DXRenderer() {} + +DXRenderer::~DXRenderer() {} + +bool DXRenderer::LoadImports() { + D3D12Lib = ::LoadLibraryW(L"d3d12.dll"); + if (!D3D12Lib) { + Debug::Log("[RenderDX] Failed to load d3d12.dll.\n"); + return false; + } + +#if DXRENDER_DEBUG + FP_D3D12GetDebugInterface = reinterpret_cast(::GetProcAddress(D3D12Lib, "D3D12GetDebugInterface")); + if (!FP_D3D12GetDebugInterface) { + Debug::Log("[RenderDX] Failed to get address of D3D12GetDebugInterface.\n"); + return false; + } +#endif + FP_D3D12CreateDevice = reinterpret_cast(::GetProcAddress(D3D12Lib, "D3D12CreateDevice")); + if (!FP_D3D12CreateDevice) { + Debug::Log("[RenderDX] Failed to get address of D3D12CreateDevice.\n"); + return false; + } + FP_D3D12SerializeRootSignature = reinterpret_cast(::GetProcAddress(D3D12Lib, "D3D12SerializeRootSignature")); + if (!FP_D3D12SerializeRootSignature) { + Debug::Log("[RenderDX] Failed to get address of D3D12SerializeRootSignature.\n"); + return false; + } + + DXGILib = ::LoadLibraryW(L"dxgi.dll"); + if (!DXGILib) { + Debug::Log("[RenderDX] Failed to load dxgi.dll.\n"); + return false; + } + FP_CreateDXGIFactory2 = reinterpret_cast(::GetProcAddress(DXGILib, "CreateDXGIFactory2")); + if (!FP_CreateDXGIFactory2) { + Debug::Log("[RenderDX] Failed to get address of CreateDXGIFactory2.\n"); + return false; + } + + D3DCompilerLib = ::LoadLibraryW(L"d3dcompiler_47.dll"); + if (!D3DCompilerLib) { + Debug::Log("[RenderDX] Failed to load d3dcompiler_47.dll.\n"); + return false; + } + FP_D3DCompile = reinterpret_cast(::GetProcAddress(D3DCompilerLib, "D3DCompile")); + if (!FP_D3DCompile) { + Debug::Log("[RenderDX] Failed to get address of D3DCompile.\n"); + return false; + } + + return true; +} + +void DXRenderer::UnloadImports() { + if (D3DCompilerLib) { + ::FreeLibrary(D3DCompilerLib); + FP_D3DCompile = nullptr; + } + if (DXGILib) { + ::FreeLibrary(DXGILib); + FP_CreateDXGIFactory2 = nullptr; + } + if (D3D12Lib) { + ::FreeLibrary(D3D12Lib); +#if DXRENDER_DEBUG + FP_D3D12GetDebugInterface = nullptr; +#endif + FP_D3D12CreateDevice = nullptr; + FP_D3D12SerializeRootSignature = nullptr; + } +} + +bool DXRenderer::CreateDevice() { + UINT dxgiFactoryFlags = 0; +#if DXRENDER_DEBUG + Microsoft::WRL::ComPtr debugController; + if (SUCCEEDED(FP_D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)))) { + debugController->EnableDebugLayer(); + } + dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG; +#endif + if (FAILED(FP_CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&Factory)))) { + return false; + } + constexpr bool kUseWarpDevice = false; + if (kUseWarpDevice) { + Microsoft::WRL::ComPtr warpAdapter; + if (FAILED(Factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)))) { + Debug::Log("[RenderDX] Failed to create WARP adapter.\n"); + return false; + } + if (FAILED(FP_D3D12CreateDevice(warpAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) { + Debug::Log("[RenderDX] Failed to create WARP adapter.\n"); + return false; + } + + Debug::Log("[RenderDX] D3D12 WARP device created successfully.\n"); + } + else { + Microsoft::WRL::ComPtr hardwareAdapter; + for (UINT adapterIndex = 0; Factory->EnumAdapterByGpuPreference(adapterIndex, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&hardwareAdapter)) != DXGI_ERROR_NOT_FOUND; ++adapterIndex) { + DXGI_ADAPTER_DESC1 desc; + hardwareAdapter->GetDesc1(&desc); + if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) { + continue; + } + if (SUCCEEDED(FP_D3D12CreateDevice(hardwareAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) { + Debug::Log("[RenderDX] D3D12 device created successfully on adapter: %ls\n", desc.Description); + break; + } + } + if (!Device) { + return false; + } + + Debug::Log("[RenderDX] D3D12 device created successfully.\n"); + } + + return true; +} + +bool DXRenderer::CreateCommandQueue() { + D3D12_COMMAND_QUEUE_DESC queueDesc {}; + queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + queueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; + queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; + queueDesc.NodeMask = 0; + + if (FAILED(Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&CommandQueue)))) { + Debug::Log("[RenderDX] Failed to create command queue.\n"); + return false; + } + + Debug::Log("[RenderDX] Command queue created successfully.\n"); + return true; +} + +bool DXRenderer::CreateSwapChain() { + DXGI_SWAP_CHAIN_DESC1 desc {}; + desc.Width = WindowWidth; + desc.Height = WindowHeight; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.Stereo = FALSE; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + desc.BufferCount = kFrameCount; + desc.Scaling = DXGI_SCALING_STRETCH; + desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + desc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; + desc.Flags = 0; + + Microsoft::WRL::ComPtr swapChain1; + if (FAILED(Factory->CreateSwapChainForHwnd(CommandQueue.Get(), Hwnd, &desc, nullptr, nullptr, &swapChain1))) { + Debug::Log("[RenderDX] Failed to create swap chain.\n"); + return false; + } + + if (FAILED(Factory->MakeWindowAssociation(Hwnd, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_PRINT_SCREEN))) { + Debug::Log("[RenderDX] Failed to set window association.\n"); + return false; + } + + if (FAILED(swapChain1->QueryInterface(IID_PPV_ARGS(&SwapChain)))) { + Debug::Log("[RenderDX] Failed to query IDXGISwapChain3 interface.\n"); + return false; + } + + Debug::Log("[RenderDX] Swap chain created successfully.\n"); + FrameIndex = SwapChain->GetCurrentBackBufferIndex(); + return true; +} + +bool DXRenderer::CreateRtvHeap() { + D3D12_DESCRIPTOR_HEAP_DESC desc {}; + desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; + desc.NumDescriptors = kFrameCount; + desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + desc.NodeMask = 0; + + if (FAILED(Device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&RtvHeap)))) { + Debug::Log("[RenderDX] Failed to create RTV descriptor heap.\n"); + return false; + } + + RtvDescriptorSize = Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); + Debug::Log("[RenderDX] RTV descriptor heap created successfully.\n"); + return true; +} + +bool DXRenderer::CreateRenderTargetViews() { + D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = RtvHeap->GetCPUDescriptorHandleForHeapStart(); + for (UINT i = 0; i < kFrameCount; ++i) { + if (FAILED(SwapChain->GetBuffer(i, IID_PPV_ARGS(&RenderTargets[i])))) { + Debug::Log("[RenderDX] Failed to get back buffer %u.\n", i); + return false; + } + Device->CreateRenderTargetView(RenderTargets[i].Get(), nullptr, rtvHandle); + rtvHandle.ptr += RtvDescriptorSize; + } + + Debug::Log("[RenderDX] Render target views created successfully.\n"); + return true; +} + +bool DXRenderer::CreateSrvHeap() { + D3D12_DESCRIPTOR_HEAP_DESC desc {}; + desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + desc.NumDescriptors = 1; // For surface texture SRV + desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + desc.NodeMask = 0; + + if (FAILED(Device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&SrvHeap)))) { + Debug::Log("[RenderDX] Failed to create SRV descriptor heap.\n"); + return false; + } + + Debug::Log("[RenderDX] SRV descriptor heap created successfully.\n"); + return true; +} + +bool DXRenderer::CreateSurfacePipeline() { + D3D12_DESCRIPTOR_RANGE srvRange {}; + srvRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + srvRange.NumDescriptors = 1; + srvRange.BaseShaderRegister = 0; + srvRange.RegisterSpace = 0; + srvRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND; + + D3D12_ROOT_PARAMETER rootParam {}; + rootParam.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + rootParam.DescriptorTable.NumDescriptorRanges = 1; + rootParam.DescriptorTable.pDescriptorRanges = &srvRange; + rootParam.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + + D3D12_STATIC_SAMPLER_DESC sampler {}; + sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; + sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.MipLODBias = 0.0f; + sampler.MaxAnisotropy = 1; + sampler.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; + sampler.BorderColor = D3D12_STATIC_BORDER_COLOR_OPAQUE_BLACK; + sampler.MinLOD = 0.0f; + sampler.MaxLOD = D3D12_FLOAT32_MAX; + sampler.ShaderRegister = 0; + sampler.RegisterSpace = 0; + sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + + D3D12_ROOT_SIGNATURE_DESC rootSigDesc {}; + rootSigDesc.NumParameters = 1; + rootSigDesc.pParameters = &rootParam; + rootSigDesc.NumStaticSamplers = 1; + rootSigDesc.pStaticSamplers = &sampler; + rootSigDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; + + Microsoft::WRL::ComPtr rootSigBlob; + Microsoft::WRL::ComPtr errorBlob; + + HRESULT hr = FP_D3D12SerializeRootSignature( + &rootSigDesc, + D3D_ROOT_SIGNATURE_VERSION_1, + &rootSigBlob, + &errorBlob + ); + + if (FAILED(hr)) { + if (errorBlob) + Debug::Log("[RenderDX] Root signature serialization error: %s\n", static_cast(errorBlob->GetBufferPointer())); + else + Debug::Log("[RenderDX] Unknown root signature serialization error.\n"); + return false; + } + + if (FAILED(Device->CreateRootSignature(0, rootSigBlob->GetBufferPointer(), rootSigBlob->GetBufferSize(), IID_PPV_ARGS(&RootSignature)))) { + Debug::Log("[RenderDX] Failed to create root signature.\n"); + return false; + } + + static constexpr const char* shaderSource = R"( +Texture2D gSurface : register(t0); +SamplerState gSampler : register(s0); + +struct VSOut +{ + float4 position : SV_Position; + float2 uv : TEXCOORD0; +}; + +VSOut VSMain(uint vertexId : SV_VertexID) +{ + VSOut o; + + // Full-screen triangle vertices and UVs + float2 positions[3] = + { + float2(-1.0f, -1.0f), + float2(-1.0f, 3.0f), + float2( 3.0f, -1.0f) + }; + + float2 uvs[3] = + { + float2(0.0f, 1.0f), + float2(0.0f, -1.0f), + float2(2.0f, 1.0f) + }; + + o.position = float4(positions[vertexId], 0.0f, 1.0f); + o.uv = uvs[vertexId]; + + return o; +} + +float4 PSMain(VSOut input) : SV_Target0 +{ + return gSurface.Sample(gSampler, input.uv); +} +)"; + + auto vertexShader = CompileShader(shaderSource, "VSMain", "vs_5_0"); + auto pixelShader = CompileShader(shaderSource, "PSMain", "ps_5_0"); + + D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc {}; + psoDesc.pRootSignature = RootSignature.Get(); + psoDesc.VS.pShaderBytecode = vertexShader->GetBufferPointer(); + psoDesc.VS.BytecodeLength = vertexShader->GetBufferSize(); + psoDesc.PS.pShaderBytecode = pixelShader->GetBufferPointer(); + psoDesc.PS.BytecodeLength = pixelShader->GetBufferSize(); + psoDesc.BlendState.AlphaToCoverageEnable = FALSE; + psoDesc.BlendState.IndependentBlendEnable = FALSE; + D3D12_RENDER_TARGET_BLEND_DESC rtBlend {}; + rtBlend.BlendEnable = FALSE; + rtBlend.LogicOpEnable = FALSE; + rtBlend.SrcBlend = D3D12_BLEND_ONE; + rtBlend.DestBlend = D3D12_BLEND_ZERO; + rtBlend.BlendOp = D3D12_BLEND_OP_ADD; + rtBlend.SrcBlendAlpha = D3D12_BLEND_ONE; + rtBlend.DestBlendAlpha = D3D12_BLEND_ZERO; + rtBlend.BlendOpAlpha = D3D12_BLEND_OP_ADD; + rtBlend.LogicOp = D3D12_LOGIC_OP_NOOP; + rtBlend.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + psoDesc.BlendState.RenderTarget[0] = rtBlend; + psoDesc.SampleMask = UINT_MAX; + psoDesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + psoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + psoDesc.RasterizerState.FrontCounterClockwise = FALSE; + psoDesc.RasterizerState.DepthBias = D3D12_DEFAULT_DEPTH_BIAS; + psoDesc.RasterizerState.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP; + psoDesc.RasterizerState.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS; + psoDesc.RasterizerState.DepthClipEnable = TRUE; + psoDesc.RasterizerState.MultisampleEnable = FALSE; + psoDesc.RasterizerState.AntialiasedLineEnable = FALSE; + psoDesc.RasterizerState.ForcedSampleCount = 0; + psoDesc.RasterizerState.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF; + psoDesc.DepthStencilState.DepthEnable = FALSE; + psoDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO; + psoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_ALWAYS; + psoDesc.DepthStencilState.StencilEnable = FALSE; + psoDesc.DepthStencilState.StencilReadMask = D3D12_DEFAULT_STENCIL_READ_MASK; + psoDesc.DepthStencilState.StencilWriteMask = D3D12_DEFAULT_STENCIL_WRITE_MASK; + psoDesc.DepthStencilState.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP; + psoDesc.DepthStencilState.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP; + psoDesc.DepthStencilState.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP; + psoDesc.DepthStencilState.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS; + psoDesc.DepthStencilState.BackFace = psoDesc.DepthStencilState.FrontFace; + psoDesc.InputLayout = {}; + psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + psoDesc.NumRenderTargets = 1; + psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; + psoDesc.DSVFormat = DXGI_FORMAT_UNKNOWN; + psoDesc.SampleDesc.Count = 1; + psoDesc.SampleDesc.Quality = 0; + + if (FAILED(Device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&PipelineState)))) { + Debug::Log("[RenderDX] Failed to create pipeline state.\n"); + return false; + } + + Debug::Log("[RenderDX] Pipeline state created successfully.\n"); + return true; +} + +bool DXRenderer::CreateCommandObjects() { + for (UINT i = 0; i < kFrameCount; ++i) { + if (FAILED(Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&CommandAllocators[i])))) { + Debug::Log("[RenderDX] Failed to create command allocator %u.\n", i); + return false; + } + } + + if (FAILED(Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, CommandAllocators[FrameIndex].Get(), PipelineState.Get(), IID_PPV_ARGS(&CommandList)))) { + Debug::Log("[RenderDX] Failed to create command list.\n"); + return false; + } + + Debug::Log("[RenderDX] Command objects created successfully.\n"); + CommandList->Close(); // Command list needs to be closed before reset in the render loop. + return true; +} + +bool DXRenderer::CreateFenceObjects() { + FenceValues.fill(0); + + if (FAILED(Device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&Fence)))) { + Debug::Log("[RenderDX] Failed to create fence.\n"); + return false; + } + + FenceValues[FrameIndex] = 1; + FenceEvent = ::CreateEventW(nullptr, FALSE, FALSE, nullptr); + + if (!FenceEvent) { + Debug::Log("[RenderDX] Failed to create fence event.\n"); + return false; + } + + Debug::Log("[RenderDX] Fence objects created successfully.\n"); + return true; +} + +bool DXRenderer::CreateFixedSurfaceGpuResources() { + const UINT sourceRowBytes = RenderWidth * sizeof(std::uint16_t); + SurfaceUploadRowPitch = (sourceRowBytes + D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1) & ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1); // Align up + SurfaceUploadBufferSize = static_cast(SurfaceUploadRowPitch) * static_cast(RenderHeight); + + D3D12_HEAP_PROPERTIES defaultHeap {}; + defaultHeap.Type = D3D12_HEAP_TYPE_DEFAULT; + defaultHeap.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; + defaultHeap.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; + defaultHeap.CreationNodeMask = 1; + defaultHeap.VisibleNodeMask = 1; + + D3D12_RESOURCE_DESC textureDesc {}; + textureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + textureDesc.Alignment = 0; + textureDesc.Width = RenderWidth; + textureDesc.Height = RenderHeight; + textureDesc.DepthOrArraySize = 1; + textureDesc.MipLevels = 1; + textureDesc.Format = DXGI_FORMAT_B5G6R5_UNORM; + textureDesc.SampleDesc.Count = 1; + textureDesc.SampleDesc.Quality = 0; + textureDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + textureDesc.Flags = D3D12_RESOURCE_FLAG_NONE; + + SurfaceTextureState = D3D12_RESOURCE_STATE_COPY_DEST; + + if (FAILED(Device->CreateCommittedResource( + &defaultHeap, + D3D12_HEAP_FLAG_NONE, + &textureDesc, + SurfaceTextureState, + nullptr, + IID_PPV_ARGS(&SurfaceTexture) + ))) { + Debug::Log("[RenderDX] Failed to create surface texture resource.\n"); + return false; + } + + D3D12_HEAP_PROPERTIES uploadHeap {}; + uploadHeap.Type = D3D12_HEAP_TYPE_UPLOAD; + uploadHeap.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; + uploadHeap.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; + uploadHeap.CreationNodeMask = 1; + uploadHeap.VisibleNodeMask = 1; + + D3D12_RESOURCE_DESC uploadDesc {}; + uploadDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + uploadDesc.Alignment = 0; + uploadDesc.Width = SurfaceUploadBufferSize; + uploadDesc.Height = 1; + uploadDesc.DepthOrArraySize = 1; + uploadDesc.MipLevels = 1; + uploadDesc.Format = DXGI_FORMAT_UNKNOWN; + uploadDesc.SampleDesc.Count = 1; + uploadDesc.SampleDesc.Quality = 0; + uploadDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + uploadDesc.Flags = D3D12_RESOURCE_FLAG_NONE; + + for (UINT i = 0; i < kFrameCount; ++i) { + if (FAILED(Device->CreateCommittedResource( + &uploadHeap, + D3D12_HEAP_FLAG_NONE, + &uploadDesc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(&SurfaceUploadBuffers[i]) + ))) { + Debug::Log("[RenderDX] Failed to create surface upload buffer %u.\n", i); + return false; + } + + D3D12_RANGE readRange {}; + readRange.Begin = 0; + readRange.End = 0; + + if (FAILED(SurfaceUploadBuffers[i]->Map(0, &readRange, reinterpret_cast(&SurfaceUploadMapped[i])))) { + Debug::Log("[RenderDX] Failed to map surface upload buffer %u.\n", i); + return false; + } + } + + D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc {}; + srvDesc.Format = DXGI_FORMAT_B5G6R5_UNORM; + srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srvDesc.Texture2D.MostDetailedMip = 0; + srvDesc.Texture2D.MipLevels = 1; + srvDesc.Texture2D.PlaneSlice = 0; + srvDesc.Texture2D.ResourceMinLODClamp = 0.0f; + + Device->CreateShaderResourceView( + SurfaceTexture.Get(), + &srvDesc, + SrvHeap->GetCPUDescriptorHandleForHeapStart() + ); + + return true; +} + +void DXRenderer::UpdateViewportAndScissor() { + if (ScaleRender) { + RenderViewportX = 0.0f; + RenderViewportY = 0.0f; + RenderViewportWidth = static_cast(WindowWidth); + RenderViewportHeight = static_cast(WindowHeight); + + if (DXRenderOptions::Config().PreserveAspectRatio && RenderWidth > 0 && RenderHeight > 0 && WindowWidth > 0 && WindowHeight > 0) { + const float scale = std::min( + static_cast(WindowWidth) / static_cast(RenderWidth), + static_cast(WindowHeight) / static_cast(RenderHeight) + ); + + RenderViewportWidth = static_cast(RenderWidth) * scale; + RenderViewportHeight = static_cast(RenderHeight) * scale; + RenderViewportX = (static_cast(WindowWidth) - RenderViewportWidth) * 0.5f; + RenderViewportY = (static_cast(WindowHeight) - RenderViewportHeight) * 0.5f; + } + + Viewport.TopLeftX = RenderViewportX; + Viewport.TopLeftY = RenderViewportY; + Viewport.Width = RenderViewportWidth; + Viewport.Height = RenderViewportHeight; + Viewport.MinDepth = 0.0f; + Viewport.MaxDepth = 1.0f; + + ScissorRect.left = static_cast(RenderViewportX); + ScissorRect.top = static_cast(RenderViewportY); + ScissorRect.right = static_cast(RenderViewportX + RenderViewportWidth); + ScissorRect.bottom = static_cast(RenderViewportY + RenderViewportHeight); + } + else { + // Just render the surface at its native resolution in the top-left corner of the viewport. + RenderViewportX = 0.0f; + RenderViewportY = 0.0f; + RenderViewportWidth = static_cast(RenderWidth); + RenderViewportHeight = static_cast(RenderHeight); + + Viewport.TopLeftX = RenderViewportX; + Viewport.TopLeftY = RenderViewportY; + Viewport.Width = RenderViewportWidth; + Viewport.Height = RenderViewportHeight; + Viewport.MinDepth = 0.0f; + Viewport.MaxDepth = 1.0f; + + ScissorRect.left = 0; + ScissorRect.top = 0; + ScissorRect.right = static_cast(RenderWidth); + ScissorRect.bottom = static_cast(RenderHeight); + } +} + +bool DXRenderer::WaitForGpu() { + if (!CommandQueue || !Fence || !FenceEvent) { + return true; // If we don't have the necessary objects, we can't wait, but also can't report an error. + } + + const UINT64 currentFenceValue = FenceValues[FrameIndex]; + if (FAILED(CommandQueue->Signal(Fence.Get(), currentFenceValue))) { + Debug::Log("[RenderDX] Failed to signal command queue for GPU synchronization.\n"); + return false; + } + + if (FAILED(Fence->SetEventOnCompletion(currentFenceValue, FenceEvent))) { + Debug::Log("[RenderDX] Failed to set event on fence completion for GPU synchronization.\n"); + return false; + } + + DWORD waitResult = ::WaitForSingleObjectEx(FenceEvent, INFINITE, FALSE); + if (waitResult != WAIT_OBJECT_0) { + Debug::Log("[RenderDX] Wait for GPU synchronization event failed with error code: %lu\n", GetLastError()); + return false; + } + + ++FenceValues[FrameIndex]; + return true; +} + +Microsoft::WRL::ComPtr DXRenderer::CompileShader(std::string_view source, std::string_view entryPoint, std::string_view target) { + Microsoft::WRL::ComPtr shaderBlob; + Microsoft::WRL::ComPtr errorBlob; + + UINT compileFlags = 0; + HRESULT hr = FP_D3DCompile(source.data(), source.length(), nullptr, nullptr, nullptr, entryPoint.data(), target.data(), compileFlags, 0, &shaderBlob, &errorBlob); + + if (FAILED(hr)) { + if (errorBlob) + Debug::Log("[RenderDX] Shader compilation error: %s\n", static_cast(errorBlob->GetBufferPointer())); + else + Debug::Log("[RenderDX] Unknown shader compilation error.\n"); + } + + return shaderBlob; +} + +bool DXRenderer::PopulateCommandListForCPUSurface(const void* pixels, int source_pitch) { + if (FAILED(CommandAllocators[FrameIndex]->Reset())) { + Debug::Log("[RenderDX] Failed to reset command allocator for populating command list.\n"); + return false; + } + + if (FAILED(CommandList->Reset(CommandAllocators[FrameIndex].Get(), PipelineState.Get()))) { + Debug::Log("[RenderDX] Failed to reset command list for populating commands.\n"); + return false; + } + + UploadSurfaceToGpu(pixels, source_pitch); + + CommandList->RSSetViewports(1, &Viewport); + CommandList->RSSetScissorRects(1, &ScissorRect); + + D3D12_RESOURCE_BARRIER backBufferToRenderTarget {}; + backBufferToRenderTarget.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + backBufferToRenderTarget.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + backBufferToRenderTarget.Transition.pResource = RenderTargets[FrameIndex].Get(); + backBufferToRenderTarget.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + backBufferToRenderTarget.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; + backBufferToRenderTarget.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + CommandList->ResourceBarrier(1, &backBufferToRenderTarget); + + D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = RtvHeap->GetCPUDescriptorHandleForHeapStart(); + rtvHandle.ptr += static_cast(FrameIndex) * static_cast(RtvDescriptorSize); + CommandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr); + const float clearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; + CommandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); + CommandList->SetDescriptorHeaps(1, SrvHeap.GetAddressOf()); + CommandList->SetGraphicsRootSignature(RootSignature.Get()); + CommandList->SetGraphicsRootDescriptorTable(0, SrvHeap->GetGPUDescriptorHandleForHeapStart()); + CommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + CommandList->DrawInstanced(3, 1, 0, 0); + + D3D12_RESOURCE_BARRIER backBufferToPresent {}; + backBufferToPresent.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + backBufferToPresent.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + backBufferToPresent.Transition.pResource = RenderTargets[FrameIndex].Get(); + backBufferToPresent.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + backBufferToPresent.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + backBufferToPresent.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; + + CommandList->ResourceBarrier(1, &backBufferToPresent); + + if (FAILED(CommandList->Close())) { + Debug::Log("[RenderDX] Failed to close command list after populating commands.\n"); + return false; + } + + return true; +} + +void DXRenderer::UploadSurfaceToGpu(const void* pixels, int source_pitch) { + auto dstBase = SurfaceUploadMapped[FrameIndex]; + const auto* srcBase = static_cast(pixels); + const UINT sourceRowBytes = RenderWidth * static_cast(sizeof(std::uint16_t)); + for (int y = 0; y < RenderHeight; ++y) { + auto* dstRow = dstBase + static_cast(y) * SurfaceUploadRowPitch; + const auto* srcRow = srcBase + static_cast(y) * source_pitch; + std::memcpy(dstRow, srcRow, sourceRowBytes); + } + + TransitionSurfaceTexture(SurfaceTextureState, D3D12_RESOURCE_STATE_COPY_DEST); + + D3D12_TEXTURE_COPY_LOCATION dstLocation {}; + dstLocation.pResource = SurfaceTexture.Get(); + dstLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dstLocation.SubresourceIndex = 0; + + D3D12_TEXTURE_COPY_LOCATION srcLocation {}; + srcLocation.pResource = SurfaceUploadBuffers[FrameIndex].Get(); + srcLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + srcLocation.PlacedFootprint.Offset = 0; + srcLocation.PlacedFootprint.Footprint.Format = DXGI_FORMAT_B5G6R5_UNORM; + srcLocation.PlacedFootprint.Footprint.Width = RenderWidth; + srcLocation.PlacedFootprint.Footprint.Height = RenderHeight; + srcLocation.PlacedFootprint.Footprint.Depth = 1; + srcLocation.PlacedFootprint.Footprint.RowPitch = SurfaceUploadRowPitch; + + CommandList->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, nullptr); + + TransitionSurfaceTexture(SurfaceTextureState, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); +} + +void DXRenderer::TransitionSurfaceTexture(D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after) { + if (before == after) { + return; + } + + D3D12_RESOURCE_BARRIER barrier {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + barrier.Transition.pResource = SurfaceTexture.Get(); + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrier.Transition.StateBefore = before; + barrier.Transition.StateAfter = after; + + CommandList->ResourceBarrier(1, &barrier); + + SurfaceTextureState = after; +} + +bool DXRenderer::MoveToNextFrame() { + const UINT64 currentFenceValue = FenceValues[FrameIndex]; + if (FAILED(CommandQueue->Signal(Fence.Get(), currentFenceValue))) { + Debug::Log("[RenderDX] Failed to signal command queue for moving to next frame.\n"); + return false; + } + + FrameIndex = SwapChain->GetCurrentBackBufferIndex(); + if (Fence->GetCompletedValue() < FenceValues[FrameIndex]) { + if (FAILED(Fence->SetEventOnCompletion(FenceValues[FrameIndex], FenceEvent))) { + Debug::Log("[RenderDX] Failed to set event on fence completion for moving to next frame.\n"); + return false; + } + DWORD waitResult = ::WaitForSingleObjectEx(FenceEvent, INFINITE, FALSE); + if (waitResult != WAIT_OBJECT_0) { + Debug::Log("[RenderDX] Wait for fence event failed while moving to next frame with error code: %lu\n", GetLastError()); + return false; + } + } + + FenceValues[FrameIndex] = currentFenceValue + 1; + return true; +} diff --git a/src/Render/Renderer.h b/src/Render/Renderer.h new file mode 100644 index 0000000000..791df077f3 --- /dev/null +++ b/src/Render/Renderer.h @@ -0,0 +1,142 @@ +#pragma once + +#include + +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#ifdef DEBUG +#define DXRENDER_DEBUG 0 +#else +#define DXRENDER_DEBUG 1 +#endif + +class DXRenderer { +public: + static DXRenderer& Instance(); + + bool CreateMainWindow(HINSTANCE instance, int cmd_show, int width, int height, WNDPROC proc); + void DestroyMainWindow(); + + bool IsRendererReady(); + bool CreateRenderer(int width, int height, int bits_per_pixel); + void DestroyRenderer(); + bool ResizeWindow(int width, int height); + + void ToggleFullscreen(); + + bool UploadSurfaceToTexture(void* surface_data, int source_pitch); + void SetRenderScale(bool scale); + bool Present(); + + void MoveWindow(int x, int y, int width, int height); + bool IsWindowed() const; + + int GetWindowWidth() const; + int GetWindowHeight() const; + float GetViewportX() const; + float GetViewportY() const; + float GetViewportWidth() const; + float GetViewportHeight() const; + +private: + DXRenderer(); + ~DXRenderer(); + + bool LoadImports(); + void UnloadImports(); + + bool CreateDevice(); + bool CreateCommandQueue(); + bool CreateSwapChain(); + bool CreateRtvHeap(); + bool CreateRenderTargetViews(); + bool CreateSrvHeap(); + bool CreateSurfacePipeline(); + bool CreateCommandObjects(); + bool CreateFenceObjects(); + bool CreateFixedSurfaceGpuResources(); + + void UpdateViewportAndScissor(); + + bool WaitForGpu(); + Microsoft::WRL::ComPtr CompileShader(std::string_view source, std::string_view entryPoint, std::string_view target); + bool PopulateCommandListForCPUSurface(const void* pixels, int source_pitch); + void UploadSurfaceToGpu(const void* pixels, int source_pitch); + void TransitionSurfaceTexture(D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after); + bool MoveToNextFrame(); + + HMODULE D3D12Lib { nullptr }; +#if DXRENDER_DEBUG + decltype(&D3D12GetDebugInterface) FP_D3D12GetDebugInterface { nullptr }; +#endif + decltype(&D3D12CreateDevice) FP_D3D12CreateDevice { nullptr }; + decltype(&D3D12SerializeRootSignature) FP_D3D12SerializeRootSignature { nullptr }; + + HMODULE DXGILib; + decltype(&CreateDXGIFactory2) FP_CreateDXGIFactory2 { nullptr }; + + HMODULE D3DCompilerLib { nullptr }; + decltype(&D3DCompile) FP_D3DCompile { nullptr }; + + HWND Hwnd { nullptr }; + int WindowWidth { 0 }; + int WindowHeight { 0 }; + float RenderViewportX { 0.0f }; // Current render viewport left in client coordinates. + float RenderViewportY { 0.0f }; // Current render viewport top in client coordinates. + float RenderViewportWidth { 0.0f }; // Current render viewport width in client coordinates. + float RenderViewportHeight { 0.0f }; // Current render viewport height in client coordinates. + RECT WindowedRect {}; // Saved window rectangle before borderless fullscreen. + LONG_PTR WindowedStyle { 0 }; // Saved window style before borderless fullscreen. + LONG_PTR WindowedExStyle { 0 }; // Saved extended window style before borderless fullscreen. + int RenderWidth { 0 }; + int RenderHeight { 0 }; + UINT RenderPitch { 0 }; + bool ScaleRender { true }; + bool Windowed { true }; + bool HasWindowedState { false }; // Whether windowed placement has been saved. + + UINT FrameIndex { 0 }; + UINT RtvDescriptorSize { 0 }; + + Microsoft::WRL::ComPtr Factory; + Microsoft::WRL::ComPtr Device; + Microsoft::WRL::ComPtr CommandQueue; + Microsoft::WRL::ComPtr SwapChain; + Microsoft::WRL::ComPtr RtvHeap; + + static constexpr UINT kFrameCount = 2; + std::array, kFrameCount> RenderTargets {}; + + std::array, kFrameCount> CommandAllocators {}; + Microsoft::WRL::ComPtr CommandList; + + Microsoft::WRL::ComPtr Fence; + std::array FenceValues {}; + HANDLE FenceEvent; + + D3D12_VIEWPORT Viewport {}; + D3D12_RECT ScissorRect {}; + + Microsoft::WRL::ComPtr SurfaceTexture; + D3D12_RESOURCE_STATES SurfaceTextureState { D3D12_RESOURCE_STATE_COPY_DEST }; + + std::array, kFrameCount> SurfaceUploadBuffers {}; + std::array SurfaceUploadMapped {}; + + UINT SurfaceUploadRowPitch { 0 }; + UINT64 SurfaceUploadBufferSize { 0 }; + + Microsoft::WRL::ComPtr SrvHeap; + Microsoft::WRL::ComPtr RootSignature; + Microsoft::WRL::ComPtr PipelineState; +}; diff --git a/src/Render/Surface.cpp b/src/Render/Surface.cpp new file mode 100644 index 0000000000..6ffc70bbac --- /dev/null +++ b/src/Render/Surface.cpp @@ -0,0 +1,286 @@ +#include "Surface.h" + +#include + +#include +#include + +class DXSurfaceImpl +{ +public: + DXSurfaceImpl(int width, int height); + ~DXSurfaceImpl(); + + int Pitch; + std::unique_ptr Buffer; +}; + +void DXSurface::CTOR(int width, int height) +{ + this->Width = width; + this->Height = height; + this->LockLevel = 0; + this->BytesPerPixel = 2; + ImplRef() = new DXSurfaceImpl(width, height); +} + +void DXSurface::DTOR() +{ + if (ImplRef()) + { + delete ImplRef(); + ImplRef() = nullptr; + } +} + +DXSurface::DXSurface(int width, int height) : DSurface { noinit_t{} } +{ + CTOR(width, height); +} + +DXSurface::~DXSurface() +{ + DTOR(); +} + +bool DXSurface::CopyFromWhole(Surface* pSrc, bool trans, bool same_copy_cpu) +{ + JMP_THIS(0x7BBAF0); +} + +bool DXSurface::CopyFromPart(RectangleStruct* pClipRect, Surface* pSrc, RectangleStruct* pSrcRect, bool trans, bool same_copy_cpu) +{ + JMP_THIS(0x7BBB90); +} + +bool DXSurface::CopyFrom(RectangleStruct* pClipRect, RectangleStruct* pClipRect2, Surface* pSrc, RectangleStruct* pDestRect, RectangleStruct* pSrcRect, bool trans, bool same_copy_cpu) +{ + JMP_THIS(0x7BBCF0); +} + +bool DXSurface::FillRectEx(RectangleStruct* pClipRect, RectangleStruct* pFillRect, COLORREF nColor) +{ + JMP_THIS(0x7BB050); +} + +bool DXSurface::FillRect(RectangleStruct* pFillRect, COLORREF nColor) +{ + JMP_THIS(0x7BB020); +} + +bool DXSurface::Fill(COLORREF nColor) +{ + JMP_THIS(0x7BBAB0); +} + +bool DXSurface::FillRectTrans(RectangleStruct* pClipRect, ColorStruct* pColor, int nOpacity) +{ + JMP_THIS(0x4BB830); +} + +bool DXSurface::DrawEllipse(int XOff, int YOff, int CenterX, int CenterY, RectangleStruct Rect, COLORREF nColor) +{ + JMP_THIS(0x7BB350); +} + +bool DXSurface::SetPixel(Point2D* pPoint, COLORREF nColor) +{ + JMP_THIS(0x7BAEB0); +} + +COLORREF DXSurface::GetPixel(Point2D* pPoint) +{ + JMP_THIS(0x7BAE60); +} + +bool DXSurface::DrawLineEx(RectangleStruct* pClipRect, Point2D* pStart, Point2D* pEnd, COLORREF nColor) +{ + JMP_THIS(0x7BA610); +} + +bool DXSurface::DrawLine(Point2D* pStart, Point2D* pEnd, COLORREF nColor) +{ + JMP_THIS(0x7BA5E0); +} + +bool DXSurface::DrawLineColor(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, COLORREF nColor, int startZ, int endZ, bool bUnk) +{ + JMP_THIS(0x4BFD30); +} + +bool DXSurface::DrawMultiplyingLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, DWORD dwMultiplier, DWORD dwUnk1, DWORD dwUnk2, bool bUnk) +{ + JMP_THIS(0x4BBCA0); +} + +bool DXSurface::DrawSubtractiveLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pColor, DWORD dwUnk1, DWORD dwUnk2, bool bUnk1, bool bUnk2, bool bUkn3, bool bUkn4, float fUkn) +{ + JMP_THIS(0x4BC750); +} + +bool DXSurface::DrawRGBMultiplyingLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pColor, float Intensity, int zSource, int zTarget) +{ + JMP_THIS(0x4BDF00); +} + +bool DXSurface::PlotLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, bool(__fastcall* fpDrawCallback)(int*)) +{ + JMP_THIS(0x7BAB90); +} + +bool DXSurface::DrawDashedLine(Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset) +{ + JMP_THIS(0x7BA8C0); +} + +bool DXSurface::DrawDashedLine_(Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset, bool bUkn) +{ + JMP_THIS(0x4C0750); +} + +bool DXSurface::DrawLine_(Point2D* pStart, Point2D* pEnd, int nColor, bool bUnk) +{ + JMP_THIS(0x4C0E30); +} + +bool DXSurface::DrawRectEx(RectangleStruct* pClipRect, RectangleStruct* pDrawRect, int nColor) +{ + JMP_THIS(0x7BADC0); +} + +bool DXSurface::DrawRect(RectangleStruct* pDrawRect, DWORD dwColor) +{ + JMP_THIS(0x7BAD90); +} + +void* DXSurface::Lock(int X, int Y) +{ + if (X >= 0 && Y >= 0) + { + ++LockLevel; + return Impl()->Buffer.get() + Y * Impl()->Pitch + X * GetBytesPerPixel(); + } + return nullptr; +} + +bool DXSurface::Unlock() +{ + if (LockLevel > 0) + { + --LockLevel; + return true; + } + + return false; +} + +bool DXSurface::CanLock(DWORD dwUkn1, DWORD dwUkn2) +{ + return true; +} + +bool DXSurface::vt_entry_68(DWORD dwUnk1, DWORD dwUnk2) +{ + return true; +} + +bool DXSurface::IsLocked() +{ + return false; +} + +int DXSurface::GetBytesPerPixel() +{ + return 2; +} + +int DXSurface::GetPitch() +{ + return Impl()->Pitch; +} + +RectangleStruct* DXSurface::GetRect(RectangleStruct* pRect) +{ + *pRect = { 0, 0, GetWidth(), GetHeight() }; + return pRect; +} + +int DXSurface::GetWidth() +{ + return Width; +} + +int DXSurface::GetHeight() +{ + return Height; +} + +bool DXSurface::IsDSurface() +{ + return true; +} + +bool DXSurface::PutPixelClip(Point2D* pPoint, short nUkn, RectangleStruct* pRect) +{ + JMP_THIS(0x7BAF90); +} + +short DXSurface::GetPixelClip(Point2D* pPoint, RectangleStruct* pRect) +{ + JMP_THIS(0x7BAF10); +} + +bool DXSurface::DrawGradientLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pStartColor, ColorStruct* pEndColor, float fStep, int nColor) +{ + JMP_THIS(0x4BF750); +} + +bool DXSurface::CanBlit() +{ + return true; +} + +void* DXSurface::GetBuffer() +{ + return Impl()->Buffer.get(); +} + +DXSurfaceImpl::DXSurfaceImpl(int width, int height) +{ + const int sourceRowBytes = width * 2; // 2 bytes per pixel for 16-bit color + Pitch = (sourceRowBytes + 256 - 1) & ~(256 - 1); // Align up to D3D12_TEXTURE_DATA_PITCH_ALIGNMENT + Buffer.reset(new BYTE[Pitch * height]); +} + +DXSurfaceImpl::~DXSurfaceImpl() {} + +static __forceinline unsigned int Build_Hicolor_Pixel(unsigned int r, unsigned int g, unsigned int b) +{ + return (r >> Drawing::RedShiftRight << Drawing::RedShiftLeft) | + (g >> Drawing::GreenShiftRight << Drawing::GreenShiftLeft) | + (b >> Drawing::BlueShiftRight << Drawing::BlueShiftLeft); +} + +DXSurface* __fastcall DXSurface::CreatePrimary() +{ + Drawing::AllowSoftwareBlitFills = false; + Drawing::AllowSoftwareBlitStretch = false; + + Debug::Log("[RenderDX] D3D12 surface created as primary surface.\n"); + + auto surface = new DXSurface(DSurface::WindowBounds.Width, DSurface::WindowBounds.Height); + + // RGB565 color shifts + Drawing::RedShiftLeft = 11; + Drawing::RedShiftRight = 3; + Drawing::GreenShiftLeft = 5; + Drawing::GreenShiftRight = 2; + Drawing::BlueShiftLeft = 0; + Drawing::BlueShiftRight = 3; + Drawing::ColorMode = RGBMode::RGB565; + Drawing::HalfbrightMask = static_cast(Build_Hicolor_Pixel(127, 127, 127)); + Drawing::QuarterbrightMask = static_cast(Build_Hicolor_Pixel(63, 63, 63)); + Drawing::EighthbrightMask = static_cast(Build_Hicolor_Pixel(31, 31, 31)); + + return surface; +} diff --git a/src/Render/Surface.h b/src/Render/Surface.h new file mode 100644 index 0000000000..51da806d2f --- /dev/null +++ b/src/Render/Surface.h @@ -0,0 +1,99 @@ +#pragma once + +#include + +// CPU render +class DXSurfaceImpl; +class DXSurface : public DSurface { +private: + DXSurfaceImpl* Impl() const { + return reinterpret_cast(Buffer); + } + + DXSurfaceImpl*& ImplRef() { + return reinterpret_cast(Buffer); + } + +public: + static DXSurface* __fastcall CreatePrimary(); + + void CTOR(int width, int height); + void DTOR(); + + DXSurface(int width, int height); + + virtual ~DXSurface() override; + + //Surface + virtual bool CopyFromWhole(Surface* pSrc, bool trans, bool same_copy_cpu) override; + + virtual bool CopyFromPart( + RectangleStruct* pClipRect, //ignored and retrieved again... + Surface* pSrc, + RectangleStruct* pSrcRect, //desired source rect of pSrc ? + bool trans, + bool same_copy_cpu) override; + + virtual bool CopyFrom( + RectangleStruct* pClipRect, + RectangleStruct* pClipRect2, //again? hmm + Surface* pSrc, + RectangleStruct* pDestRect, //desired dest rect of pSrc ? (stretched? clipped?) + RectangleStruct* pSrcRect, //desired source rect of pSrc ? + bool trans, + bool same_copy_cpu) override; + + virtual bool FillRectEx(RectangleStruct* pClipRect, RectangleStruct* pFillRect, COLORREF nColor) override; + virtual bool FillRect(RectangleStruct* pFillRect, COLORREF nColor) override; + virtual bool Fill(COLORREF nColor) override; + virtual bool FillRectTrans(RectangleStruct* pClipRect, ColorStruct* pColor, int Opacity) override; + virtual bool DrawEllipse(int XOff, int YOff, int CenterX, int CenterY, RectangleStruct Rect, COLORREF nColor) override; + virtual bool SetPixel(Point2D* pPoint, COLORREF nColor) override; + virtual COLORREF GetPixel(Point2D* pPoint) override; + virtual bool DrawLineEx(RectangleStruct* pClipRect, Point2D* pStart, Point2D* pEnd, COLORREF nColor) override; + virtual bool DrawLine(Point2D* pStart, Point2D* pEnd, COLORREF nColor) override; + virtual bool DrawLineColor( + RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, COLORREF nColor, + int startZ, int endZ, bool bUnk) override; + + virtual bool DrawMultiplyingLine( + RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, DWORD dwMultiplier, + DWORD dwUnk1, DWORD dwUnk2, bool bUnk) override; + + virtual bool DrawSubtractiveLine( + RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pColor, + DWORD dwUnk1, DWORD dwUnk2, bool bUnk1, bool bUnk2, + bool bUkn3, bool bUkn4, float fUkn) override; + + virtual bool DrawRGBMultiplyingLine( + RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pColor, + float Intensity, int zSource, int zTarget) override; + + virtual bool PlotLine( + RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, bool(__fastcall* fpDrawCallback)(int*)) override; + virtual bool DrawDashedLine( + Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset) override; + virtual bool DrawDashedLine_( + Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset, bool bUkn) override; + virtual bool DrawLine_(Point2D* pStart, Point2D* pEnd, int nColor, bool bUnk) override; + virtual bool DrawRectEx(RectangleStruct* pClipRect, RectangleStruct* pDrawRect, int nColor) override; + virtual bool DrawRect(RectangleStruct* pDrawRect, DWORD dwColor) override; + virtual void* Lock(int X, int Y) override; + virtual bool Unlock() override; + virtual bool CanLock(DWORD dwUkn1 = 0, DWORD dwUkn2 = 0) override; + virtual bool vt_entry_68(DWORD dwUnk1, DWORD dwUnk2) override; + virtual bool IsLocked() override; + virtual int GetBytesPerPixel() override; + virtual int GetPitch() override; + virtual RectangleStruct* GetRect(RectangleStruct* pRect) override; + virtual int GetWidth() override; + virtual int GetHeight() override; + virtual bool IsDSurface() override; + virtual bool PutPixelClip(Point2D* pPoint, short nUkn, RectangleStruct* pRect) override; + virtual short GetPixelClip(Point2D* pPoint, RectangleStruct* pRect) override; + virtual bool DrawGradientLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, + ColorStruct* pStartColor, ColorStruct* pEndColor, float fStep, int nColor) override; + virtual bool CanBlit() override; + + void* GetBuffer(); +}; From 576ce823258ade3de3aeaf3d9f8ddd268b418921 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Sat, 16 May 2026 21:42:12 +0800 Subject: [PATCH 02/21] Enhance D3D12/DXGI error logging with HRESULTs and Win32 error codes --- src/Render/Renderer.cpp | 131 ++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 53 deletions(-) diff --git a/src/Render/Renderer.cpp b/src/Render/Renderer.cpp index 0508521930..57c8156156 100644 --- a/src/Render/Renderer.cpp +++ b/src/Render/Renderer.cpp @@ -558,26 +558,29 @@ void DXRenderer::UnloadImports() { } bool DXRenderer::CreateDevice() { + HRESULT hr = S_OK; UINT dxgiFactoryFlags = 0; #if DXRENDER_DEBUG Microsoft::WRL::ComPtr debugController; - if (SUCCEEDED(FP_D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)))) { + if (SUCCEEDED(hr = FP_D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)))) { + Debug::Log("[RenderDX] D3D12 debug layer enabled.\n"); debugController->EnableDebugLayer(); } dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG; #endif - if (FAILED(FP_CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&Factory)))) { + if (FAILED(hr = FP_CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&Factory)))) { + Debug::Log("[RenderDX] Failed to create DXGI factory: 0x%08X\n", static_cast(hr)); return false; } constexpr bool kUseWarpDevice = false; if (kUseWarpDevice) { Microsoft::WRL::ComPtr warpAdapter; - if (FAILED(Factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)))) { - Debug::Log("[RenderDX] Failed to create WARP adapter.\n"); + if (FAILED(hr = Factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)))) { + Debug::Log("[RenderDX] Failed to create WARP adapter: 0x%08X\n", static_cast(hr)); return false; } - if (FAILED(FP_D3D12CreateDevice(warpAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) { - Debug::Log("[RenderDX] Failed to create WARP adapter.\n"); + if (FAILED(hr = FP_D3D12CreateDevice(warpAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) { + Debug::Log("[RenderDX] Failed to create WARP adapter: 0x%08X\n", static_cast(hr)); return false; } @@ -591,7 +594,7 @@ bool DXRenderer::CreateDevice() { if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) { continue; } - if (SUCCEEDED(FP_D3D12CreateDevice(hardwareAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) { + if (SUCCEEDED(hr = FP_D3D12CreateDevice(hardwareAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) { Debug::Log("[RenderDX] D3D12 device created successfully on adapter: %ls\n", desc.Description); break; } @@ -607,14 +610,16 @@ bool DXRenderer::CreateDevice() { } bool DXRenderer::CreateCommandQueue() { + HRESULT hr = S_OK; + D3D12_COMMAND_QUEUE_DESC queueDesc {}; queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; queueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; queueDesc.NodeMask = 0; - if (FAILED(Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&CommandQueue)))) { - Debug::Log("[RenderDX] Failed to create command queue.\n"); + if (FAILED(hr = Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&CommandQueue)))) { + Debug::Log("[RenderDX] Failed to create command queue: 0x%08X\n", static_cast(hr)); return false; } @@ -623,6 +628,8 @@ bool DXRenderer::CreateCommandQueue() { } bool DXRenderer::CreateSwapChain() { + HRESULT hr = S_OK; + DXGI_SWAP_CHAIN_DESC1 desc {}; desc.Width = WindowWidth; desc.Height = WindowHeight; @@ -638,18 +645,18 @@ bool DXRenderer::CreateSwapChain() { desc.Flags = 0; Microsoft::WRL::ComPtr swapChain1; - if (FAILED(Factory->CreateSwapChainForHwnd(CommandQueue.Get(), Hwnd, &desc, nullptr, nullptr, &swapChain1))) { - Debug::Log("[RenderDX] Failed to create swap chain.\n"); + if (FAILED(hr = Factory->CreateSwapChainForHwnd(CommandQueue.Get(), Hwnd, &desc, nullptr, nullptr, &swapChain1))) { + Debug::Log("[RenderDX] Failed to create swap chain: 0x%08X\n", static_cast(hr)); return false; } - if (FAILED(Factory->MakeWindowAssociation(Hwnd, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_PRINT_SCREEN))) { - Debug::Log("[RenderDX] Failed to set window association.\n"); + if (FAILED(hr = Factory->MakeWindowAssociation(Hwnd, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_PRINT_SCREEN))) { + Debug::Log("[RenderDX] Failed to set window association: 0x%08X\n", static_cast(hr)); return false; } - if (FAILED(swapChain1->QueryInterface(IID_PPV_ARGS(&SwapChain)))) { - Debug::Log("[RenderDX] Failed to query IDXGISwapChain3 interface.\n"); + if (FAILED(hr = swapChain1->QueryInterface(IID_PPV_ARGS(&SwapChain)))) { + Debug::Log("[RenderDX] Failed to query IDXGISwapChain3 interface: 0x%08X\n", static_cast(hr)); return false; } @@ -659,14 +666,16 @@ bool DXRenderer::CreateSwapChain() { } bool DXRenderer::CreateRtvHeap() { + HRESULT hr = S_OK; + D3D12_DESCRIPTOR_HEAP_DESC desc {}; desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; desc.NumDescriptors = kFrameCount; desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; desc.NodeMask = 0; - if (FAILED(Device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&RtvHeap)))) { - Debug::Log("[RenderDX] Failed to create RTV descriptor heap.\n"); + if (FAILED(hr = Device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&RtvHeap)))) { + Debug::Log("[RenderDX] Failed to create RTV descriptor heap: 0x%08X\n", static_cast(hr)); return false; } @@ -676,10 +685,12 @@ bool DXRenderer::CreateRtvHeap() { } bool DXRenderer::CreateRenderTargetViews() { + HRESULT hr = S_OK; + D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = RtvHeap->GetCPUDescriptorHandleForHeapStart(); for (UINT i = 0; i < kFrameCount; ++i) { - if (FAILED(SwapChain->GetBuffer(i, IID_PPV_ARGS(&RenderTargets[i])))) { - Debug::Log("[RenderDX] Failed to get back buffer %u.\n", i); + if (FAILED(hr = SwapChain->GetBuffer(i, IID_PPV_ARGS(&RenderTargets[i])))) { + Debug::Log("[RenderDX] Failed to get back buffer %u: 0x%08X\n", i, static_cast(hr)); return false; } Device->CreateRenderTargetView(RenderTargets[i].Get(), nullptr, rtvHandle); @@ -691,14 +702,16 @@ bool DXRenderer::CreateRenderTargetViews() { } bool DXRenderer::CreateSrvHeap() { + HRESULT hr = S_OK; + D3D12_DESCRIPTOR_HEAP_DESC desc {}; desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; desc.NumDescriptors = 1; // For surface texture SRV desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; desc.NodeMask = 0; - if (FAILED(Device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&SrvHeap)))) { - Debug::Log("[RenderDX] Failed to create SRV descriptor heap.\n"); + if (FAILED(hr = Device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&SrvHeap)))) { + Debug::Log("[RenderDX] Failed to create SRV descriptor heap: 0x%08X\n", static_cast(hr)); return false; } @@ -760,8 +773,8 @@ bool DXRenderer::CreateSurfacePipeline() { return false; } - if (FAILED(Device->CreateRootSignature(0, rootSigBlob->GetBufferPointer(), rootSigBlob->GetBufferSize(), IID_PPV_ARGS(&RootSignature)))) { - Debug::Log("[RenderDX] Failed to create root signature.\n"); + if (FAILED(hr = Device->CreateRootSignature(0, rootSigBlob->GetBufferPointer(), rootSigBlob->GetBufferSize(), IID_PPV_ARGS(&RootSignature)))) { + Debug::Log("[RenderDX] Failed to create root signature: 0x%08X\n", static_cast(hr)); return false; } @@ -860,8 +873,8 @@ float4 PSMain(VSOut input) : SV_Target0 psoDesc.SampleDesc.Count = 1; psoDesc.SampleDesc.Quality = 0; - if (FAILED(Device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&PipelineState)))) { - Debug::Log("[RenderDX] Failed to create pipeline state.\n"); + if (FAILED(hr = Device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&PipelineState)))) { + Debug::Log("[RenderDX] Failed to create pipeline state: 0x%08X\n", static_cast(hr)); return false; } @@ -870,15 +883,17 @@ float4 PSMain(VSOut input) : SV_Target0 } bool DXRenderer::CreateCommandObjects() { + HRESULT hr = S_OK; + for (UINT i = 0; i < kFrameCount; ++i) { - if (FAILED(Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&CommandAllocators[i])))) { - Debug::Log("[RenderDX] Failed to create command allocator %u.\n", i); + if (FAILED(hr = Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&CommandAllocators[i])))) { + Debug::Log("[RenderDX] Failed to create command allocator %u: 0x%08X\n", i, static_cast(hr)); return false; } } - if (FAILED(Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, CommandAllocators[FrameIndex].Get(), PipelineState.Get(), IID_PPV_ARGS(&CommandList)))) { - Debug::Log("[RenderDX] Failed to create command list.\n"); + if (FAILED(hr = Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, CommandAllocators[FrameIndex].Get(), PipelineState.Get(), IID_PPV_ARGS(&CommandList)))) { + Debug::Log("[RenderDX] Failed to create command list: 0x%08X\n", static_cast(hr)); return false; } @@ -888,10 +903,12 @@ bool DXRenderer::CreateCommandObjects() { } bool DXRenderer::CreateFenceObjects() { + HRESULT hr = S_OK; + FenceValues.fill(0); - if (FAILED(Device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&Fence)))) { - Debug::Log("[RenderDX] Failed to create fence.\n"); + if (FAILED(hr = Device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&Fence)))) { + Debug::Log("[RenderDX] Failed to create fence: 0x%08X\n", static_cast(hr)); return false; } @@ -899,7 +916,7 @@ bool DXRenderer::CreateFenceObjects() { FenceEvent = ::CreateEventW(nullptr, FALSE, FALSE, nullptr); if (!FenceEvent) { - Debug::Log("[RenderDX] Failed to create fence event.\n"); + Debug::Log("[RenderDX] Failed to create fence event: 0x%08X\n", ::GetLastError()); return false; } @@ -908,6 +925,8 @@ bool DXRenderer::CreateFenceObjects() { } bool DXRenderer::CreateFixedSurfaceGpuResources() { + HRESULT hr = S_OK; + const UINT sourceRowBytes = RenderWidth * sizeof(std::uint16_t); SurfaceUploadRowPitch = (sourceRowBytes + D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1) & ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1); // Align up SurfaceUploadBufferSize = static_cast(SurfaceUploadRowPitch) * static_cast(RenderHeight); @@ -934,7 +953,7 @@ bool DXRenderer::CreateFixedSurfaceGpuResources() { SurfaceTextureState = D3D12_RESOURCE_STATE_COPY_DEST; - if (FAILED(Device->CreateCommittedResource( + if (FAILED(hr = Device->CreateCommittedResource( &defaultHeap, D3D12_HEAP_FLAG_NONE, &textureDesc, @@ -942,7 +961,7 @@ bool DXRenderer::CreateFixedSurfaceGpuResources() { nullptr, IID_PPV_ARGS(&SurfaceTexture) ))) { - Debug::Log("[RenderDX] Failed to create surface texture resource.\n"); + Debug::Log("[RenderDX] Failed to create surface texture resource: 0x%08X\n", static_cast(hr)); return false; } @@ -967,7 +986,7 @@ bool DXRenderer::CreateFixedSurfaceGpuResources() { uploadDesc.Flags = D3D12_RESOURCE_FLAG_NONE; for (UINT i = 0; i < kFrameCount; ++i) { - if (FAILED(Device->CreateCommittedResource( + if (FAILED(hr = Device->CreateCommittedResource( &uploadHeap, D3D12_HEAP_FLAG_NONE, &uploadDesc, @@ -975,7 +994,7 @@ bool DXRenderer::CreateFixedSurfaceGpuResources() { nullptr, IID_PPV_ARGS(&SurfaceUploadBuffers[i]) ))) { - Debug::Log("[RenderDX] Failed to create surface upload buffer %u.\n", i); + Debug::Log("[RenderDX] Failed to create surface upload buffer %u: 0x%08X\n", i, static_cast(hr)); return false; } @@ -984,7 +1003,7 @@ bool DXRenderer::CreateFixedSurfaceGpuResources() { readRange.End = 0; if (FAILED(SurfaceUploadBuffers[i]->Map(0, &readRange, reinterpret_cast(&SurfaceUploadMapped[i])))) { - Debug::Log("[RenderDX] Failed to map surface upload buffer %u.\n", i); + Debug::Log("[RenderDX] Failed to map surface upload buffer %u: 0x%08X\n", i, static_cast(hr)); return false; } } @@ -1060,24 +1079,26 @@ void DXRenderer::UpdateViewportAndScissor() { } bool DXRenderer::WaitForGpu() { + HRESULT hr = S_OK; + if (!CommandQueue || !Fence || !FenceEvent) { return true; // If we don't have the necessary objects, we can't wait, but also can't report an error. } const UINT64 currentFenceValue = FenceValues[FrameIndex]; - if (FAILED(CommandQueue->Signal(Fence.Get(), currentFenceValue))) { - Debug::Log("[RenderDX] Failed to signal command queue for GPU synchronization.\n"); + if (FAILED(hr = CommandQueue->Signal(Fence.Get(), currentFenceValue))) { + Debug::Log("[RenderDX] Failed to signal command queue for GPU synchronization: 0x%08X\n", static_cast(hr)); return false; } - if (FAILED(Fence->SetEventOnCompletion(currentFenceValue, FenceEvent))) { - Debug::Log("[RenderDX] Failed to set event on fence completion for GPU synchronization.\n"); + if (FAILED(hr = Fence->SetEventOnCompletion(currentFenceValue, FenceEvent))) { + Debug::Log("[RenderDX] Failed to set event on fence completion for GPU synchronization: 0x%08X\n", static_cast(hr)); return false; } DWORD waitResult = ::WaitForSingleObjectEx(FenceEvent, INFINITE, FALSE); if (waitResult != WAIT_OBJECT_0) { - Debug::Log("[RenderDX] Wait for GPU synchronization event failed with error code: %lu\n", GetLastError()); + Debug::Log("[RenderDX] Wait for GPU synchronization event failed with error code: 0x%08X\n", ::GetLastError()); return false; } @@ -1103,13 +1124,15 @@ Microsoft::WRL::ComPtr DXRenderer::CompileShader(std::string_view sour } bool DXRenderer::PopulateCommandListForCPUSurface(const void* pixels, int source_pitch) { - if (FAILED(CommandAllocators[FrameIndex]->Reset())) { - Debug::Log("[RenderDX] Failed to reset command allocator for populating command list.\n"); + HRESULT hr = S_OK; + + if (FAILED(hr = CommandAllocators[FrameIndex]->Reset())) { + Debug::Log("[RenderDX] Failed to reset command allocator for populating command list: 0x%08X\n", static_cast(hr)); return false; } - if (FAILED(CommandList->Reset(CommandAllocators[FrameIndex].Get(), PipelineState.Get()))) { - Debug::Log("[RenderDX] Failed to reset command list for populating commands.\n"); + if (FAILED(hr = CommandList->Reset(CommandAllocators[FrameIndex].Get(), PipelineState.Get()))) { + Debug::Log("[RenderDX] Failed to reset command list for populating commands: 0x%08X\n", static_cast(hr)); return false; } @@ -1148,8 +1171,8 @@ bool DXRenderer::PopulateCommandListForCPUSurface(const void* pixels, int source CommandList->ResourceBarrier(1, &backBufferToPresent); - if (FAILED(CommandList->Close())) { - Debug::Log("[RenderDX] Failed to close command list after populating commands.\n"); + if (FAILED(hr = CommandList->Close())) { + Debug::Log("[RenderDX] Failed to close command list after populating commands: 0x%08X\n", static_cast(hr)); return false; } @@ -1207,21 +1230,23 @@ void DXRenderer::TransitionSurfaceTexture(D3D12_RESOURCE_STATES before, D3D12_RE } bool DXRenderer::MoveToNextFrame() { + HRESULT hr = S_OK; + const UINT64 currentFenceValue = FenceValues[FrameIndex]; - if (FAILED(CommandQueue->Signal(Fence.Get(), currentFenceValue))) { - Debug::Log("[RenderDX] Failed to signal command queue for moving to next frame.\n"); + if (FAILED(hr = CommandQueue->Signal(Fence.Get(), currentFenceValue))) { + Debug::Log("[RenderDX] Failed to signal command queue for moving to next frame: 0x%08X\n", static_cast(hr)); return false; } FrameIndex = SwapChain->GetCurrentBackBufferIndex(); if (Fence->GetCompletedValue() < FenceValues[FrameIndex]) { - if (FAILED(Fence->SetEventOnCompletion(FenceValues[FrameIndex], FenceEvent))) { - Debug::Log("[RenderDX] Failed to set event on fence completion for moving to next frame.\n"); + if (FAILED(hr = Fence->SetEventOnCompletion(FenceValues[FrameIndex], FenceEvent))) { + Debug::Log("[RenderDX] Failed to set event on fence completion for moving to next frame: 0x%08X\n", static_cast(hr)); return false; } DWORD waitResult = ::WaitForSingleObjectEx(FenceEvent, INFINITE, FALSE); if (waitResult != WAIT_OBJECT_0) { - Debug::Log("[RenderDX] Wait for fence event failed while moving to next frame with error code: %lu\n", GetLastError()); + Debug::Log("[RenderDX] Wait for fence event failed while moving to next frame with error code: 0x%08X\n", ::GetLastError()); return false; } } From 9d577620de81018d41be9956fefd643149ec98e5 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Sat, 16 May 2026 22:24:36 +0800 Subject: [PATCH 03/21] Disable bink movie render temporary --- src/Render/Hooks.Surface.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Render/Hooks.Surface.cpp b/src/Render/Hooks.Surface.cpp index 068cba82ac..182a966e0d 100644 --- a/src/Render/Hooks.Surface.cpp +++ b/src/Render/Hooks.Surface.cpp @@ -12,3 +12,16 @@ static DXSurface* __fastcall _DXSurface_CTOR(DXSurface* surface, void*, int widt } DEFINE_FUNCTION_JUMP(LJMP, 0x4BA5A0, _DXSurface_CTOR); +static int __stdcall _BinkDDSurfaceType(void*) +{ + return 10; // BINKSURFACE565 +} +DEFINE_PATCH_TYPED(void*, 0x7E15A8, _BinkDDSurfaceType); + +static int __stdcall _BinkCopyToBuffer(void* bnk, void* dest, int destpitch, unsigned int destheight, unsigned int destx, unsigned int desty, unsigned int flags) +{ + // Skip the bink movie render for now to avoid the crash. The movie will be rendered as black screen, but at least it won't crash. + // So we can test the other features without worrying about the movie rendering. We will try to implement the movie rendering later. + return 0; +} +DEFINE_PATCH_TYPED(void*, 0x7E15B8, _BinkCopyToBuffer); From 685c9f4773ed00c13625b272b4c4dd2d89327880 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Sat, 16 May 2026 22:36:51 +0800 Subject: [PATCH 04/21] Fix wrong resolution detection bpp --- src/Render/Functions.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Render/Functions.cpp b/src/Render/Functions.cpp index da3c6cb5ad..c5708b93ab 100644 --- a/src/Render/Functions.cpp +++ b/src/Render/Functions.cpp @@ -503,7 +503,7 @@ void __fastcall RenderDX::ResetScale() { ViewportY = 0.0f; } -int* __fastcall RenderDX::EnumDisplayModes(DWORD minw, DWORD minh, DWORD maxw, DWORD maxh, DWORD bitdepth) { +int* __fastcall RenderDX::EnumDisplayModes(DWORD minw, DWORD minh, DWORD maxw, DWORD maxh, DWORD) { std::vector> modes; DEVMODE devmode{}; DWORD mode_index = 0; @@ -513,7 +513,7 @@ int* __fastcall RenderDX::EnumDisplayModes(DWORD minw, DWORD minh, DWORD maxw, D const DWORD h = devmode.dmPelsHeight; const DWORD bpp = devmode.dmBitsPerPel; - if (w >= minw && h >= minh && w <= maxw && h <= maxh && bpp == bitdepth) { + if (w >= minw && h >= minh && w <= maxw && h <= maxh && bpp == 32) { modes.emplace_back(static_cast(w), static_cast(h)); } } From 4ce614566849940bb4d5a889a2dfd248a4a935f0 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Sat, 16 May 2026 22:37:04 +0800 Subject: [PATCH 05/21] Fix wrong PrimarySurface creation width and height --- src/Render/Surface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Render/Surface.cpp b/src/Render/Surface.cpp index 6ffc70bbac..11d56e2cbf 100644 --- a/src/Render/Surface.cpp +++ b/src/Render/Surface.cpp @@ -268,7 +268,7 @@ DXSurface* __fastcall DXSurface::CreatePrimary() Debug::Log("[RenderDX] D3D12 surface created as primary surface.\n"); - auto surface = new DXSurface(DSurface::WindowBounds.Width, DSurface::WindowBounds.Height); + auto surface = new DXSurface(Drawing::RenderWidth, Drawing::RenderHeight); // RGB565 color shifts Drawing::RedShiftLeft = 11; From 1f6a77c6e29e12982470d49e8f3486e05de6ded2 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Sun, 17 May 2026 12:37:25 +0800 Subject: [PATCH 06/21] Refine D3D12 device creation with preferred adapter and WARP fallbacks Prioritize hardware adapter selection using `IDXGIFactory6::EnumAdapterByGpuPreference` for optimal performance. Fall back to `IDXGIFactory4::EnumAdapters1` if `IDXGIFactory6` is unavailable. If no hardware device can be created, attempt to create a WARP device as a last resort. This improves robustness and compatibility across various systems. --- src/Render/Renderer.cpp | 63 ++++++++++++++++++++++++++++------------- src/Render/Renderer.h | 2 +- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/Render/Renderer.cpp b/src/Render/Renderer.cpp index 57c8156156..a5907e4fa6 100644 --- a/src/Render/Renderer.cpp +++ b/src/Render/Renderer.cpp @@ -572,41 +572,66 @@ bool DXRenderer::CreateDevice() { Debug::Log("[RenderDX] Failed to create DXGI factory: 0x%08X\n", static_cast(hr)); return false; } - constexpr bool kUseWarpDevice = false; - if (kUseWarpDevice) { - Microsoft::WRL::ComPtr warpAdapter; - if (FAILED(hr = Factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)))) { - Debug::Log("[RenderDX] Failed to create WARP adapter: 0x%08X\n", static_cast(hr)); - return false; - } - if (FAILED(hr = FP_D3D12CreateDevice(warpAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) { - Debug::Log("[RenderDX] Failed to create WARP adapter: 0x%08X\n", static_cast(hr)); - return false; - } - Debug::Log("[RenderDX] D3D12 WARP device created successfully.\n"); + Microsoft::WRL::ComPtr factory6; + if (FAILED(hr = Factory.As(&factory6))) + { + Debug::Log("[RenderDX] Failed to query IDXGIFactory6 interface: 0x%08X\nFalling back to EnumAdapters1()", static_cast(hr)); + Microsoft::WRL::ComPtr hardwareAdapter; + for (UINT adapterIndex = 0; Factory->EnumAdapters1(adapterIndex, &hardwareAdapter) != DXGI_ERROR_NOT_FOUND; ++adapterIndex) + { + DXGI_ADAPTER_DESC1 desc; + hardwareAdapter->GetDesc1(&desc); + if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) + { + continue; + } + if (SUCCEEDED(hr = FP_D3D12CreateDevice(hardwareAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) + { + Debug::Log("[RenderDX] D3D12 device created successfully on adapter: %ls\n", desc.Description); + break; + } + } } - else { + else + { + Debug::Log("[RenderDX] IDXGIFactory6 interface is available. Using EnumAdapterByGpuPreference to select the adapter.\n"); Microsoft::WRL::ComPtr hardwareAdapter; - for (UINT adapterIndex = 0; Factory->EnumAdapterByGpuPreference(adapterIndex, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&hardwareAdapter)) != DXGI_ERROR_NOT_FOUND; ++adapterIndex) { + for (UINT adapterIndex = 0; factory6->EnumAdapterByGpuPreference(adapterIndex, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&hardwareAdapter)) != DXGI_ERROR_NOT_FOUND; ++adapterIndex) + { DXGI_ADAPTER_DESC1 desc; hardwareAdapter->GetDesc1(&desc); - if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) { + if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) + { continue; } - if (SUCCEEDED(hr = FP_D3D12CreateDevice(hardwareAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) { + if (SUCCEEDED(hr = FP_D3D12CreateDevice(hardwareAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) + { Debug::Log("[RenderDX] D3D12 device created successfully on adapter: %ls\n", desc.Description); break; } } - if (!Device) { + } + if (!Device) + { + Debug::Log("[RenderDX] Failed to create D3D12 device on a hardware adapter. Attempting to create WARP device.\n"); + + Microsoft::WRL::ComPtr warpAdapter; + if (FAILED(hr = Factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)))) + { + Debug::Log("[RenderDX] Failed to create WARP adapter: 0x%08X\n", static_cast(hr)); + return false; + } + if (FAILED(hr = FP_D3D12CreateDevice(warpAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) + { + Debug::Log("[RenderDX] Failed to create WARP adapter: 0x%08X\n", static_cast(hr)); return false; } - Debug::Log("[RenderDX] D3D12 device created successfully.\n"); + Debug::Log("[RenderDX] D3D12 WARP device created successfully.\n"); } - return true; + return Device != nullptr; } bool DXRenderer::CreateCommandQueue() { diff --git a/src/Render/Renderer.h b/src/Render/Renderer.h index 791df077f3..cb89f04fe7 100644 --- a/src/Render/Renderer.h +++ b/src/Render/Renderer.h @@ -108,7 +108,7 @@ class DXRenderer { UINT FrameIndex { 0 }; UINT RtvDescriptorSize { 0 }; - Microsoft::WRL::ComPtr Factory; + Microsoft::WRL::ComPtr Factory; Microsoft::WRL::ComPtr Device; Microsoft::WRL::ComPtr CommandQueue; Microsoft::WRL::ComPtr SwapChain; From ef8a00390a1dadbc401edaa845ab5eb9d8afb27b Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Sun, 17 May 2026 23:32:43 +0800 Subject: [PATCH 07/21] Add AGENTS.md --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..4d54f91a34 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +.github/copilot-instructions.md From 8b0ddb1376ac7da3361302c13e5545558a7f41fd Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Sun, 17 May 2026 23:57:15 +0800 Subject: [PATCH 08/21] Switch to DirectX11 for compability --- Phobos.vcxproj | 5 +- src/Render/Functions.cpp | 225 +++---- src/Render/Functions.h | 10 +- src/Render/Hooks.Mouse.cpp | 29 +- src/Render/Hooks.Options.cpp | 9 +- src/Render/Hooks.Surface.cpp | 22 +- src/Render/Hooks.cpp | 139 ++-- src/Render/Mouse.cpp | 276 ++++---- src/Render/Mouse.h | 126 ++-- src/Render/Options.cpp | 0 src/Render/Options.h | 8 +- src/Render/Renderer.SurfacePS.h | 81 +++ src/Render/Renderer.SurfaceVS.h | 145 +++++ src/Render/Renderer.cpp | 1048 +++++++++++-------------------- src/Render/Renderer.h | 120 ++-- src/Render/Surface.cpp | 269 ++++---- src/Render/Surface.h | 49 +- 17 files changed, 1181 insertions(+), 1380 deletions(-) delete mode 100644 src/Render/Options.cpp create mode 100644 src/Render/Renderer.SurfacePS.h create mode 100644 src/Render/Renderer.SurfaceVS.h diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 3ea12b25cf..d218024677 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -271,7 +271,6 @@ - @@ -401,6 +400,8 @@ + + @@ -427,4 +428,4 @@ - \ No newline at end of file + diff --git a/src/Render/Functions.cpp b/src/Render/Functions.cpp index c5708b93ab..ef25fe802a 100644 --- a/src/Render/Functions.cpp +++ b/src/Render/Functions.cpp @@ -15,9 +15,10 @@ #include #include +#include #include -bool __fastcall RenderDX::AllocateSurfaces(const RectangleStruct& hidden_rect, const RectangleStruct& composite_rect, const RectangleStruct& tile_rect, const RectangleStruct& sidebar_rect, bool hidden_first) { +bool __fastcall RenderDX::AllocateSurfaces(const RectangleStruct& hiddenRect, const RectangleStruct& compositeRect, const RectangleStruct& tileRect, const RectangleStruct& sidebarRect, bool hiddenFirst) { Debug::Log("[RenderDX] Allocating new surfaces\n"); if (DSurface::Alternate) { @@ -50,47 +51,47 @@ bool __fastcall RenderDX::AllocateSurfaces(const RectangleStruct& hidden_rect, c DSurface::Sidebar = nullptr; } - if (hidden_first && hidden_rect.Width > 0 && hidden_rect.Height > 0) { - DSurface::Hidden = GameCreate(hidden_rect.Width, hidden_rect.Height); + if (hiddenFirst && hiddenRect.Width > 0 && hiddenRect.Height > 0) { + DSurface::Hidden = GameCreate(hiddenRect.Width, hiddenRect.Height); DSurface::Hidden->Fill(0); - Debug::Log("[RenderDX] HiddenSurface (%dx%d)\n", hidden_rect.Width, hidden_rect.Height); + Debug::Log("[RenderDX] HiddenSurface (%dx%d)\n", hiddenRect.Width, hiddenRect.Height); } - if (composite_rect.Width > 0 && composite_rect.Height > 0) { - DSurface::Composite = GameCreate(composite_rect.Width, composite_rect.Height); + if (compositeRect.Width > 0 && compositeRect.Height > 0) { + DSurface::Composite = GameCreate(compositeRect.Width, compositeRect.Height); DSurface::Composite->Fill(0); - Debug::Log("[RenderDX] CompositeSurface (%dx%d)\n", composite_rect.Width, composite_rect.Height); + Debug::Log("[RenderDX] CompositeSurface (%dx%d)\n", compositeRect.Width, compositeRect.Height); } - if (tile_rect.Width > 0 && tile_rect.Height > 0) { - DSurface::Tile = GameCreate(tile_rect.Width, tile_rect.Height); + if (tileRect.Width > 0 && tileRect.Height > 0) { + DSurface::Tile = GameCreate(tileRect.Width, tileRect.Height); DSurface::Tile->Fill(0); - Debug::Log("[RenderDX] TileSurface (%dx%d)\n", tile_rect.Width, tile_rect.Height); + Debug::Log("[RenderDX] TileSurface (%dx%d)\n", tileRect.Width, tileRect.Height); } - if (sidebar_rect.Width > 0 && sidebar_rect.Height > 0) { - DSurface::Sidebar = GameCreate(sidebar_rect.Width, sidebar_rect.Height); + if (sidebarRect.Width > 0 && sidebarRect.Height > 0) { + DSurface::Sidebar = GameCreate(sidebarRect.Width, sidebarRect.Height); DSurface::Sidebar->Fill(0); - Debug::Log("[RenderDX] SidebarSurface (%dx%d)\n", sidebar_rect.Width, sidebar_rect.Height); + Debug::Log("[RenderDX] SidebarSurface (%dx%d)\n", sidebarRect.Width, sidebarRect.Height); } - if (!hidden_first && hidden_rect.Width > 0 && hidden_rect.Height > 0) { - DSurface::Hidden = GameCreate(hidden_rect.Width, hidden_rect.Height); + if (!hiddenFirst && hiddenRect.Width > 0 && hiddenRect.Height > 0) { + DSurface::Hidden = GameCreate(hiddenRect.Width, hiddenRect.Height); DSurface::Hidden->Fill(0); - Debug::Log("[RenderDX] HiddenSurface (%dx%d)\n", hidden_rect.Width, hidden_rect.Height); + Debug::Log("[RenderDX] HiddenSurface (%dx%d)\n", hiddenRect.Width, hiddenRect.Height); } - if (hidden_rect.Width > 0 && hidden_rect.Height > 0) { - DSurface::Alternate = GameCreate(hidden_rect.Width, hidden_rect.Height); + if (hiddenRect.Width > 0 && hiddenRect.Height > 0) { + DSurface::Alternate = GameCreate(hiddenRect.Width, hiddenRect.Height); DSurface::Alternate->Fill(0); - Debug::Log("[RenderDX] AlternateSurface (%dx%d)\n", hidden_rect.Width, hidden_rect.Height); + Debug::Log("[RenderDX] AlternateSurface (%dx%d)\n", hiddenRect.Width, hiddenRect.Height); } return true; } -bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bits_per_pixel) { - Debug::Log("[RenderDX] Setting video mode to %dx%d@%d\n", width, height, bits_per_pixel); +bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bitsPerPixel) { + Debug::Log("[RenderDX] Setting video mode to %dx%d@%d\n", width, height, bitsPerPixel); if (!DXRenderer::Instance().IsRendererReady()) { Debug::Log("[RenderDX] Renderer is not ready\n"); @@ -98,14 +99,14 @@ bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bits_per } ResetVideoMode(); - if (!DXRenderer::Instance().CreateRenderer(width, height, bits_per_pixel)) { + if (!DXRenderer::Instance().CreateRenderer(width, height, bitsPerPixel)) { Debug::Log("[RenderDX] Failed to create renderer\n"); return false; } Drawing::RenderWidth = width; Drawing::RenderHeight = height; - Drawing::RenderBitsPerPixel = bits_per_pixel; + Drawing::RenderBitsPerPixel = bitsPerPixel; RenderDX::UpdateScale(); @@ -133,9 +134,9 @@ static void RecalcMouseWindowRegion(bool rebuildCursor) { if (!DXMouse::Instance) return; - DXMouse::Instance->Recalc_Capture_Region(); + DXMouse::Instance->RecalcCaptureRegion(); if (rebuildCursor) - DXMouse::Instance->Rebuild_Cursor_Image(); + DXMouse::Instance->RebuildCursorImage(); } static void ApplyWindowResize(int width, int height) { @@ -143,8 +144,8 @@ static void ApplyWindowResize(int width, int height) { RecalcMouseWindowRegion(true); } -static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { - switch (msg) { +static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + switch (message) { case WM_MOUSEMOVE: case WM_LBUTTONDOWN: case WM_LBUTTONUP: @@ -161,13 +162,13 @@ static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARA { // Scale mouse inputs before they are processed by SDL or the game. if (RenderDX::ShouldScale()) { - int x = GET_X_LPARAM(lparam); - int y = GET_Y_LPARAM(lparam); + int x = GET_X_LPARAM(lParam); + int y = GET_Y_LPARAM(lParam); x = RenderDX::ClientToRenderX(x); y = RenderDX::ClientToRenderY(y); - lparam = MAKELPARAM(x, y); + lParam = MAKELPARAM(x, y); } break; } @@ -175,7 +176,7 @@ static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARA case WM_MOVE: { if (DXMouse::Instance) { - DXMouse::Instance->Recalc_Capture_Region(); + DXMouse::Instance->RecalcCaptureRegion(); } return 0; // handled } @@ -198,7 +199,7 @@ static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARA int height = DeferredWindowHeight; RECT clientRect {}; - if (::GetClientRect(hwnd, &clientRect)) { + if (::GetClientRect(hWnd, &clientRect)) { width = clientRect.right - clientRect.left; height = clientRect.bottom - clientRect.top; } @@ -216,9 +217,9 @@ static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARA case WM_SIZE: { - const int width = LOWORD(lparam); - const int height = HIWORD(lparam); - if (wparam == SIZE_MINIMIZED || width == 0 || height == 0) { + const int width = LOWORD(lParam); + const int height = HIWORD(lParam); + if (wParam == SIZE_MINIMIZED || width == 0 || height == 0) { DeferredWindowResize = false; RecalcMouseWindowRegion(false); } @@ -228,7 +229,7 @@ static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARA DeferredWindowHeight = height; RecalcMouseWindowRegion(false); } - else { + else { ApplyWindowResize(width, height); } @@ -238,11 +239,11 @@ static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARA case WM_SYSKEYDOWN: { // Handle Alt+Enter for fullscreen toggle - if (wparam == VK_RETURN && (lparam & (1 << 29))) { + if (wParam == VK_RETURN && (lParam & (1 << 29))) { DXRenderer::Instance().ToggleFullscreen(); if (DXMouse::Instance) { - DXMouse::Instance->Recalc_Capture_Region(); - DXMouse::Instance->Rebuild_Cursor_Image(); + DXMouse::Instance->RecalcCaptureRegion(); + DXMouse::Instance->RebuildCursorImage(); } return 0; // handled } @@ -252,9 +253,9 @@ static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARA case WM_SETCURSOR: { // Prevent the system from setting the cursor when it's over our window, since we handle it ourselves. - if (LOWORD(lparam) == HTCLIENT) { + if (LOWORD(lParam) == HTCLIENT) { if (DXMouse::Instance) - DXMouse::Instance->Set_Cached_Cursor(); + DXMouse::Instance->SetCachedCursor(); return TRUE; // handled } break; @@ -262,19 +263,19 @@ static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARA case WM_ACTIVATEAPP: { - if (DXRenderOptions::Config().PauseGameWhenLoseFocus) + if (RenderOptions::Config().PauseGameWhenLoseFocus) break; // goto the original window procedure to allow the game to pause when losing focus - if (hwnd == Game::hWnd) { - Unsorted::ScenarioInit = true; // game is always active - if (wparam) { + if (hWnd == Game::hWnd) { + Unsorted::GameInFocus = true; // game is always active + if (wParam) { Debug::Log("[RenderDX] Game window activated\n"); if (DXMouse::Instance) - DXMouse::Instance->Capture_Mouse(); + DXMouse::Instance->CaptureMouse(); } else { Debug::Log("[RenderDX] Game window deactivated\n"); if (DXMouse::Instance) - DXMouse::Instance->Release_Mouse(); + DXMouse::Instance->ReleaseMouse(); } } return 0; // handled - prevent the game from pausing when the window is deactivated @@ -282,12 +283,12 @@ static LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARA } // Call original window procedure for default processing - return reinterpret_cast(0x7775C0)(hwnd, msg, wparam, lparam); + return reinterpret_cast(0x7775C0)(hWnd, message, wParam, lParam); } -void __fastcall RenderDX::CreateMainWindow(HINSTANCE instance, int cmd_show, int width, int height) { +void __fastcall RenderDX::CreateMainWindow(HINSTANCE instance, int cmdShow, int width, int height) { Debug::Log("[RenderDX] Creating main window\n"); - if (!DXRenderer::Instance().CreateMainWindow(instance, cmd_show, width, height, MainWindowProc)) { + if (!DXRenderer::Instance().CreateMainWindow(instance, cmdShow, width, height, MainWindowProc)) { Debug::Log("[RenderDX] Failed to create main window\n"); ::MessageBoxA(nullptr, "Failed to create main window", "Error", MB_ICONERROR); ::ExitProcess(0xC0DEBEEF); @@ -299,8 +300,8 @@ void __fastcall RenderDX::DestroyMainWindow() { DXRenderer::Instance().DestroyMainWindow(); } -bool __fastcall RenderDX::UpdateScreen(Surface* surface) { - if (!surface) { +bool __fastcall RenderDX::UpdateScreen(Surface* pSurface) { + if (!pSurface) { Debug::Log("[RenderDX] UpdateScreen called with null surface\n"); return false; } @@ -309,13 +310,13 @@ bool __fastcall RenderDX::UpdateScreen(Surface* surface) { DXRenderer::Instance().SetRenderScale(shouldScale); // Retrieve the game surface data - if (void* pixels = surface->Lock(0, 0)) { - if (!DXRenderer::Instance().UploadSurfaceToTexture(pixels, surface->GetPitch())) { + if (void* pPixels = pSurface->Lock(0, 0)) { + if (!DXRenderer::Instance().UploadSurfaceToTexture(pPixels, pSurface->GetPitch())) { Debug::Log("[RenderDX] Failed to upload surface to texture\n"); - surface->Unlock(); + pSurface->Unlock(); return false; } - surface->Unlock(); + pSurface->Unlock(); } static bool scaled = ShouldScale(); @@ -324,7 +325,7 @@ bool __fastcall RenderDX::UpdateScreen(Surface* surface) { if (scaled != shouldScale) { scaled = shouldScale; if (DXMouse::Instance) - DXMouse::Instance->Rebuild_Cursor_Image(); + DXMouse::Instance->RebuildCursorImage(); } DXRenderer::Instance().Present(); @@ -336,62 +337,62 @@ bool __fastcall RenderDX::ShouldScale() { return Unsorted::SpecialDialog == 0 && Unsorted::WSDialogCount == 0; } -static void RebuildDisplayState(const RectangleStruct& view_rect) { - auto temp = view_rect; - temp.X = GameOptionsClass::Instance.SidebarMode ? 0 : 168; - temp.Y = 16; - temp.Width -= 168; - temp.Height -= 16; +static void RebuildDisplayState(const RectangleStruct& viewRect) { + auto sidebarRect = viewRect; + sidebarRect.X = GameOptionsClass::Instance.SidebarMode ? 0 : 168; + sidebarRect.Y = 16; + sidebarRect.Width -= 168; + sidebarRect.Height -= 16; - DSurface::ViewBounds = view_rect; - Drawing::RenderWidth = view_rect.Width; - Drawing::RenderHeight = view_rect.Height; + DSurface::ViewBounds = viewRect; + Drawing::RenderWidth = viewRect.Width; + Drawing::RenderHeight = viewRect.Height; DSurface::Primary = DXSurface::CreatePrimary(); RenderDX::AllocateSurfaces( - view_rect, - RectangleStruct { 0,0,temp.Width,view_rect.Height }, - RectangleStruct { 0,0,temp.Width,view_rect.Height }, - RectangleStruct { 0,0,168,view_rect.Height }, + viewRect, + RectangleStruct { 0, 0, sidebarRect.Width, viewRect.Height }, + RectangleStruct { 0, 0, sidebarRect.Width, viewRect.Height }, + RectangleStruct { 0, 0, 168, viewRect.Height }, false ); DSurface::Temp = DSurface::Hidden; if (DXMouse::Instance) { - DXMouse::Instance->Rebuild_Cursor_Image(); + DXMouse::Instance->RebuildCursorImage(); } - SidebarClass::Instance.Set_View_Dimensions(temp); + SidebarClass::Instance.Set_View_Dimensions(sidebarRect); SidebarClass::Instance.Init_IO(); SidebarClass::Instance.Activate(1); SidebarClass::Instance.InitGUI(); SidebarClass::Instance.MarkNeedsRedraw(2); // REDRAW_ALL - DXMouse::Instance->Show_Mouse(); + DXMouse::Instance->ShowMouse(); } bool __fastcall RenderDX::ChangeDisplayMode(int width, int height) { Debug::Log("[RenderDX] Changing display mode to %dx%d\n", width, height); // Save current window position - RectangleStruct old_rect = DSurface::ViewBounds; - if (old_rect.Width <= 0 || old_rect.Height <= 0) { + RectangleStruct oldRect = DSurface::ViewBounds; + if (oldRect.Width <= 0 || oldRect.Height <= 0) { if (Drawing::RenderWidth > 0 && Drawing::RenderHeight > 0) { Debug::Log("[RenderDX] Current view bounds are invalid, using RenderWidth/RenderHeight\n"); - old_rect = RectangleStruct { 0, 0, Drawing::RenderWidth, Drawing::RenderHeight }; + oldRect = RectangleStruct { 0, 0, Drawing::RenderWidth, Drawing::RenderHeight }; } } - const int old_render_width = Drawing::RenderWidth; - const int old_render_height = Drawing::RenderHeight; - const int old_render_bpp = Drawing::RenderBitsPerPixel; + const int oldRenderWidth = Drawing::RenderWidth; + const int oldRenderHeight = Drawing::RenderHeight; + const int oldRenderBpp = Drawing::RenderBitsPerPixel; - int old_window_x = 0; - int old_window_y = 0; - int old_window_width = DXRenderer::Instance().GetWindowWidth(); - int old_window_height = DXRenderer::Instance().GetWindowHeight(); + int oldWindowX = 0; + int oldWindowY = 0; + int oldWindowWidth = DXRenderer::Instance().GetWindowWidth(); + int oldWindowHeight = DXRenderer::Instance().GetWindowHeight(); - DXMouse::Instance->Hide_Mouse(); + DXMouse::Instance->HideMouse(); // Delete the old primary surface if (DSurface::Primary) { @@ -401,53 +402,53 @@ bool __fastcall RenderDX::ChangeDisplayMode(int width, int height) { } if (DXRenderer::Instance().IsWindowed()) { - int window_width = width; - int window_height = height; + int windowWidth = width; + int windowHeight = height; RECT temp; ::GetWindowRect(Game::hWnd, &temp); - old_window_x = temp.left; - old_window_y = temp.top; - old_window_width = temp.right - temp.left; - old_window_height = temp.bottom - temp.top; + oldWindowX = temp.left; + oldWindowY = temp.top; + oldWindowWidth = temp.right - temp.left; + oldWindowHeight = temp.bottom - temp.top; + + int centerX = oldWindowX + oldWindowWidth / 2; + int centerY = oldWindowY + oldWindowHeight / 2; - int center_x = old_window_x + old_window_width / 2; - int center_y = old_window_y + old_window_height / 2; - - int new_x = center_x - window_width / 2; - int new_y = center_y - window_height / 2; + int newX = centerX - windowWidth / 2; + int newY = centerY - windowHeight / 2; - DXRenderer::Instance().MoveWindow(new_x, new_y, window_width, window_height); + DXRenderer::Instance().MoveWindow(newX, newY, windowWidth, windowHeight); - Debug::Log("[RenderDX] Moved window to (%d, %d) with size %dx%d\n", new_x, new_y, window_width, window_height); + Debug::Log("[RenderDX] Moved window to (%d, %d) with size %dx%d\n", newX, newY, windowWidth, windowHeight); } // Recreate all intermediates if (!SetVideoMode(Game::hWnd, width, height, 16)) { if (DXRenderer::Instance().IsWindowed()) { - DXRenderer::Instance().MoveWindow(old_window_x, old_window_y, old_window_width, old_window_height); - Debug::Log("[RenderDX] Restore window to (%d, %d) with size %dx%d\n", old_window_x, old_window_y, old_window_width, old_window_height); + DXRenderer::Instance().MoveWindow(oldWindowX, oldWindowY, oldWindowWidth, oldWindowHeight); + Debug::Log("[RenderDX] Restore window to (%d, %d) with size %dx%d\n", oldWindowX, oldWindowY, oldWindowWidth, oldWindowHeight); } - if (old_rect.X > 0 && old_rect.Y > 0 && old_render_width > 0 && old_render_height > 0) { + if (oldRect.X > 0 && oldRect.Y > 0 && oldRenderWidth > 0 && oldRenderHeight > 0) { Debug::Log("[RenderDX] Restoring old display mode.\n"); - if (!SetVideoMode(Game::hWnd, old_render_width, old_render_height, old_render_bpp)) { + if (!SetVideoMode(Game::hWnd, oldRenderWidth, oldRenderHeight, oldRenderBpp)) { Debug::Log("[RenderDX] Failed to restore old display mode.\n"); - DXMouse::Instance->Show_Mouse(); + DXMouse::Instance->ShowMouse(); return false; } - RebuildDisplayState(old_rect); + RebuildDisplayState(oldRect); } else { Debug::Log("[RenderDX] Old view bounds are invalid, cannot restore\n"); } - DXMouse::Instance->Show_Mouse(); + DXMouse::Instance->ShowMouse(); return false; } - RectangleStruct new_view_rect = { 0, 0, width, height }; - RebuildDisplayState(new_view_rect); + RectangleStruct newViewRect = { 0, 0, width, height }; + RebuildDisplayState(newViewRect); Debug::Log("[RenderDX]: ViewBounds: %dx%d\n", width, height); Debug::Log("[RenderDX] Mode change complete.\n"); @@ -503,17 +504,17 @@ void __fastcall RenderDX::ResetScale() { ViewportY = 0.0f; } -int* __fastcall RenderDX::EnumDisplayModes(DWORD minw, DWORD minh, DWORD maxw, DWORD maxh, DWORD) { +int* __fastcall RenderDX::EnumDisplayModes(DWORD minWidth, DWORD minHeight, DWORD maxWidth, DWORD maxHeight, DWORD) { std::vector> modes; DEVMODE devmode{}; - DWORD mode_index = 0; + DWORD modeIndex = 0; - while (::EnumDisplaySettingsA(nullptr, mode_index++, &devmode)) { + while (::EnumDisplaySettingsA(nullptr, modeIndex++, &devmode)) { const DWORD w = devmode.dmPelsWidth; const DWORD h = devmode.dmPelsHeight; const DWORD bpp = devmode.dmBitsPerPel; - if (w >= minw && h >= minh && w <= maxw && h <= maxh && bpp == 32) { + if (w >= minWidth && h >= minHeight && w <= maxWidth && h <= maxHeight && bpp == 32) { modes.emplace_back(static_cast(w), static_cast(h)); } } @@ -543,14 +544,14 @@ int* __fastcall RenderDX::EnumDisplayModes(DWORD minw, DWORD minh, DWORD maxw, D void __fastcall RenderDX::MainProcHandlePaint() { if (DXMouse::Instance && DSurface::Primary && DSurface::Hidden && DSurface::Composite) { if (Unsorted::ScenarioStarted) { - GScreenClass::UpdatePrimarySurface(DXMouse::Instance->Is_Captured(), DSurface::Composite, nullptr); + GScreenClass::UpdatePrimarySurface(DXMouse::Instance->IsCaptured(), DSurface::Composite, nullptr); SidebarClass::Instance.BlitSidebar(true); } else if (Game::IsMoviePlaying()) { Game::BlitMovie(); } else { - GScreenClass::UpdatePrimarySurface(DXMouse::Instance->Is_Captured(), DSurface::Hidden, nullptr); + GScreenClass::UpdatePrimarySurface(DXMouse::Instance->IsCaptured(), DSurface::Hidden, nullptr); } } } diff --git a/src/Render/Functions.h b/src/Render/Functions.h index 8f97794811..6d679a00e8 100644 --- a/src/Render/Functions.h +++ b/src/Render/Functions.h @@ -6,12 +6,12 @@ class Surface; class RenderDX { public: - static bool __fastcall AllocateSurfaces(const RectangleStruct& hidden_rect, const RectangleStruct& composite_rect, const RectangleStruct& tile_rect, const RectangleStruct& sidebar_rect, bool hidden_first); - static bool __fastcall SetVideoMode(HWND, int width, int height, int bits_per_pixel); + static bool __fastcall AllocateSurfaces(const RectangleStruct& hiddenRect, const RectangleStruct& compositeRect, const RectangleStruct& tileRect, const RectangleStruct& sidebarRect, bool hiddenFirst); + static bool __fastcall SetVideoMode(HWND, int width, int height, int bitsPerPixel); static void __fastcall ResetVideoMode(); - static void __fastcall CreateMainWindow(HINSTANCE instance, int cmd_show, int width, int height); + static void __fastcall CreateMainWindow(HINSTANCE instance, int cmdShow, int width, int height); static void __fastcall DestroyMainWindow(); - static bool __fastcall UpdateScreen(Surface* surface); + static bool __fastcall UpdateScreen(Surface* pSurface); static bool __fastcall ShouldScale(); static bool __fastcall ChangeDisplayMode(int width, int height); static float __fastcall GetXScale(); @@ -20,6 +20,6 @@ class RenderDX { static int __fastcall ClientToRenderY(int y); static void __fastcall UpdateScale(); static void __fastcall ResetScale(); - static int* __fastcall EnumDisplayModes(DWORD minw, DWORD minh, DWORD maxw, DWORD maxh, DWORD bitdepth); + static int* __fastcall EnumDisplayModes(DWORD minWidth, DWORD minHeight, DWORD maxWidth, DWORD maxHeight, DWORD bitDepth); static void __fastcall MainProcHandlePaint(); }; diff --git a/src/Render/Hooks.Mouse.cpp b/src/Render/Hooks.Mouse.cpp index c60153197c..e0a4ae9b44 100644 --- a/src/Render/Hooks.Mouse.cpp +++ b/src/Render/Hooks.Mouse.cpp @@ -1,6 +1,5 @@ #include "Mouse.h" -#include #include #include @@ -9,7 +8,7 @@ #include -DEFINE_HOOK(0x6BDEF9, WinMain_CreateWWMouse, 0x5) { +DEFINE_HOOK(0x6BDEF9, WinMainCreateWWMouse, 0x5) { DXMouse::Instance = GameCreate(DSurface::Primary, Game::hWnd); R->EAX(DXMouse::Instance); return 0x6BDF25; @@ -18,25 +17,25 @@ DEFINE_HOOK(0x6BDEF9, WinMain_CreateWWMouse, 0x5) { static HANDLE MouseThread; static std::binary_semaphore MouseThreadSemaphore { 0 }; -static DWORD WINAPI _MouseThread(LPVOID) { +static DWORD WINAPI MouseThreadProc(LPVOID) { while (!MouseThreadSemaphore.try_acquire_for(std::chrono::milliseconds(10))) { if (DXMouse::Instance) - DXMouse::Instance->Process_Mouse(); + DXMouse::Instance->ProcessMouse(); } return 0; } -static void __fastcall _DXMouse_StartMouseThread() { - MouseThread = ::CreateThread(nullptr, 0, _MouseThread, nullptr, 0, nullptr); +static void __fastcall DXMouseStartMouseThread() { + MouseThread = ::CreateThread(nullptr, 0, MouseThreadProc, nullptr, 0, nullptr); if (!MouseThread) { MouseThreadSemaphore.release(); return; } ::SetThreadPriority(MouseThread, THREAD_PRIORITY_TIME_CRITICAL); } -DEFINE_FUNCTION_JUMP(LJMP, 0x7B84F0, _DXMouse_StartMouseThread); +DEFINE_FUNCTION_JUMP(LJMP, 0x7B84F0, DXMouseStartMouseThread); -static void __fastcall _DXMouse_EndMouseThread() { +static void __fastcall DXMouseEndMouseThread() { MouseThreadSemaphore.release(); if (MouseThread) { ::WaitForSingleObject(MouseThread, INFINITE); @@ -44,17 +43,17 @@ static void __fastcall _DXMouse_EndMouseThread() { MouseThread = nullptr; } } -DEFINE_FUNCTION_JUMP(LJMP, 0x7B86B0, _DXMouse_EndMouseThread); +DEFINE_FUNCTION_JUMP(LJMP, 0x7B86B0, DXMouseEndMouseThread); -static void __fastcall _DXMouse_ProcessMouse(DXMouse* This) { - This->Process_Mouse(); +static void __fastcall DXMouseProcessMouse(DXMouse* pThis) { + pThis->ProcessMouse(); } -DEFINE_FUNCTION_JUMP(LJMP, 0x7BA090, _DXMouse_ProcessMouse); +DEFINE_FUNCTION_JUMP(LJMP, 0x7BA090, DXMouseProcessMouse); -DEFINE_HOOK_AGAIN(0x72429E, DXMouse_TooltipManager_GetMousePosition, 0xA); -DEFINE_HOOK(0x724359, DXMouse_TooltipManager_GetMousePosition, 0xA) { +DEFINE_HOOK_AGAIN(0x72429E, DXMouseTooltipManagerGetMousePosition, 0xA); +DEFINE_HOOK(0x724359, DXMouseTooltipManagerGetMousePosition, 0xA) { GET(ToolTipManager*, pThis, ESI); - pThis->CurrentMousePosition = DXMouse::Instance->Get_Mouse_Point(); + pThis->CurrentMousePosition = DXMouse::Instance->GetMousePoint(); R->EBX(&pThis->CurrentMousePosition); return R->Origin() + 0x15; } diff --git a/src/Render/Hooks.Options.cpp b/src/Render/Hooks.Options.cpp index e25f51c332..0880487c65 100644 --- a/src/Render/Hooks.Options.cpp +++ b/src/Render/Hooks.Options.cpp @@ -1,12 +1,11 @@ -#include - #include "Options.h" +#include + #include -DEFINE_HOOK(0x6BC141, DXRender_LoadConfigFromRA2MD, 0x7) -{ - auto& config = DXRenderOptions::Config(); +DEFINE_HOOK(0x6BC141, RenderConfig_LoadRA2MD, 0x7) { + auto& config = RenderOptions::Config(); config.PreserveAspectRatio = CCINIClass::INI_RA2MD.ReadBool("DXRender", "PreserveAspectRatio", config.PreserveAspectRatio); config.WindowedBorder = CCINIClass::INI_RA2MD.ReadBool("DXRender", "WindowedBorder", config.WindowedBorder); config.StartFullscreen = CCINIClass::INI_RA2MD.ReadBool("DXRender", "StartFullscreen", config.StartFullscreen); diff --git a/src/Render/Hooks.Surface.cpp b/src/Render/Hooks.Surface.cpp index 182a966e0d..aadf56bf59 100644 --- a/src/Render/Hooks.Surface.cpp +++ b/src/Render/Hooks.Surface.cpp @@ -1,27 +1,17 @@ #include "Surface.h" -#include #include DEFINE_FUNCTION_JUMP(LJMP, 0x4BA770, DXSurface::CreatePrimary); -DEFINE_JUMP(LJMP, 0x77747A, 0x777575); // Skip Restore_Check +DEFINE_JUMP(LJMP, 0x77747A, 0x777575); // Skip Restore_Check. -static DXSurface* __fastcall _DXSurface_CTOR(DXSurface* surface, void*, int width, int height, bool system_mem, bool enable_3d) { - return new(surface) DXSurface(width, height); +static DXSurface* __fastcall DXSurfaceCtor(DXSurface* pSurface, void*, int width, int height, bool systemMem, bool enable3D) { + return new(pSurface) DXSurface(width, height); } -DEFINE_FUNCTION_JUMP(LJMP, 0x4BA5A0, _DXSurface_CTOR); +DEFINE_FUNCTION_JUMP(LJMP, 0x4BA5A0, DXSurfaceCtor); -static int __stdcall _BinkDDSurfaceType(void*) -{ +static int __stdcall BinkDDSurfaceType(void*) { return 10; // BINKSURFACE565 } -DEFINE_PATCH_TYPED(void*, 0x7E15A8, _BinkDDSurfaceType); - -static int __stdcall _BinkCopyToBuffer(void* bnk, void* dest, int destpitch, unsigned int destheight, unsigned int destx, unsigned int desty, unsigned int flags) -{ - // Skip the bink movie render for now to avoid the crash. The movie will be rendered as black screen, but at least it won't crash. - // So we can test the other features without worrying about the movie rendering. We will try to implement the movie rendering later. - return 0; -} -DEFINE_PATCH_TYPED(void*, 0x7E15B8, _BinkCopyToBuffer); +DEFINE_PATCH_TYPED(void*, 0x7E15A8, BinkDDSurfaceType); diff --git a/src/Render/Hooks.cpp b/src/Render/Hooks.cpp index 5697bb3de8..02e66529f3 100644 --- a/src/Render/Hooks.cpp +++ b/src/Render/Hooks.cpp @@ -1,41 +1,37 @@ #include "Functions.h" -#include #include #include +#include #include #include -#include #ifdef CALL #undef CALL #endif -// Main window creation DEFINE_FUNCTION_JUMP(LJMP, 0x777C30, RenderDX::CreateMainWindow); -static BOOL WINAPI _ClientToScreen(HWND hWnd, LPPOINT lpPoint) { +static BOOL WINAPI ClientToScreenHook(HWND hWnd, LPPOINT lpPoint) { return TRUE; } -// Disable ClientToScreen -DEFINE_PATCH_TYPED(void*, 0x7E14B8, _ClientToScreen); +DEFINE_PATCH_TYPED(void*, 0x7E14B8, ClientToScreenHook); -// But these 3 need to use the real ClientToScreen so that dialogs are where they should be. static void __fastcall CenterWindowIn(HWND window, HWND parent) { - RECT rcl; - ::GetClientRect(parent, &rcl); + RECT parentRect; + ::GetClientRect(parent, &parentRect); if (parent == Game::hWnd) { - rcl.right = Drawing::RenderWidth; - rcl.bottom = Drawing::RenderHeight; + parentRect.right = Drawing::RenderWidth; + parentRect.bottom = Drawing::RenderHeight; } - ::ClientToScreen(parent, reinterpret_cast(&rcl)); - ::ClientToScreen(parent, reinterpret_cast(&rcl.right)); - rcl.right -= rcl.left; - rcl.bottom -= rcl.top; + ::ClientToScreen(parent, reinterpret_cast(&parentRect)); + ::ClientToScreen(parent, reinterpret_cast(&parentRect.right)); + parentRect.right -= parentRect.left; + parentRect.bottom -= parentRect.top; RECT rect; ::GetClientRect(window, &rect); @@ -43,8 +39,8 @@ static void __fastcall CenterWindowIn(HWND window, HWND parent) { ::ClientToScreen(window, reinterpret_cast(&rect.right)); rect.right -= rect.left; rect.bottom -= rect.top; - int x = (rcl.right - rect.right + 1) / 2; - int y = (rcl.bottom - rect.bottom + 1) / 2; + int x = (parentRect.right - rect.right + 1) / 2; + int y = (parentRect.bottom - rect.bottom + 1) / 2; x = std::max(x, 0); y = std::max(y, 0); @@ -53,46 +49,42 @@ static void __fastcall CenterWindowIn(HWND window, HWND parent) { } DEFINE_FUNCTION_JUMP(LJMP, 0x777080, CenterWindowIn); -static BOOL __fastcall ODMoveDialog(HWND window, int x, int y) { - int xpos; - int ypos; +static BOOL __fastcall MoveDialog(HWND window, int x, int y) { + int xPos; + int yPos; - RECT rect1; - rect1.left = 0; - rect1.top = 0; - rect1.right = *reinterpret_cast(0x8A00A4); - rect1.bottom = *reinterpret_cast(0x8A00A8); + RECT screenRect; + screenRect.left = 0; + screenRect.top = 0; + screenRect.right = Drawing::RenderWidth; + screenRect.bottom = Drawing::RenderHeight; - ::ClientToScreen(Game::hWnd, reinterpret_cast(&rect1)); - ::ClientToScreen(Game::hWnd, reinterpret_cast(&rect1.right)); + ::ClientToScreen(Game::hWnd, reinterpret_cast(&screenRect)); + ::ClientToScreen(Game::hWnd, reinterpret_cast(&screenRect.right)); - RECT rect2; - ::GetWindowRect(window, &rect2); + RECT windowRect; + ::GetWindowRect(window, &windowRect); - rect2.right -= rect2.left; - rect2.bottom -= rect2.top; + windowRect.right -= windowRect.left; + windowRect.bottom -= windowRect.top; - if (x == -1) { - xpos = rect2.left - rect1.left; - } - else { - xpos = x; - } - rect2.left = xpos; + if (x == -1) + xPos = windowRect.left - screenRect.left; + else + xPos = x; + windowRect.left = xPos; - if (y == -1) { - ypos = rect2.top - rect1.top; - } - else { - ypos = y; - } - rect2.top = ypos; + if (y == -1) + yPos = windowRect.top - screenRect.top; + else + yPos = y; + windowRect.top = yPos; - return ::MoveWindow(window, rect2.left, rect2.top, rect2.right, rect2.bottom, FALSE); + return ::MoveWindow(window, windowRect.left, windowRect.top, windowRect.right, windowRect.bottom, FALSE); } -DEFINE_FUNCTION_JUMP(LJMP, 0x623170, ODMoveDialog); +DEFINE_FUNCTION_JUMP(LJMP, 0x623170, MoveDialog); -static BOOL __fastcall WinDialog_GetRectangle(HWND hWnd, LPRECT rect) { +static BOOL __fastcall WinDialogGetRectangle(HWND hWnd, LPRECT rect) { BOOL result = ::GetWindowRect(hWnd, rect); if (result) { RECT client; @@ -105,16 +97,14 @@ static BOOL __fastcall WinDialog_GetRectangle(HWND hWnd, LPRECT rect) { } return result; } -DEFINE_FUNCTION_JUMP(LJMP, 0x775690, WinDialog_GetRectangle); +DEFINE_FUNCTION_JUMP(LJMP, 0x775690, WinDialogGetRectangle); -// However this WinDialog_GetRectangle is used for drawing offset, so we need to use the original window rect -static BOOL __fastcall _GetWindowRect(HWND hWnd, LPRECT rect) { +static BOOL __fastcall GetWindowRectHook(HWND hWnd, LPRECT rect) { return ::GetWindowRect(hWnd, rect); } -DEFINE_FUNCTION_JUMP(CALL, 0x610E77, _GetWindowRect); +DEFINE_FUNCTION_JUMP(CALL, 0x610E77, GetWindowRectHook); -// All controls inside the window is repositioned by this function, fix it up too -static BOOL __fastcall OD_MoveIngameWindowControls(HWND hWnd) { +static BOOL __fastcall MoveIngameWindowControls(HWND hWnd) { if (!SessionClass::Instance.CurrentlyInGame) return FALSE; @@ -134,47 +124,38 @@ static BOOL __fastcall OD_MoveIngameWindowControls(HWND hWnd) { return ::MoveWindow(hWnd, x, y, rect.right - rect.left, rect.bottom - rect.top, FALSE); } -DEFINE_FUNCTION_JUMP(LJMP, 0x60B7A0, OD_MoveIngameWindowControls); +DEFINE_FUNCTION_JUMP(LJMP, 0x60B7A0, MoveIngameWindowControls); DEFINE_JUMP(LJMP, 0x4A4830, 0x4A4848); // Skip Wait_Blit DEFINE_JUMP(LJMP, 0x4A4780, 0x4A4825); // Skip Set_DD_Palette -// Rendering preps. DEFINE_FUNCTION_JUMP(LJMP, 0x533FD0, RenderDX::AllocateSurfaces); DEFINE_FUNCTION_JUMP(LJMP, 0x4A42F0, RenderDX::SetVideoMode); DEFINE_FUNCTION_JUMP(LJMP, 0x4A44F0, RenderDX::ResetVideoMode); DEFINE_FUNCTION_JUMP(LJMP, 0x560BF0, RenderDX::ChangeDisplayMode); -// Update the window surface when the game updates its PrimarySurface -DEFINE_HOOK(0x4F4B7E, DXRender_UpdateScreen_GScreenClass_Blit, 0x5) { +DEFINE_HOOK(0x4F4B7E, DXRenderUpdateScreenGScreenClassBlit, 0x5) { RenderDX::UpdateScreen(DSurface::Primary); return 0; } -DEFINE_HOOK(0x5D233A, DXRender_UpdateScreen_MSEngine_Blit_Rects, 0x5) { - const auto eflags = R->EFLAGS(); - // not JLE - const auto zf = eflags & 0x40; - const auto sf = eflags & 0x80; - const auto of = eflags & 0x800; - if (!(zf || sf != of)) { - RenderDX::UpdateScreen(DSurface::Primary); - } +DEFINE_HOOK(0x5D2320, DXRenderUpdateScreenMSEngineBlitRects, 0x7) { + RenderDX::UpdateScreen(DSurface::Primary); return 0; } -DEFINE_HOOK(0x5D1F15, DXRender_UpdateScreen_MSEngine_Frame_Update, 0x5) { +DEFINE_HOOK(0x5D1F15, DXRenderUpdateScreenMSEngineFrameUpdate, 0x5) { RenderDX::UpdateScreen(DSurface::Primary); return 0; } -DEFINE_HOOK(0x690228, DXRender_UpdateScreen_ScoreClass_Call_Back_Delay, 0x6) { +DEFINE_HOOK(0x690228, DXRenderUpdateScreenScoreClassCallBackDelay, 0x6) { RenderDX::UpdateScreen(DSurface::Primary); return 0; } -DEFINE_NAKED_HOOK(0x5C0477, DXRender_UpdateScreen_Movie_Blit_To_Screen) { +DEFINE_NAKED_HOOK(0x5C0477, DXRenderUpdateScreenMovieBlitToScreen) { __asm { call dword ptr[edx + 8] mov ecx, dword ptr ds:[0x887308] @@ -187,31 +168,27 @@ DEFINE_NAKED_HOOK(0x5C0477, DXRender_UpdateScreen_Movie_Blit_To_Screen) { } } -// Windows controls -static LRESULT CALLBACK OwnerDraw_Window_Procedure_(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { - auto result = reinterpret_cast(0x610CA0)(hwnd, msg, wparam, lparam); - if (msg == WM_PAINT) { +static LRESULT CALLBACK OwnerDrawWindowProcedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + auto result = reinterpret_cast(0x610CA0)(hWnd, message, wParam, lParam); + if (message == WM_PAINT) RenderDX::UpdateScreen(DSurface::Primary); - } return result; } -DEFINE_PATCH_TYPED(void*, 0x60FF06, OwnerDraw_Window_Procedure_); +DEFINE_PATCH_TYPED(void*, 0x60FF06, OwnerDrawWindowProcedure); -DEFINE_HOOK_AGAIN(0x611FB0, DXRender_UpdateScreen_OwnerDraw_Window, 0x6); -DEFINE_HOOK(0x61187D, DXRender_UpdateScreen_OwnerDraw_Window, 0xA) { +DEFINE_HOOK_AGAIN(0x611FB0, DXRenderUpdateScreenOwnerDrawWindow, 0x6); +DEFINE_HOOK(0x61187D, DXRenderUpdateScreenOwnerDrawWindow, 0xA) { RenderDX::UpdateScreen(DSurface::Primary); return 0; } -DEFINE_HOOK(0x7776B5, MainWindowProc_WMPAINT, 0x6) { +DEFINE_HOOK(0x7776B5, MainWindowProcWMPaint, 0x6) { RenderDX::MainProcHandlePaint(); return 0x7779B5; } -// Call Set_Video_Mode even when windowed. DEFINE_JUMP(LJMP, 0x6BD9D9, 0x6BDA61); DEFINE_JUMP(LJMP, 0x6BDB16, 0x6BDB6D); -// Disable DirectDraw. DEFINE_JUMP(LJMP, 0x4A3FD0, 0x4A4019); // Skip Prep_Direct_Draw DEFINE_FUNCTION_JUMP(LJMP, 0x4A4900, RenderDX::EnumDisplayModes); diff --git a/src/Render/Mouse.cpp b/src/Render/Mouse.cpp index 5e20eacebf..e0607fde59 100644 --- a/src/Render/Mouse.cpp +++ b/src/Render/Mouse.cpp @@ -6,147 +6,145 @@ #include "Functions.h" +#include #include +#include #include -DXMouse::DXMouse(Surface* surface, HWND hwnd) {} +DXMouse::DXMouse(Surface* pSurface, HWND hWnd) {} DXMouse::~DXMouse() { - Delete_Cursor_Image(); + DeleteCursorImage(); if (Cursor) { ::DestroyCursor(Cursor); Cursor = nullptr; } } -void DXMouse::Set_Cursor(Point2D const& hotspot, SHPStruct const* cursor, int shape) { - if (cursor == nullptr || shape < 0 || shape >= cursor->Frames) { - Delete_Cursor_Image(); - Set_System_Cursor(); +void DXMouse::SetCursor(Point2D const& hotspot, SHPStruct const* pCursor, int shape) { + if (pCursor == nullptr || shape < 0 || shape >= pCursor->Frames) { + DeleteCursorImage(); + SetSystemCursor(); return; } - if (MouseShape == cursor && ShapeNumber == shape) { - return; // No change needed - } + if (MouseShape == pCursor && ShapeNumber == shape) + return; - if (cursor != MouseShape) { - Delete_Cursor_Image(); - Convert_Custor_Image(cursor); + if (pCursor != MouseShape) { + DeleteCursorImage(); + ConvertCursorImage(pCursor); } - MouseShape = cursor; + MouseShape = pCursor; ShapeNumber = shape; const auto& info = CursorInfo[shape]; Hotspot = hotspot; - Point2D scaled_hotspot; - scaled_hotspot.X = std::clamp(Hotspot.X * Get_Cursor_Scale(), 0, info.Width - 1); - scaled_hotspot.Y = std::clamp(Hotspot.Y * Get_Cursor_Scale(), 0, info.Height - 1); + Point2D scaledHotspot; + scaledHotspot.X = std::clamp(Hotspot.X * GetCursorScale(), 0, info.Width - 1); + scaledHotspot.Y = std::clamp(Hotspot.Y * GetCursorScale(), 0, info.Height - 1); - Replace_Cursor(Build_Cursor(info, scaled_hotspot.X, scaled_hotspot.Y)); + ReplaceCursor(BuildCursor(info, scaledHotspot.X, scaledHotspot.Y)); } -bool DXMouse::Is_Hidden() const { - return !IsVisible; +bool DXMouse::IsHidden() const { + return !Visible; } -void DXMouse::Hide_Mouse() { +void DXMouse::HideMouse() { Debug::Log("Hiding mouse cursor\n"); - if (!IsVisible) + if (!Visible) return; ::SetCursor(nullptr); - IsVisible = false; + Visible = false; } -void DXMouse::Show_Mouse() { +void DXMouse::ShowMouse() { Debug::Log("Showing mouse cursor\n"); - if (IsVisible) + if (Visible) return; ::SetCursor(Cursor); - IsVisible = true; + Visible = true; } -void DXMouse::Release_Mouse() { - if (!IsCaptured) +void DXMouse::ReleaseMouse() { + if (!Captured) return; ::ClipCursor(nullptr); - IsCaptured = false; + Captured = false; } -void DXMouse::Capture_Mouse() { - if (IsCaptured) +void DXMouse::CaptureMouse() { + if (Captured) return; - RECT client_rect; - ::GetClientRect(Game::hWnd, &client_rect); - ::MapWindowPoints(Game::hWnd, nullptr, reinterpret_cast(&client_rect), 2); - ::ClipCursor(&client_rect); + RECT clientRect; + ::GetClientRect(Game::hWnd, &clientRect); + ::MapWindowPoints(Game::hWnd, nullptr, reinterpret_cast(&clientRect), 2); + ::ClipCursor(&clientRect); - IsCaptured = true; + Captured = true; } -bool DXMouse::Is_Captured() const { - return IsCaptured; +bool DXMouse::IsCaptured() const { + return Captured; } -void DXMouse::Conditional_Hide_Mouse(RectangleStruct region) { - Hide_Mouse(); +void DXMouse::ConditionalHideMouse(RectangleStruct region) { + HideMouse(); } -void DXMouse::Conditional_Show_Mouse() { - Show_Mouse(); +void DXMouse::ConditionalShowMouse() { + ShowMouse(); } -int DXMouse::Get_Mouse_State() const { - return IsVisible ? 0 : -1; +int DXMouse::GetMouseState() const { + return Visible ? 0 : -1; } -int DXMouse::Get_Mouse_X() const { +int DXMouse::GetMouseX() const { return MouseX; } -int DXMouse::Get_Mouse_Y() const { +int DXMouse::GetMouseY() const { return MouseY; } -Point2D DXMouse::Get_Mouse_Point() const { +Point2D DXMouse::GetMousePoint() const { return Point2D { MouseX, MouseY }; } -void DXMouse::Set_Mouse_Point(int x, int y) { +void DXMouse::SetMousePoint(int x, int y) { MouseX = x; MouseY = y; } // Hardware cursor drawing is handled by the OS, so these functions are no-ops. -void DXMouse::Draw_Mouse(Surface* scr, bool issidebarsurface) {} +void DXMouse::DrawMouse(Surface* pSurface, bool isSidebarSurface) {} -void DXMouse::Erase_Mouse(Surface* scr, bool issidebarsurface) {} +void DXMouse::EraseMouse(Surface* pSurface, bool isSidebarSurface) {} // Coordinate conversion is not needed when using hardware cursor, so this is a no-op. -void DXMouse::Convert_Coordinate(int& x, int& y) const {} +void DXMouse::ConvertCoordinate(int& x, int& y) const {} -void DXMouse::Process_Mouse() { - // Update mouse position via GetCursorPos and ScreenToClient +void DXMouse::ProcessMouse() { if (!Unsorted::GameInFocus) return; POINT pt; - if (!::GetCursorPos(&pt)) { + if (!::GetCursorPos(&pt)) return; - } - if (!::ScreenToClient(Game::hWnd, &pt)) { + if (!::ScreenToClient(Game::hWnd, &pt)) return; - } if (RenderDX::ShouldScale()) { MouseX = RenderDX::ClientToRenderX(pt.x); @@ -159,64 +157,64 @@ void DXMouse::Process_Mouse() { } -void DXMouse::Recalc_Capture_Region() { - if (Is_Captured()) { - Release_Mouse(); - Capture_Mouse(); +void DXMouse::RecalcCaptureRegion() { + if (IsCaptured()) { + ReleaseMouse(); + CaptureMouse(); } } -void DXMouse::Set_Cached_Cursor() { - if (IsVisible) +void DXMouse::SetCachedCursor() { + if (Visible) ::SetCursor(Cursor); else ::SetCursor(nullptr); } -void DXMouse::Rebuild_Cursor_Image() { +void DXMouse::RebuildCursorImage() { SHPStruct const* shape = MouseShape; int number = ShapeNumber; - Delete_Cursor_Image(); - Set_Cursor(Hotspot, shape, number); + DeleteCursorImage(); + SetCursor(Hotspot, shape, number); } -void DXMouse::Delete_Cursor_Image() { +void DXMouse::DeleteCursorImage() { CursorInfo.clear(); MouseShape = nullptr; ShapeNumber = 0; } -void DXMouse::Convert_Custor_Image(SHPStruct const* cursor) { - if (!cursor || cursor->Frames <= 0) +void DXMouse::ConvertCursorImage(SHPStruct const* pCursor) { + if (!pCursor || pCursor->Frames <= 0) return; for (int i = 0; i < 256; ++i) { const auto color = static_cast(FileSystem::MOUSE_PAL->PaletteData)[i]; - auto clr = ColorStruct { color }; + auto clr = ColorStruct { static_cast(color) }; MousePalette[i] = ((i == 0 ? 0 : 255) << 24) | (clr.R << 16) | (clr.G << 8) | clr.B; } - CursorInfo.resize(cursor->Frames); - for (int i = 0; i < cursor->Frames; ++i) - Shape_To_Cursor(cursor, i, CursorInfo[i]); + CursorInfo.resize(pCursor->Frames); + for (int i = 0; i < pCursor->Frames; ++i) + ShapeToCursor(pCursor, i, CursorInfo[i]); } -void DXMouse::Shape_To_Cursor(SHPStruct const* cursor, int frame, CursorData& result) { - int width = cursor->Width; - int height = cursor->Height; +void DXMouse::ShapeToCursor(SHPStruct const* pCursor, int frame, CursorData& result) { + int width = pCursor->Width; + int height = pCursor->Height; - std::vector original_colors; - original_colors.resize(width * height); + std::vector originalColors; + originalColors.resize(width * height); - int scaled_width = static_cast(width * Get_Cursor_Scale()); - int scaled_height = static_cast(height * Get_Cursor_Scale()); + int scaledWidth = static_cast(width * GetCursorScale()); + int scaledHeight = static_cast(height * GetCursorScale()); BITMAPV5HEADER bi {}; bi.bV5Size = sizeof(BITMAPV5HEADER); - bi.bV5Width = scaled_width; - bi.bV5Height = -scaled_height; // Negative height for top-down bitmap + bi.bV5Width = scaledWidth; + bi.bV5Height = -scaledHeight; // Negative height creates a top-down bitmap. bi.bV5Planes = 1; bi.bV5BitCount = 32; bi.bV5Compression = BI_BITFIELDS; @@ -225,108 +223,104 @@ void DXMouse::Shape_To_Cursor(SHPStruct const* cursor, int frame, CursorData& re bi.bV5BlueMask = 0x000000FF; bi.bV5AlphaMask = 0xFF000000; - HDC hdc = ::GetDC(nullptr); - void* dst = nullptr; - HBITMAP bitmap = ::CreateDIBSection(hdc, reinterpret_cast(&bi), DIB_RGB_COLORS, &dst, nullptr, 0); - ::ReleaseDC(nullptr, hdc); + HDC hDC = ::GetDC(nullptr); + void* pDestPixels = nullptr; + HBITMAP bitmap = ::CreateDIBSection(hDC, reinterpret_cast(&bi), DIB_RGB_COLORS, &pDestPixels, nullptr, 0); + ::ReleaseDC(nullptr, hDC); - if (!dst || !bitmap) { + if (!pDestPixels || !bitmap) return; - } - const auto* src = static_cast(cursor->GetPixels(frame)); - const auto r = cursor->GetFrameBounds(frame); + const auto* pSource = static_cast(pCursor->GetPixels(frame)); + const auto rect = pCursor->GetFrameBounds(frame); - if (cursor->HasCompression(frame)) { - const uint8_t* psrc = src; - for (int y = 0; y < r.Height; ++y) { - uint32_t* dst_row = original_colors.data() + (r.Y + y) * width + r.X; - int len = psrc[0] | (psrc[1] << 8); + if (pCursor->HasCompression(frame)) { + const uint8_t* pSrc = pSource; + for (int y = 0; y < rect.Height; ++y) { + uint32_t* pDestRow = originalColors.data() + (rect.Y + y) * width + rect.X; + int length = pSrc[0] | (pSrc[1] << 8); int pos = 0; - for (int k = 2; k < len; ++k) { - uint8_t b = psrc[k]; - if (b == 0) { - uint8_t count = psrc[++k]; - for (int i = 0; i < count; ++i) { - dst_row[pos++] = MousePalette[0]; - } + for (int k = 2; k < length; ++k) { + uint8_t value = pSrc[k]; + if (value == 0) { + uint8_t count = pSrc[++k]; + for (int i = 0; i < count; ++i) + pDestRow[pos++] = MousePalette[0]; } else - dst_row[pos++] = MousePalette[b]; + pDestRow[pos++] = MousePalette[value]; } - psrc += len; + pSrc += length; } } else { - for (int y = 0; y < r.Height; ++y) { - uint32_t* dst_row = original_colors.data() + (r.Y + y) * width + r.X; - const uint8_t* src_row = src + y * r.Width; - for (int x = 0; x < r.Width; ++x) { - const auto color = MousePalette[src_row[x]]; - dst_row[x] = color; + for (int y = 0; y < rect.Height; ++y) { + uint32_t* pDestRow = originalColors.data() + (rect.Y + y) * width + rect.X; + const uint8_t* pSourceRow = pSource + y * rect.Width; + for (int x = 0; x < rect.Width; ++x) { + const auto color = MousePalette[pSourceRow[x]]; + pDestRow[x] = color; } } } - Scale_Bitmap_Image(original_colors.data(), width, height, static_cast(dst), scaled_width, scaled_height); + ScaleBitmapImage(originalColors.data(), width, height, static_cast(pDestPixels), scaledWidth, scaledHeight); - HBITMAP mask = ::CreateBitmap(scaled_width, scaled_height, 1, 1, nullptr); - - result.Width = scaled_width; - result.Height = scaled_height; + HBITMAP mask = ::CreateBitmap(scaledWidth, scaledHeight, 1, 1, nullptr); + + result.Width = scaledWidth; + result.Height = scaledHeight; result.Color = bitmap; result.Mask = mask; } -void DXMouse::Scale_Bitmap_Image(const uint32_t* src_ptr, int src_w, int src_h, uint32_t* dst, int dst_w, int dst_h) { - // Using nearest neighbor scaling for simplicity - const uint64_t inc_y = (static_cast(src_h) << 16) / dst_h; - const uint64_t inc_x = (static_cast(src_w) << 16) / dst_w; +void DXMouse::ScaleBitmapImage(const uint32_t* pSource, int sourceWidth, int sourceHeight, uint32_t* pDest, int destWidth, int destHeight) { + const uint64_t yStep = (static_cast(sourceHeight) << 16) / destHeight; + const uint64_t xStep = (static_cast(sourceWidth) << 16) / destWidth; - uint64_t pos_y = inc_y / 2; + uint64_t yPosition = yStep / 2; - for (int y = 0; y < dst_h; ++y) { - const uint64_t src_y = pos_y >> 16; - const uint32_t* src_row = src_ptr + src_y * src_w; + for (int y = 0; y < destHeight; ++y) { + const uint64_t sourceY = yPosition >> 16; + const uint32_t* pSourceRow = pSource + sourceY * sourceWidth; - pos_y += inc_y; + yPosition += yStep; - uint64_t pos_x = inc_x / 2; + uint64_t xPosition = xStep / 2; - for (int x = 0; x < dst_w; ++x) { - const uint64_t src_x = pos_x >> 16; - pos_x += inc_x; - *dst++ = src_row[src_x]; + for (int x = 0; x < destWidth; ++x) { + const uint64_t sourceX = xPosition >> 16; + xPosition += xStep; + *pDest++ = pSourceRow[sourceX]; } } } - -void DXMouse::Replace_Cursor(HCURSOR cursor) { - auto old_cursor = std::exchange(Cursor, cursor); +void DXMouse::ReplaceCursor(HCURSOR cursor) { + auto oldCursor = std::exchange(Cursor, cursor); ::SetCursor(Cursor); - if (old_cursor) { - ::DestroyCursor(old_cursor); - } + if (oldCursor) + ::DestroyCursor(oldCursor); } -void DXMouse::Set_System_Cursor() { Replace_Cursor(::LoadCursorA(nullptr, IDC_ARROW)); } +void DXMouse::SetSystemCursor() { + ReplaceCursor(::LoadCursorA(nullptr, IDC_ARROW)); +} -HCURSOR DXMouse::Build_Cursor(const CursorData& data, int hotspot_x, int hotspot_y) { +HCURSOR DXMouse::BuildCursor(const CursorData& data, int hotspotX, int hotspotY) { ICONINFO ii {}; ii.fIcon = FALSE; - ii.xHotspot = static_cast(hotspot_x); - ii.yHotspot = static_cast(hotspot_y); + ii.xHotspot = static_cast(hotspotX); + ii.yHotspot = static_cast(hotspotY); ii.hbmColor = data.Color; ii.hbmMask = data.Mask; return static_cast(::CreateIconIndirect(&ii)); } -int DXMouse::Get_Cursor_Scale() { - if (!RenderDX::ShouldScale()) { +int DXMouse::GetCursorScale() { + if (!RenderDX::ShouldScale()) return 1; - } return std::max(1, static_cast(std::round(1.0f / RenderDX::GetYScale()))); } diff --git a/src/Render/Mouse.h b/src/Render/Mouse.h index 26a508134f..b61577bee2 100644 --- a/src/Render/Mouse.h +++ b/src/Render/Mouse.h @@ -12,60 +12,60 @@ class Surface; class Mouse { public: virtual ~Mouse() {} - virtual void Set_Cursor(Point2D const& hotspot, SHPStruct const* cursor, int shape) = 0; - virtual bool Is_Hidden() const = 0; - virtual void Hide_Mouse() = 0; - virtual void Show_Mouse() = 0; - virtual void Release_Mouse() = 0; - virtual void Capture_Mouse() = 0; - virtual bool Is_Captured() const = 0; - virtual void Conditional_Hide_Mouse(RectangleStruct region) = 0; - virtual void Conditional_Show_Mouse() = 0; - virtual int Get_Mouse_State() const = 0; - virtual int Get_Mouse_X() const = 0; - virtual int Get_Mouse_Y() const = 0; - virtual Point2D Get_Mouse_Point() const = 0; - virtual void Set_Mouse_Point(int x, int y) = 0; - virtual void Draw_Mouse(Surface* scr, bool issidebarsurface = false) = 0; - virtual void Erase_Mouse(Surface* scr, bool issidebarsurface = false) = 0; - virtual void Convert_Coordinate(int& x, int& y) const = 0; + virtual void SetCursor(Point2D const& hotspot, SHPStruct const* pCursor, int shape) = 0; + virtual bool IsHidden() const = 0; + virtual void HideMouse() = 0; + virtual void ShowMouse() = 0; + virtual void ReleaseMouse() = 0; + virtual void CaptureMouse() = 0; + virtual bool IsCaptured() const = 0; + virtual void ConditionalHideMouse(RectangleStruct region) = 0; + virtual void ConditionalShowMouse() = 0; + virtual int GetMouseState() const = 0; + virtual int GetMouseX() const = 0; + virtual int GetMouseY() const = 0; + virtual Point2D GetMousePoint() const = 0; + virtual void SetMousePoint(int x, int y) = 0; + virtual void DrawMouse(Surface* pSurface, bool isSidebarSurface = false) = 0; + virtual void EraseMouse(Surface* pSurface, bool isSidebarSurface = false) = 0; + virtual void ConvertCoordinate(int& x, int& y) const = 0; }; class DXMouse : public Mouse { public: DEFINE_REFERENCE(DXMouse*, Instance, 0x887640u) - DXMouse(Surface* surface, HWND hwnd); + DXMouse(Surface* pSurface, HWND hWnd); virtual ~DXMouse() override; - virtual void Set_Cursor(Point2D const& hotspot, SHPStruct const* cursor, int shape) override; - virtual bool Is_Hidden() const override; - virtual void Hide_Mouse() override; - virtual void Show_Mouse() override; - virtual void Release_Mouse() override; - virtual void Capture_Mouse() override; - virtual bool Is_Captured() const override; - virtual void Conditional_Hide_Mouse(RectangleStruct region) override; - virtual void Conditional_Show_Mouse() override; - virtual int Get_Mouse_State() const override; - virtual int Get_Mouse_X() const override; - virtual int Get_Mouse_Y() const override; - virtual Point2D Get_Mouse_Point() const override; - virtual void Set_Mouse_Point(int x, int y) override; - virtual void Draw_Mouse(Surface* scr, bool issidebarsurface = false) override; - virtual void Erase_Mouse(Surface* scr, bool issidebarsurface = false) override; - virtual void Convert_Coordinate(int& x, int& y) const override; - - void Process_Mouse(); - void Recalc_Capture_Region(); - void Set_Cached_Cursor(); - - void Rebuild_Cursor_Image(); + virtual void SetCursor(Point2D const& hotspot, SHPStruct const* pCursor, int shape) override; + virtual bool IsHidden() const override; + virtual void HideMouse() override; + virtual void ShowMouse() override; + virtual void ReleaseMouse() override; + virtual void CaptureMouse() override; + virtual bool IsCaptured() const override; + virtual void ConditionalHideMouse(RectangleStruct region) override; + virtual void ConditionalShowMouse() override; + virtual int GetMouseState() const override; + virtual int GetMouseX() const override; + virtual int GetMouseY() const override; + virtual Point2D GetMousePoint() const override; + virtual void SetMousePoint(int x, int y) override; + virtual void DrawMouse(Surface* pSurface, bool isSidebarSurface = false) override; + virtual void EraseMouse(Surface* pSurface, bool isSidebarSurface = false) override; + virtual void ConvertCoordinate(int& x, int& y) const override; + + void ProcessMouse(); + void RecalcCaptureRegion(); + void SetCachedCursor(); + + void RebuildCursorImage(); private: - SHPStruct const* MouseShape { nullptr }; - int ShapeNumber { 0 }; + SHPStruct const* MouseShape { nullptr }; // Current SHP cursor data. + int ShapeNumber { 0 }; // Current cursor frame index. - DWORD MousePalette[256] { 0 }; + DWORD MousePalette[256] { 0 }; // ARGB palette converted from mouse.pal. struct CursorData { ~CursorData() { @@ -77,30 +77,30 @@ class DXMouse : public Mouse { } } - int Width { 0 }; - int Height { 0 }; - HBITMAP Color { nullptr }; - HBITMAP Mask { nullptr }; + int Width { 0 }; // Cursor bitmap width. + int Height { 0 }; // Cursor bitmap height. + HBITMAP Color { nullptr }; // Color bitmap handle. + HBITMAP Mask { nullptr }; // Mask bitmap handle. }; - std::vector CursorInfo; + std::vector CursorInfo; // Cached cursor frames. - Point2D Hotspot { 0,0 }; - HCURSOR Cursor { nullptr }; + Point2D Hotspot { 0, 0 }; // Cursor hotspot in render coordinates. + HCURSOR Cursor { nullptr }; // Current Win32 cursor handle. - bool IsCaptured { false }; - bool IsVisible { true }; + bool Captured { false }; // Whether the cursor is clipped to the game window. + bool Visible { true }; // Whether the cursor should be displayed. - int MouseX { 0 }; - int MouseY { 0 }; + int MouseX { 0 }; // Current render-space cursor X. + int MouseY { 0 }; // Current render-space cursor Y. - void Delete_Cursor_Image(); - void Convert_Custor_Image(SHPStruct const* cursor); - void Shape_To_Cursor(SHPStruct const* cursor, int frame, CursorData& result); - void Scale_Bitmap_Image(const uint32_t* src_ptr, int src_w, int src_h, uint32_t* dst, int dst_w, int dst_h); - void Replace_Cursor(HCURSOR cursor); - void Set_System_Cursor(); - HCURSOR Build_Cursor(const CursorData& data, int hotspot_x, int hotspot_y); + void DeleteCursorImage(); + void ConvertCursorImage(SHPStruct const* pCursor); + void ShapeToCursor(SHPStruct const* pCursor, int frame, CursorData& result); + void ScaleBitmapImage(const uint32_t* pSource, int sourceWidth, int sourceHeight, uint32_t* pDest, int destWidth, int destHeight); + void ReplaceCursor(HCURSOR cursor); + void SetSystemCursor(); + HCURSOR BuildCursor(const CursorData& data, int hotspotX, int hotspotY); - static int Get_Cursor_Scale(); + static int GetCursorScale(); }; diff --git a/src/Render/Options.cpp b/src/Render/Options.cpp deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/Render/Options.h b/src/Render/Options.h index e46ac1cfbb..b871eef3b1 100644 --- a/src/Render/Options.h +++ b/src/Render/Options.h @@ -1,10 +1,8 @@ #pragma once -struct DXRenderOptions -{ - static DXRenderOptions& Config() - { - static DXRenderOptions instance; +struct RenderOptions { + static RenderOptions& Config() { + static RenderOptions instance; return instance; } diff --git a/src/Render/Renderer.SurfacePS.h b/src/Render/Renderer.SurfacePS.h new file mode 100644 index 0000000000..4aff26fed8 --- /dev/null +++ b/src/Render/Renderer.SurfacePS.h @@ -0,0 +1,81 @@ +#if 0 +// +// Generated by Microsoft (R) D3D Shader Disassembler +// +// +// Input signature: +// +// Name Index Mask Register SysValue Format Used +// -------------------- ----- ------ -------- -------- ------- ------ +// SV_Position 0 xyzw 0 POS float +// TEXCOORD 0 xy 1 NONE float xy +// +// +// Output signature: +// +// Name Index Mask Register SysValue Format Used +// -------------------- ----- ------ -------- -------- ------- ------ +// SV_Target 0 xyzw 0 TARGET float xyzw +// +ps_4_0 +dcl_sampler s0, mode_default +dcl_resource_texture2d (float,float,float,float) t0 +dcl_input_ps linear v1.xy +dcl_output o0.xyzw +sample o0.xyzw, v1.xyxx, t0.xyzw, s0 +ret +// Approximately 0 instruction slots used +#endif + +const BYTE SurfacePixelShaderBytecode[] = +{ + 68, 88, 66, 67, 108, 129, + 30, 239, 130, 190, 108, 21, + 28, 176, 127, 137, 202, 107, + 77, 84, 1, 0, 0, 0, + 36, 1, 0, 0, 3, 0, + 0, 0, 44, 0, 0, 0, + 132, 0, 0, 0, 184, 0, + 0, 0, 73, 83, 71, 78, + 80, 0, 0, 0, 2, 0, + 0, 0, 8, 0, 0, 0, + 56, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, + 3, 0, 0, 0, 0, 0, + 0, 0, 15, 0, 0, 0, + 68, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 1, 0, + 0, 0, 3, 3, 0, 0, + 83, 86, 95, 80, 111, 115, + 105, 116, 105, 111, 110, 0, + 84, 69, 88, 67, 79, 79, + 82, 68, 0, 171, 171, 171, + 79, 83, 71, 78, 44, 0, + 0, 0, 1, 0, 0, 0, + 8, 0, 0, 0, 32, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 3, 0, + 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 83, 86, + 95, 84, 97, 114, 103, 101, + 116, 0, 171, 171, 83, 72, + 68, 82, 100, 0, 0, 0, + 64, 0, 0, 0, 25, 0, + 0, 0, 90, 0, 0, 3, + 0, 96, 16, 0, 0, 0, + 0, 0, 88, 24, 0, 4, + 0, 112, 16, 0, 0, 0, + 0, 0, 85, 85, 0, 0, + 98, 16, 0, 3, 50, 16, + 16, 0, 1, 0, 0, 0, + 101, 0, 0, 3, 242, 32, + 16, 0, 0, 0, 0, 0, + 69, 0, 0, 9, 242, 32, + 16, 0, 0, 0, 0, 0, + 70, 16, 16, 0, 1, 0, + 0, 0, 70, 126, 16, 0, + 0, 0, 0, 0, 0, 96, + 16, 0, 0, 0, 0, 0, + 62, 0, 0, 1 +}; diff --git a/src/Render/Renderer.SurfaceVS.h b/src/Render/Renderer.SurfaceVS.h new file mode 100644 index 0000000000..fd32186053 --- /dev/null +++ b/src/Render/Renderer.SurfaceVS.h @@ -0,0 +1,145 @@ +#if 0 +// +// Generated by Microsoft (R) D3D Shader Disassembler +// +// +// Input signature: +// +// Name Index Mask Register SysValue Format Used +// -------------------- ----- ------ -------- -------- ------- ------ +// SV_VertexID 0 x 0 VERTID uint x +// +// +// Output signature: +// +// Name Index Mask Register SysValue Format Used +// -------------------- ----- ------ -------- -------- ------- ------ +// SV_Position 0 xyzw 0 POS float xyzw +// TEXCOORD 0 xy 1 NONE float xy +// +vs_4_0 +dcl_input_sgv v0.x, vertex_id +dcl_output_siv o0.xyzw, position +dcl_output o1.xy +dcl_temps 1 +dcl_indexableTemp x0[3], 4 +dcl_indexableTemp x1[3], 4 +mov x0[0].xy, l(-1.000000,-1.000000,0,0) +mov x0[1].xy, l(-1.000000,3.000000,0,0) +mov x0[2].xy, l(3.000000,-1.000000,0,0) +mov x1[0].xy, l(0,1.000000,0,0) +mov x1[1].xy, l(0,-1.000000,0,0) +mov x1[2].xy, l(2.000000,1.000000,0,0) +mov r0.x, v0.x +mov o0.xy, x0[r0.x + 0].xyxx +mov o1.xy, x1[r0.x + 0].xyxx +mov o0.zw, l(0,0,0,1.000000) +ret +// Approximately 0 instruction slots used +#endif + +const BYTE SurfaceVertexShaderBytecode[] = +{ + 68, 88, 66, 67, 109, 35, + 224, 23, 12, 241, 22, 25, + 192, 199, 158, 245, 207, 212, + 50, 22, 1, 0, 0, 0, + 100, 2, 0, 0, 3, 0, + 0, 0, 44, 0, 0, 0, + 96, 0, 0, 0, 184, 0, + 0, 0, 73, 83, 71, 78, + 44, 0, 0, 0, 1, 0, + 0, 0, 8, 0, 0, 0, + 32, 0, 0, 0, 0, 0, + 0, 0, 6, 0, 0, 0, + 1, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 0, 0, + 83, 86, 95, 86, 101, 114, + 116, 101, 120, 73, 68, 0, + 79, 83, 71, 78, 80, 0, + 0, 0, 2, 0, 0, 0, + 8, 0, 0, 0, 56, 0, + 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 3, 0, + 0, 0, 0, 0, 0, 0, + 15, 0, 0, 0, 68, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 3, 0, + 0, 0, 1, 0, 0, 0, + 3, 12, 0, 0, 83, 86, + 95, 80, 111, 115, 105, 116, + 105, 111, 110, 0, 84, 69, + 88, 67, 79, 79, 82, 68, + 0, 171, 171, 171, 83, 72, + 68, 82, 164, 1, 0, 0, + 64, 0, 1, 0, 105, 0, + 0, 0, 96, 0, 0, 4, + 18, 16, 16, 0, 0, 0, + 0, 0, 6, 0, 0, 0, + 103, 0, 0, 4, 242, 32, + 16, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 101, 0, + 0, 3, 50, 32, 16, 0, + 1, 0, 0, 0, 104, 0, + 0, 2, 1, 0, 0, 0, + 105, 0, 0, 4, 0, 0, + 0, 0, 3, 0, 0, 0, + 4, 0, 0, 0, 105, 0, + 0, 4, 1, 0, 0, 0, + 3, 0, 0, 0, 4, 0, + 0, 0, 54, 0, 0, 9, + 50, 48, 32, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 2, 64, 0, 0, 0, 0, + 128, 191, 0, 0, 128, 191, + 0, 0, 0, 0, 0, 0, + 0, 0, 54, 0, 0, 9, + 50, 48, 32, 0, 0, 0, + 0, 0, 1, 0, 0, 0, + 2, 64, 0, 0, 0, 0, + 128, 191, 0, 0, 64, 64, + 0, 0, 0, 0, 0, 0, + 0, 0, 54, 0, 0, 9, + 50, 48, 32, 0, 0, 0, + 0, 0, 2, 0, 0, 0, + 2, 64, 0, 0, 0, 0, + 64, 64, 0, 0, 128, 191, + 0, 0, 0, 0, 0, 0, + 0, 0, 54, 0, 0, 9, + 50, 48, 32, 0, 1, 0, + 0, 0, 0, 0, 0, 0, + 2, 64, 0, 0, 0, 0, + 0, 0, 0, 0, 128, 63, + 0, 0, 0, 0, 0, 0, + 0, 0, 54, 0, 0, 9, + 50, 48, 32, 0, 1, 0, + 0, 0, 1, 0, 0, 0, + 2, 64, 0, 0, 0, 0, + 0, 0, 0, 0, 128, 191, + 0, 0, 0, 0, 0, 0, + 0, 0, 54, 0, 0, 9, + 50, 48, 32, 0, 1, 0, + 0, 0, 2, 0, 0, 0, + 2, 64, 0, 0, 0, 0, + 0, 64, 0, 0, 128, 63, + 0, 0, 0, 0, 0, 0, + 0, 0, 54, 0, 0, 5, + 18, 0, 16, 0, 0, 0, + 0, 0, 10, 16, 16, 0, + 0, 0, 0, 0, 54, 0, + 0, 7, 50, 32, 16, 0, + 0, 0, 0, 0, 70, 48, + 32, 4, 0, 0, 0, 0, + 10, 0, 16, 0, 0, 0, + 0, 0, 54, 0, 0, 7, + 50, 32, 16, 0, 1, 0, + 0, 0, 70, 48, 32, 4, + 1, 0, 0, 0, 10, 0, + 16, 0, 0, 0, 0, 0, + 54, 0, 0, 8, 194, 32, + 16, 0, 0, 0, 0, 0, + 2, 64, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 128, 63, 62, 0, 0, 1 +}; diff --git a/src/Render/Renderer.cpp b/src/Render/Renderer.cpp index a5907e4fa6..6e6daf96ff 100644 --- a/src/Render/Renderer.cpp +++ b/src/Render/Renderer.cpp @@ -8,8 +8,12 @@ #include "Functions.h" #include "Options.h" +#include "Renderer.SurfacePS.h" +#include "Renderer.SurfaceVS.h" #include +#include +#include DXRenderer& DXRenderer::Instance() { static DXRenderer instance; @@ -17,7 +21,7 @@ DXRenderer& DXRenderer::Instance() { } static LONG_PTR GetConfiguredWindowedStyle(LONG_PTR style, bool visible) { - if (DXRenderOptions::Config().WindowedBorder) + if (RenderOptions::Config().WindowedBorder) style = (style & ~WS_POPUP) | WS_OVERLAPPEDWINDOW; else style = (style & ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU)) | WS_POPUP; @@ -31,7 +35,7 @@ static LONG_PTR GetConfiguredWindowedStyle(LONG_PTR style, bool visible) { } static LONG_PTR GetConfiguredWindowedExStyle(LONG_PTR exStyle) { - if (DXRenderOptions::Config().WindowedBorder) + if (RenderOptions::Config().WindowedBorder) return exStyle; return exStyle & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE); @@ -55,8 +59,8 @@ static bool GetPrimaryMonitorRect(RECT& monitorRect) { return GetMonitorRect(::MonitorFromPoint(point, MONITOR_DEFAULTTOPRIMARY), monitorRect); } -static bool GetNearestMonitorRect(HWND hwnd, RECT& monitorRect) { - return GetMonitorRect(::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), monitorRect); +static bool GetNearestMonitorRect(HWND hWnd, RECT& monitorRect) { + return GetMonitorRect(::MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST), monitorRect); } static void CenterRectInMonitor(RECT& rect, const RECT& monitorRect) { @@ -69,7 +73,7 @@ static void CenterRectInMonitor(RECT& rect, const RECT& monitorRect) { rect.bottom = rect.top + height; } -bool DXRenderer::CreateMainWindow(HINSTANCE instance, int cmd_show, int width, int height, WNDPROC proc) { +bool DXRenderer::CreateMainWindow(HINSTANCE instance, int cmdShow, int width, int height, WNDPROC proc) { ::InitCommonControls(); WNDCLASSA wc {}; @@ -95,12 +99,12 @@ bool DXRenderer::CreateMainWindow(HINSTANCE instance, int cmd_show, int width, i centerWindow = true; } - int window_width = rect.right - rect.left; - int window_height = rect.bottom - rect.top; - int window_x = centerWindow ? rect.left : CW_USEDEFAULT; - int window_y = centerWindow ? rect.top : CW_USEDEFAULT; + int windowWidth = rect.right - rect.left; + int windowHeight = rect.bottom - rect.top; + int windowX = centerWindow ? rect.left : CW_USEDEFAULT; + int windowY = centerWindow ? rect.top : CW_USEDEFAULT; - Game::hWnd = ::CreateWindowExA(static_cast(exStyle), wc.lpszClassName, wc.lpszClassName, static_cast(style), window_x, window_y, window_width, window_height, nullptr, nullptr, instance, nullptr); + Game::hWnd = ::CreateWindowExA(static_cast(exStyle), wc.lpszClassName, wc.lpszClassName, static_cast(style), windowX, windowY, windowWidth, windowHeight, nullptr, nullptr, instance, nullptr); if (!Game::hWnd) { Debug::Log("[RenderDX] Failed to create main window\n"); @@ -113,13 +117,13 @@ bool DXRenderer::CreateMainWindow(HINSTANCE instance, int cmd_show, int width, i SetWindowLongPtrA(Game::hWnd, GWL_STYLE, style); Hwnd = Game::hWnd; - WindowWidth = window_width; - WindowHeight = window_height; + WindowWidth = windowWidth; + WindowHeight = windowHeight; - if (DXRenderOptions::Config().StartFullscreen) + if (RenderOptions::Config().StartFullscreen) ToggleFullscreen(); - ::ShowWindow(Game::hWnd, cmd_show); + ::ShowWindow(Game::hWnd, cmdShow); ::UpdateWindow(Game::hWnd); Game::hIMC = ::ImmGetContext(Game::hWnd); @@ -153,9 +157,9 @@ bool DXRenderer::IsRendererReady() { return true; } -bool DXRenderer::CreateRenderer(int width, int height, int bits_per_pixel) { - if (bits_per_pixel != 16) { - Debug::Log("[RenderDX] Unsupported bits per pixel: %d\n", bits_per_pixel); +bool DXRenderer::CreateRenderer(int width, int height, int bitsPerPixel) { + if (bitsPerPixel != 16) { + Debug::Log("[RenderDX] Unsupported bits per pixel: %d\n", bitsPerPixel); return false; } @@ -169,12 +173,7 @@ bool DXRenderer::CreateRenderer(int width, int height, int bits_per_pixel) { UpdateViewportAndScissor(); if (!CreateDevice()) { - Debug::Log("[RenderDX] Failed to create D3D12 device\n"); - return false; - } - - if (!CreateCommandQueue()) { - Debug::Log("[RenderDX] Failed to create command queue\n"); + Debug::Log("[RenderDX] Failed to create D3D11 device\n"); return false; } @@ -183,36 +182,16 @@ bool DXRenderer::CreateRenderer(int width, int height, int bits_per_pixel) { return false; } - if (!CreateRtvHeap()) { - Debug::Log("[RenderDX] Failed to create RTV descriptor heap\n"); - return false; - } - if (!CreateRenderTargetViews()) { Debug::Log("[RenderDX] Failed to create render target views\n"); return false; } - if (!CreateSrvHeap()) { - Debug::Log("[RenderDX] Failed to create SRV descriptor heap\n"); - return false; - } - if (!CreateSurfacePipeline()) { Debug::Log("[RenderDX] Failed to create surface pipeline\n"); return false; } - if (!CreateCommandObjects()) { - Debug::Log("[RenderDX] Failed to create command objects\n"); - return false; - } - - if (!CreateFenceObjects()) { - Debug::Log("[RenderDX] Failed to create fence objects\n"); - return false; - } - if (!CreateFixedSurfaceGpuResources()) { Debug::Log("[RenderDX] Failed to create fixed surface GPU resources\n"); return false; @@ -222,81 +201,35 @@ bool DXRenderer::CreateRenderer(int width, int height, int bits_per_pixel) { } void DXRenderer::DestroyRenderer() { - if (SurfaceTexture) { - SurfaceTexture.Reset(); - } - - for (UINT i = 0; i < kFrameCount; ++i) { - if (SurfaceUploadBuffers[i] && SurfaceUploadMapped[i]) { - SurfaceUploadBuffers[i]->Unmap(0, nullptr); - SurfaceUploadMapped[i] = nullptr; - } - } - - for (UINT i = 0; i < kFrameCount; ++i) { - if (SurfaceUploadBuffers[i]) { - SurfaceUploadBuffers[i].Reset(); - } - } - - if (Fence) { - Fence.Reset(); - } - if (FenceEvent) { - ::CloseHandle(FenceEvent); - FenceEvent = nullptr; - } - - if (CommandList) { - CommandList.Reset(); - } - for (UINT i = 0; i < kFrameCount; ++i) { - if (CommandAllocators[i]) { - CommandAllocators[i].Reset(); - } + if (DeviceContext) { + DeviceContext->ClearState(); + DeviceContext->Flush(); } - if (PipelineState) { - PipelineState.Reset(); - } - if (RootSignature) { - RootSignature.Reset(); - } - - if (SrvHeap) { - SrvHeap.Reset(); - } + DepthStencilState.Reset(); + BlendState.Reset(); + RasterizerState.Reset(); + SamplerState.Reset(); + PixelShader.Reset(); + VertexShader.Reset(); + SurfaceShaderResourceView.Reset(); + SurfaceTexture.Reset(); - for (UINT i = 0; i < kFrameCount; ++i) { - if (RenderTargets[i]) - RenderTargets[i].Reset(); - } - - if (RtvHeap) { - RtvHeap.Reset(); - } - RtvDescriptorSize = 0; + RenderTargetView.Reset(); if (SwapChain) { BOOL fullscreenState = FALSE; Microsoft::WRL::ComPtr pTarget; - if (SUCCEEDED(SwapChain->GetFullscreenState(&fullscreenState, &pTarget)) && fullscreenState) + if (SUCCEEDED(SwapChain->GetFullscreenState(&fullscreenState, pTarget.GetAddressOf())) && fullscreenState) SwapChain->SetFullscreenState(FALSE, nullptr); SwapChain.Reset(); } - FrameIndex = 0; + SwapChainBufferCount = 0; - if (CommandQueue) { - CommandQueue.Reset(); - } - - if (Device) { - Device.Reset(); - } - if (Factory) { - Factory.Reset(); - } + DeviceContext.Reset(); + Device.Reset(); + Factory.Reset(); } bool DXRenderer::ResizeWindow(int width, int height) { @@ -315,22 +248,17 @@ bool DXRenderer::ResizeWindow(int width, int height) { return false; } - for (auto& target : RenderTargets) { - target.Reset(); - } + RenderTargetView.Reset(); - if (FAILED(SwapChain->ResizeBuffers(kFrameCount, WindowWidth, WindowHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0))) { + const UINT bufferCount = SwapChainBufferCount ? SwapChainBufferCount : 1; + if (FAILED(SwapChain->ResizeBuffers(bufferCount, WindowWidth, WindowHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0))) { Debug::Log("[RenderDX] Failed to resize swap chain buffers.\n"); return false; } - FrameIndex = SwapChain->GetCurrentBackBufferIndex(); if (!CreateRenderTargetViews()) return false; - const UINT64 newFenceValue = Fence->GetCompletedValue() + 1; - FenceValues.fill(newFenceValue); - Debug::Log("[RenderDX] Swap chain resized successfully to %ux%u.\n", WindowWidth, WindowHeight); return true; @@ -358,7 +286,7 @@ void DXRenderer::ToggleFullscreen() { int windowX = WindowedRect.left; int windowY = WindowedRect.top; - if (!DXRenderOptions::Config().WindowedBorder) { + if (!RenderOptions::Config().WindowedBorder) { RECT monitorRect {}; if (GetNearestMonitorRect(Hwnd, monitorRect)) { RECT centeredRect = { 0, 0, width, height }; @@ -405,19 +333,17 @@ void DXRenderer::ToggleFullscreen() { Debug::Log("[RenderDX] Borderless fullscreen enabled.\n"); } -bool DXRenderer::UploadSurfaceToTexture(void* surface_data, int source_pitch) { +bool DXRenderer::UploadSurfaceToTexture(void* pSurfaceData, int sourcePitch) { const int sourceRowBytes = RenderWidth * static_cast(sizeof(std::uint16_t)); - if (source_pitch < sourceRowBytes) { - Debug::Log("[RenderDX] Source pitch %d is smaller than required row bytes %d.\n", source_pitch, sourceRowBytes); + if (sourcePitch < sourceRowBytes) { + Debug::Log("[RenderDX] Source pitch %d is smaller than required row bytes %d.\n", sourcePitch, sourceRowBytes); return false; } - if (!PopulateCommandListForCPUSurface(surface_data, source_pitch)) + if (!UploadSurfaceToGpu(pSurfaceData, sourcePitch)) return false; - ID3D12CommandList* list[] = { CommandList.Get() }; - CommandQueue->ExecuteCommandLists(1, list); - return true; + return RenderSurface(); } void DXRenderer::SetRenderScale(bool scale) { @@ -435,11 +361,11 @@ bool DXRenderer::Present() { return false; } - return MoveToNextFrame(); + return true; } void DXRenderer::MoveWindow(int x, int y, int width, int height) { - if (Windowed && !DXRenderOptions::Config().WindowedBorder) { + if (Windowed && !RenderOptions::Config().WindowedBorder) { RECT monitorRect {}; if (GetNearestMonitorRect(Hwnd, monitorRect)) { RECT centeredRect = { 0, 0, width, height }; @@ -489,49 +415,33 @@ DXRenderer::DXRenderer() {} DXRenderer::~DXRenderer() {} bool DXRenderer::LoadImports() { - D3D12Lib = ::LoadLibraryW(L"d3d12.dll"); - if (!D3D12Lib) { - Debug::Log("[RenderDX] Failed to load d3d12.dll.\n"); - return false; - } + UnloadImports(); -#if DXRENDER_DEBUG - FP_D3D12GetDebugInterface = reinterpret_cast(::GetProcAddress(D3D12Lib, "D3D12GetDebugInterface")); - if (!FP_D3D12GetDebugInterface) { - Debug::Log("[RenderDX] Failed to get address of D3D12GetDebugInterface.\n"); - return false; - } -#endif - FP_D3D12CreateDevice = reinterpret_cast(::GetProcAddress(D3D12Lib, "D3D12CreateDevice")); - if (!FP_D3D12CreateDevice) { - Debug::Log("[RenderDX] Failed to get address of D3D12CreateDevice.\n"); + D3D11Lib = ::LoadLibraryW(L"d3d11.dll"); + if (!D3D11Lib) { + Debug::Log("[RenderDX] Failed to load d3d11.dll.\n"); return false; } - FP_D3D12SerializeRootSignature = reinterpret_cast(::GetProcAddress(D3D12Lib, "D3D12SerializeRootSignature")); - if (!FP_D3D12SerializeRootSignature) { - Debug::Log("[RenderDX] Failed to get address of D3D12SerializeRootSignature.\n"); + + D3D11CreateDeviceProc = reinterpret_cast(::GetProcAddress(D3D11Lib, "D3D11CreateDevice")); + if (!D3D11CreateDeviceProc) { + Debug::Log("[RenderDX] Failed to get address of D3D11CreateDevice.\n"); + UnloadImports(); return false; } DXGILib = ::LoadLibraryW(L"dxgi.dll"); if (!DXGILib) { Debug::Log("[RenderDX] Failed to load dxgi.dll.\n"); + UnloadImports(); return false; } - FP_CreateDXGIFactory2 = reinterpret_cast(::GetProcAddress(DXGILib, "CreateDXGIFactory2")); - if (!FP_CreateDXGIFactory2) { - Debug::Log("[RenderDX] Failed to get address of CreateDXGIFactory2.\n"); - return false; - } - - D3DCompilerLib = ::LoadLibraryW(L"d3dcompiler_47.dll"); - if (!D3DCompilerLib) { - Debug::Log("[RenderDX] Failed to load d3dcompiler_47.dll.\n"); - return false; - } - FP_D3DCompile = reinterpret_cast(::GetProcAddress(D3DCompilerLib, "D3DCompile")); - if (!FP_D3DCompile) { - Debug::Log("[RenderDX] Failed to get address of D3DCompile.\n"); + CreateDXGIFactory2Proc = reinterpret_cast(::GetProcAddress(DXGILib, "CreateDXGIFactory2")); + CreateDXGIFactory1Proc = reinterpret_cast(::GetProcAddress(DXGILib, "CreateDXGIFactory1")); + CreateDXGIFactoryProc = reinterpret_cast(::GetProcAddress(DXGILib, "CreateDXGIFactory")); + if (!CreateDXGIFactory2Proc && !CreateDXGIFactory1Proc && !CreateDXGIFactoryProc) { + Debug::Log("[RenderDX] Failed to get a DXGI factory creation entry point.\n"); + UnloadImports(); return false; } @@ -539,120 +449,210 @@ bool DXRenderer::LoadImports() { } void DXRenderer::UnloadImports() { - if (D3DCompilerLib) { - ::FreeLibrary(D3DCompilerLib); - FP_D3DCompile = nullptr; - } if (DXGILib) { ::FreeLibrary(DXGILib); - FP_CreateDXGIFactory2 = nullptr; + DXGILib = nullptr; + CreateDXGIFactory2Proc = nullptr; + CreateDXGIFactory1Proc = nullptr; + CreateDXGIFactoryProc = nullptr; } - if (D3D12Lib) { - ::FreeLibrary(D3D12Lib); -#if DXRENDER_DEBUG - FP_D3D12GetDebugInterface = nullptr; -#endif - FP_D3D12CreateDevice = nullptr; - FP_D3D12SerializeRootSignature = nullptr; + if (D3D11Lib) { + ::FreeLibrary(D3D11Lib); + D3D11Lib = nullptr; + D3D11CreateDeviceProc = nullptr; + } +} + +bool DXRenderer::CreateFactory(UINT dxgiFactoryFlags) { + HRESULT hr = S_OK; + Factory.Reset(); + + if (CreateDXGIFactory2Proc) { + Microsoft::WRL::ComPtr factory2; + hr = CreateDXGIFactory2Proc(dxgiFactoryFlags, IID_PPV_ARGS(factory2.GetAddressOf())); + if (SUCCEEDED(hr) && SUCCEEDED(hr = factory2.As(&Factory))) { + Debug::Log("[RenderDX] DXGI factory created with CreateDXGIFactory2.\n"); + return true; + } + + Debug::Log("[RenderDX] CreateDXGIFactory2 failed: 0x%08X. Falling back to older factory entry points.\n", static_cast(hr)); + } + + if (CreateDXGIFactory1Proc) { + Microsoft::WRL::ComPtr factory1; + hr = CreateDXGIFactory1Proc(IID_PPV_ARGS(factory1.GetAddressOf())); + if (SUCCEEDED(hr) && SUCCEEDED(hr = factory1.As(&Factory))) { + Debug::Log("[RenderDX] DXGI factory created with CreateDXGIFactory1.\n"); + return true; + } + + Debug::Log("[RenderDX] CreateDXGIFactory1 failed: 0x%08X.\n", static_cast(hr)); + } + + if (CreateDXGIFactoryProc) { + hr = CreateDXGIFactoryProc(IID_PPV_ARGS(Factory.ReleaseAndGetAddressOf())); + if (SUCCEEDED(hr)) { + Debug::Log("[RenderDX] DXGI factory created with CreateDXGIFactory.\n"); + return true; + } + + Debug::Log("[RenderDX] CreateDXGIFactory failed: 0x%08X.\n", static_cast(hr)); } + + return false; } bool DXRenderer::CreateDevice() { HRESULT hr = S_OK; UINT dxgiFactoryFlags = 0; + UINT deviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; #if DXRENDER_DEBUG - Microsoft::WRL::ComPtr debugController; - if (SUCCEEDED(hr = FP_D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)))) { - Debug::Log("[RenderDX] D3D12 debug layer enabled.\n"); - debugController->EnableDebugLayer(); - } dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG; + deviceFlags |= D3D11_CREATE_DEVICE_DEBUG; #endif - if (FAILED(hr = FP_CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&Factory)))) { - Debug::Log("[RenderDX] Failed to create DXGI factory: 0x%08X\n", static_cast(hr)); + + if (!CreateFactory(dxgiFactoryFlags)) { + Debug::Log("[RenderDX] Failed to create DXGI factory.\n"); return false; } + const D3D_FEATURE_LEVEL featureLevels[] = { + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0 + }; + D3D_FEATURE_LEVEL createdLevel = D3D_FEATURE_LEVEL_11_0; + Microsoft::WRL::ComPtr factory6; - if (FAILED(hr = Factory.As(&factory6))) - { - Debug::Log("[RenderDX] Failed to query IDXGIFactory6 interface: 0x%08X\nFalling back to EnumAdapters1()", static_cast(hr)); - Microsoft::WRL::ComPtr hardwareAdapter; - for (UINT adapterIndex = 0; Factory->EnumAdapters1(adapterIndex, &hardwareAdapter) != DXGI_ERROR_NOT_FOUND; ++adapterIndex) - { - DXGI_ADAPTER_DESC1 desc; + if (SUCCEEDED(hr = Factory.As(&factory6))) { + Debug::Log("[RenderDX] IDXGIFactory6 interface is available. Using EnumAdapterByGpuPreference to select the adapter.\n"); + for (UINT adapterIndex = 0; !Device; ++adapterIndex) { + Microsoft::WRL::ComPtr hardwareAdapter; + hr = factory6->EnumAdapterByGpuPreference(adapterIndex, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(hardwareAdapter.GetAddressOf())); + if (hr == DXGI_ERROR_NOT_FOUND) + break; + if (FAILED(hr)) { + Debug::Log("[RenderDX] EnumAdapterByGpuPreference failed: 0x%08X\n", static_cast(hr)); + break; + } + + DXGI_ADAPTER_DESC1 desc {}; hardwareAdapter->GetDesc1(&desc); if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) - { continue; - } - if (SUCCEEDED(hr = FP_D3D12CreateDevice(hardwareAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) - { - Debug::Log("[RenderDX] D3D12 device created successfully on adapter: %ls\n", desc.Description); + + if (SUCCEEDED(hr = D3D11CreateDeviceProc(hardwareAdapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, deviceFlags, featureLevels, _countof(featureLevels), D3D11_SDK_VERSION, Device.ReleaseAndGetAddressOf(), &createdLevel, DeviceContext.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] D3D11 device created successfully on adapter: %ls\n", desc.Description); break; } } } - else - { - Debug::Log("[RenderDX] IDXGIFactory6 interface is available. Using EnumAdapterByGpuPreference to select the adapter.\n"); - Microsoft::WRL::ComPtr hardwareAdapter; - for (UINT adapterIndex = 0; factory6->EnumAdapterByGpuPreference(adapterIndex, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&hardwareAdapter)) != DXGI_ERROR_NOT_FOUND; ++adapterIndex) - { - DXGI_ADAPTER_DESC1 desc; - hardwareAdapter->GetDesc1(&desc); - if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) - { - continue; + else { + Debug::Log("[RenderDX] IDXGIFactory6 is unavailable: 0x%08X. Falling back to older adapter enumeration.\n", static_cast(hr)); + } + + if (!Device) { + Microsoft::WRL::ComPtr factory1; + if (SUCCEEDED(hr = Factory.As(&factory1))) { + for (UINT adapterIndex = 0; !Device; ++adapterIndex) { + Microsoft::WRL::ComPtr hardwareAdapter; + hr = factory1->EnumAdapters1(adapterIndex, hardwareAdapter.ReleaseAndGetAddressOf()); + if (hr == DXGI_ERROR_NOT_FOUND) + break; + if (FAILED(hr)) { + Debug::Log("[RenderDX] EnumAdapters1 failed: 0x%08X\n", static_cast(hr)); + break; + } + + DXGI_ADAPTER_DESC1 desc {}; + hardwareAdapter->GetDesc1(&desc); + if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) + continue; + + if (SUCCEEDED(hr = D3D11CreateDeviceProc(hardwareAdapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, deviceFlags, featureLevels, _countof(featureLevels), D3D11_SDK_VERSION, Device.ReleaseAndGetAddressOf(), &createdLevel, DeviceContext.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] D3D11 device created successfully on adapter: %ls\n", desc.Description); + break; + } + } + } + else { + Debug::Log("[RenderDX] IDXGIFactory1 is unavailable: 0x%08X. Falling back to EnumAdapters().\n", static_cast(hr)); + } + } + + if (!Device) { + for (UINT adapterIndex = 0; !Device; ++adapterIndex) { + Microsoft::WRL::ComPtr hardwareAdapter; + hr = Factory->EnumAdapters(adapterIndex, hardwareAdapter.ReleaseAndGetAddressOf()); + if (hr == DXGI_ERROR_NOT_FOUND) + break; + if (FAILED(hr)) { + Debug::Log("[RenderDX] EnumAdapters failed: 0x%08X\n", static_cast(hr)); + break; } - if (SUCCEEDED(hr = FP_D3D12CreateDevice(hardwareAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) - { - Debug::Log("[RenderDX] D3D12 device created successfully on adapter: %ls\n", desc.Description); + + DXGI_ADAPTER_DESC desc {}; + hardwareAdapter->GetDesc(&desc); + + if (SUCCEEDED(hr = D3D11CreateDeviceProc(hardwareAdapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, deviceFlags, featureLevels, _countof(featureLevels), D3D11_SDK_VERSION, Device.ReleaseAndGetAddressOf(), &createdLevel, DeviceContext.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] D3D11 device created successfully on adapter: %ls\n", desc.Description); break; } } } - if (!Device) - { - Debug::Log("[RenderDX] Failed to create D3D12 device on a hardware adapter. Attempting to create WARP device.\n"); - - Microsoft::WRL::ComPtr warpAdapter; - if (FAILED(hr = Factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)))) - { - Debug::Log("[RenderDX] Failed to create WARP adapter: 0x%08X\n", static_cast(hr)); - return false; - } - if (FAILED(hr = FP_D3D12CreateDevice(warpAdapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&Device)))) - { - Debug::Log("[RenderDX] Failed to create WARP adapter: 0x%08X\n", static_cast(hr)); + + if (!Device) { + Debug::Log("[RenderDX] Failed to create D3D11 device on enumerated adapters. Attempting default hardware device.\n"); + + if (SUCCEEDED(hr = D3D11CreateDeviceProc(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, deviceFlags, featureLevels, _countof(featureLevels), D3D11_SDK_VERSION, Device.ReleaseAndGetAddressOf(), &createdLevel, DeviceContext.ReleaseAndGetAddressOf()))) + Debug::Log("[RenderDX] Default D3D11 hardware device created successfully.\n"); + } + + if (!Device) { + Debug::Log("[RenderDX] Failed to create D3D11 device on a hardware adapter. Attempting to create WARP device.\n"); + + if (FAILED(hr = D3D11CreateDeviceProc(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, deviceFlags, featureLevels, _countof(featureLevels), D3D11_SDK_VERSION, Device.ReleaseAndGetAddressOf(), &createdLevel, DeviceContext.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create D3D11 WARP device: 0x%08X\n", static_cast(hr)); return false; } - Debug::Log("[RenderDX] D3D12 WARP device created successfully.\n"); + Debug::Log("[RenderDX] D3D11 WARP device created successfully.\n"); } return Device != nullptr; } -bool DXRenderer::CreateCommandQueue() { +bool DXRenderer::CreateSwapChain() { HRESULT hr = S_OK; - D3D12_COMMAND_QUEUE_DESC queueDesc {}; - queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; - queueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; - queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; - queueDesc.NodeMask = 0; + SwapChain.Reset(); + SwapChainBufferCount = 0; - if (FAILED(hr = Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&CommandQueue)))) { - Debug::Log("[RenderDX] Failed to create command queue: 0x%08X\n", static_cast(hr)); - return false; + Microsoft::WRL::ComPtr factory2; + if (SUCCEEDED(hr = Factory.As(&factory2))) { + if (!CreateFlipSwapChain(factory2.Get(), DXGI_SWAP_EFFECT_FLIP_DISCARD)) + CreateFlipSwapChain(factory2.Get(), DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL); + } + else { + Debug::Log("[RenderDX] IDXGIFactory2 is unavailable: 0x%08X. Falling back to legacy swap chain.\n", static_cast(hr)); } - Debug::Log("[RenderDX] Command queue created successfully.\n"); + if (!SwapChain && !CreateLegacySwapChain()) + return false; + + const UINT fullAssociationFlags = DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_PRINT_SCREEN; + const UINT reducedAssociationFlags = DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER; + hr = Factory->MakeWindowAssociation(Hwnd, fullAssociationFlags); + if (FAILED(hr)) + hr = Factory->MakeWindowAssociation(Hwnd, reducedAssociationFlags); + if (FAILED(hr)) + Debug::Log("[RenderDX] Failed to set window association: 0x%08X\n", static_cast(hr)); + + Debug::Log("[RenderDX] Swap chain created successfully.\n"); return true; } -bool DXRenderer::CreateSwapChain() { +bool DXRenderer::CreateFlipSwapChain(IDXGIFactory2* pFactory2, DXGI_SWAP_EFFECT swapEffect) { HRESULT hr = S_OK; DXGI_SWAP_CHAIN_DESC1 desc {}; @@ -663,390 +663,167 @@ bool DXRenderer::CreateSwapChain() { desc.SampleDesc.Count = 1; desc.SampleDesc.Quality = 0; desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; - desc.BufferCount = kFrameCount; + desc.BufferCount = FrameCount; desc.Scaling = DXGI_SCALING_STRETCH; - desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + desc.SwapEffect = swapEffect; desc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; desc.Flags = 0; Microsoft::WRL::ComPtr swapChain1; - if (FAILED(hr = Factory->CreateSwapChainForHwnd(CommandQueue.Get(), Hwnd, &desc, nullptr, nullptr, &swapChain1))) { - Debug::Log("[RenderDX] Failed to create swap chain: 0x%08X\n", static_cast(hr)); + if (FAILED(hr = pFactory2->CreateSwapChainForHwnd(Device.Get(), Hwnd, &desc, nullptr, nullptr, swapChain1.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create flip swap chain with effect %u: 0x%08X\n", static_cast(swapEffect), static_cast(hr)); return false; } - if (FAILED(hr = Factory->MakeWindowAssociation(Hwnd, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_PRINT_SCREEN))) { - Debug::Log("[RenderDX] Failed to set window association: 0x%08X\n", static_cast(hr)); + if (FAILED(hr = swapChain1.As(&SwapChain))) { + Debug::Log("[RenderDX] Failed to query base swap chain interface: 0x%08X\n", static_cast(hr)); return false; } - if (FAILED(hr = swapChain1->QueryInterface(IID_PPV_ARGS(&SwapChain)))) { - Debug::Log("[RenderDX] Failed to query IDXGISwapChain3 interface: 0x%08X\n", static_cast(hr)); - return false; - } - - Debug::Log("[RenderDX] Swap chain created successfully.\n"); - FrameIndex = SwapChain->GetCurrentBackBufferIndex(); + SwapChainBufferCount = FrameCount; + Debug::Log("[RenderDX] Flip swap chain created with effect %u.\n", static_cast(swapEffect)); return true; } -bool DXRenderer::CreateRtvHeap() { +bool DXRenderer::CreateLegacySwapChain() { HRESULT hr = S_OK; - D3D12_DESCRIPTOR_HEAP_DESC desc {}; - desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; - desc.NumDescriptors = kFrameCount; - desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; - desc.NodeMask = 0; + DXGI_SWAP_CHAIN_DESC desc {}; + desc.BufferDesc.Width = WindowWidth; + desc.BufferDesc.Height = WindowHeight; + desc.BufferDesc.RefreshRate.Numerator = 0; + desc.BufferDesc.RefreshRate.Denominator = 1; + desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED; + desc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + desc.BufferCount = 1; + desc.OutputWindow = Hwnd; + desc.Windowed = TRUE; + desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; + desc.Flags = 0; - if (FAILED(hr = Device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&RtvHeap)))) { - Debug::Log("[RenderDX] Failed to create RTV descriptor heap: 0x%08X\n", static_cast(hr)); + if (FAILED(hr = Factory->CreateSwapChain(Device.Get(), &desc, SwapChain.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create legacy swap chain: 0x%08X\n", static_cast(hr)); return false; } - RtvDescriptorSize = Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); - Debug::Log("[RenderDX] RTV descriptor heap created successfully.\n"); + SwapChainBufferCount = 1; + Debug::Log("[RenderDX] Legacy swap chain created.\n"); return true; } bool DXRenderer::CreateRenderTargetViews() { HRESULT hr = S_OK; - D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = RtvHeap->GetCPUDescriptorHandleForHeapStart(); - for (UINT i = 0; i < kFrameCount; ++i) { - if (FAILED(hr = SwapChain->GetBuffer(i, IID_PPV_ARGS(&RenderTargets[i])))) { - Debug::Log("[RenderDX] Failed to get back buffer %u: 0x%08X\n", i, static_cast(hr)); - return false; - } - Device->CreateRenderTargetView(RenderTargets[i].Get(), nullptr, rtvHandle); - rtvHandle.ptr += RtvDescriptorSize; + Microsoft::WRL::ComPtr backBuffer; + if (FAILED(hr = SwapChain->GetBuffer(0, IID_PPV_ARGS(backBuffer.GetAddressOf())))) { + Debug::Log("[RenderDX] Failed to get back buffer: 0x%08X\n", static_cast(hr)); + return false; } - - Debug::Log("[RenderDX] Render target views created successfully.\n"); - return true; -} - -bool DXRenderer::CreateSrvHeap() { - HRESULT hr = S_OK; - - D3D12_DESCRIPTOR_HEAP_DESC desc {}; - desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; - desc.NumDescriptors = 1; // For surface texture SRV - desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; - desc.NodeMask = 0; - - if (FAILED(hr = Device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&SrvHeap)))) { - Debug::Log("[RenderDX] Failed to create SRV descriptor heap: 0x%08X\n", static_cast(hr)); + if (FAILED(hr = Device->CreateRenderTargetView(backBuffer.Get(), nullptr, RenderTargetView.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create render target view: 0x%08X\n", static_cast(hr)); return false; } - Debug::Log("[RenderDX] SRV descriptor heap created successfully.\n"); + Debug::Log("[RenderDX] Render target view created successfully.\n"); return true; } bool DXRenderer::CreateSurfacePipeline() { - D3D12_DESCRIPTOR_RANGE srvRange {}; - srvRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; - srvRange.NumDescriptors = 1; - srvRange.BaseShaderRegister = 0; - srvRange.RegisterSpace = 0; - srvRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND; - - D3D12_ROOT_PARAMETER rootParam {}; - rootParam.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; - rootParam.DescriptorTable.NumDescriptorRanges = 1; - rootParam.DescriptorTable.pDescriptorRanges = &srvRange; - rootParam.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; - - D3D12_STATIC_SAMPLER_DESC sampler {}; - sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; - sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.MipLODBias = 0.0f; - sampler.MaxAnisotropy = 1; - sampler.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; - sampler.BorderColor = D3D12_STATIC_BORDER_COLOR_OPAQUE_BLACK; - sampler.MinLOD = 0.0f; - sampler.MaxLOD = D3D12_FLOAT32_MAX; - sampler.ShaderRegister = 0; - sampler.RegisterSpace = 0; - sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; - - D3D12_ROOT_SIGNATURE_DESC rootSigDesc {}; - rootSigDesc.NumParameters = 1; - rootSigDesc.pParameters = &rootParam; - rootSigDesc.NumStaticSamplers = 1; - rootSigDesc.pStaticSamplers = &sampler; - rootSigDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; - - Microsoft::WRL::ComPtr rootSigBlob; - Microsoft::WRL::ComPtr errorBlob; - - HRESULT hr = FP_D3D12SerializeRootSignature( - &rootSigDesc, - D3D_ROOT_SIGNATURE_VERSION_1, - &rootSigBlob, - &errorBlob - ); - - if (FAILED(hr)) { - if (errorBlob) - Debug::Log("[RenderDX] Root signature serialization error: %s\n", static_cast(errorBlob->GetBufferPointer())); - else - Debug::Log("[RenderDX] Unknown root signature serialization error.\n"); + HRESULT hr = S_OK; + if (FAILED(hr = Device->CreateVertexShader(SurfaceVertexShaderBytecode, sizeof(SurfaceVertexShaderBytecode), nullptr, VertexShader.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create vertex shader: 0x%08X\n", static_cast(hr)); return false; } - if (FAILED(hr = Device->CreateRootSignature(0, rootSigBlob->GetBufferPointer(), rootSigBlob->GetBufferSize(), IID_PPV_ARGS(&RootSignature)))) { - Debug::Log("[RenderDX] Failed to create root signature: 0x%08X\n", static_cast(hr)); + if (FAILED(hr = Device->CreatePixelShader(SurfacePixelShaderBytecode, sizeof(SurfacePixelShaderBytecode), nullptr, PixelShader.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create pixel shader: 0x%08X\n", static_cast(hr)); return false; } - static constexpr const char* shaderSource = R"( -Texture2D gSurface : register(t0); -SamplerState gSampler : register(s0); - -struct VSOut -{ - float4 position : SV_Position; - float2 uv : TEXCOORD0; -}; - -VSOut VSMain(uint vertexId : SV_VertexID) -{ - VSOut o; - - // Full-screen triangle vertices and UVs - float2 positions[3] = - { - float2(-1.0f, -1.0f), - float2(-1.0f, 3.0f), - float2( 3.0f, -1.0f) - }; - - float2 uvs[3] = - { - float2(0.0f, 1.0f), - float2(0.0f, -1.0f), - float2(2.0f, 1.0f) - }; - - o.position = float4(positions[vertexId], 0.0f, 1.0f); - o.uv = uvs[vertexId]; - - return o; -} - -float4 PSMain(VSOut input) : SV_Target0 -{ - return gSurface.Sample(gSampler, input.uv); -} -)"; - - auto vertexShader = CompileShader(shaderSource, "VSMain", "vs_5_0"); - auto pixelShader = CompileShader(shaderSource, "PSMain", "ps_5_0"); - - D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc {}; - psoDesc.pRootSignature = RootSignature.Get(); - psoDesc.VS.pShaderBytecode = vertexShader->GetBufferPointer(); - psoDesc.VS.BytecodeLength = vertexShader->GetBufferSize(); - psoDesc.PS.pShaderBytecode = pixelShader->GetBufferPointer(); - psoDesc.PS.BytecodeLength = pixelShader->GetBufferSize(); - psoDesc.BlendState.AlphaToCoverageEnable = FALSE; - psoDesc.BlendState.IndependentBlendEnable = FALSE; - D3D12_RENDER_TARGET_BLEND_DESC rtBlend {}; - rtBlend.BlendEnable = FALSE; - rtBlend.LogicOpEnable = FALSE; - rtBlend.SrcBlend = D3D12_BLEND_ONE; - rtBlend.DestBlend = D3D12_BLEND_ZERO; - rtBlend.BlendOp = D3D12_BLEND_OP_ADD; - rtBlend.SrcBlendAlpha = D3D12_BLEND_ONE; - rtBlend.DestBlendAlpha = D3D12_BLEND_ZERO; - rtBlend.BlendOpAlpha = D3D12_BLEND_OP_ADD; - rtBlend.LogicOp = D3D12_LOGIC_OP_NOOP; - rtBlend.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; - psoDesc.BlendState.RenderTarget[0] = rtBlend; - psoDesc.SampleMask = UINT_MAX; - psoDesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; - psoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; - psoDesc.RasterizerState.FrontCounterClockwise = FALSE; - psoDesc.RasterizerState.DepthBias = D3D12_DEFAULT_DEPTH_BIAS; - psoDesc.RasterizerState.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP; - psoDesc.RasterizerState.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS; - psoDesc.RasterizerState.DepthClipEnable = TRUE; - psoDesc.RasterizerState.MultisampleEnable = FALSE; - psoDesc.RasterizerState.AntialiasedLineEnable = FALSE; - psoDesc.RasterizerState.ForcedSampleCount = 0; - psoDesc.RasterizerState.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF; - psoDesc.DepthStencilState.DepthEnable = FALSE; - psoDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO; - psoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_ALWAYS; - psoDesc.DepthStencilState.StencilEnable = FALSE; - psoDesc.DepthStencilState.StencilReadMask = D3D12_DEFAULT_STENCIL_READ_MASK; - psoDesc.DepthStencilState.StencilWriteMask = D3D12_DEFAULT_STENCIL_WRITE_MASK; - psoDesc.DepthStencilState.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP; - psoDesc.DepthStencilState.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP; - psoDesc.DepthStencilState.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP; - psoDesc.DepthStencilState.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS; - psoDesc.DepthStencilState.BackFace = psoDesc.DepthStencilState.FrontFace; - psoDesc.InputLayout = {}; - psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; - psoDesc.NumRenderTargets = 1; - psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; - psoDesc.DSVFormat = DXGI_FORMAT_UNKNOWN; - psoDesc.SampleDesc.Count = 1; - psoDesc.SampleDesc.Quality = 0; - - if (FAILED(hr = Device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&PipelineState)))) { - Debug::Log("[RenderDX] Failed to create pipeline state: 0x%08X\n", static_cast(hr)); + D3D11_SAMPLER_DESC samplerDesc {}; + samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT; + samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.ComparisonFunc = D3D11_COMPARISON_ALWAYS; + samplerDesc.MinLOD = 0.0f; + samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; + if (FAILED(hr = Device->CreateSamplerState(&samplerDesc, SamplerState.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create sampler state: 0x%08X\n", static_cast(hr)); return false; } - Debug::Log("[RenderDX] Pipeline state created successfully.\n"); - return true; -} - -bool DXRenderer::CreateCommandObjects() { - HRESULT hr = S_OK; - - for (UINT i = 0; i < kFrameCount; ++i) { - if (FAILED(hr = Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&CommandAllocators[i])))) { - Debug::Log("[RenderDX] Failed to create command allocator %u: 0x%08X\n", i, static_cast(hr)); - return false; - } - } - - if (FAILED(hr = Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, CommandAllocators[FrameIndex].Get(), PipelineState.Get(), IID_PPV_ARGS(&CommandList)))) { - Debug::Log("[RenderDX] Failed to create command list: 0x%08X\n", static_cast(hr)); + D3D11_RASTERIZER_DESC rasterizerDesc {}; + rasterizerDesc.FillMode = D3D11_FILL_SOLID; + rasterizerDesc.CullMode = D3D11_CULL_NONE; + rasterizerDesc.DepthClipEnable = TRUE; + rasterizerDesc.ScissorEnable = TRUE; + if (FAILED(hr = Device->CreateRasterizerState(&rasterizerDesc, RasterizerState.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create rasterizer state: 0x%08X\n", static_cast(hr)); return false; } - Debug::Log("[RenderDX] Command objects created successfully.\n"); - CommandList->Close(); // Command list needs to be closed before reset in the render loop. - return true; -} - -bool DXRenderer::CreateFenceObjects() { - HRESULT hr = S_OK; - - FenceValues.fill(0); - - if (FAILED(hr = Device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&Fence)))) { - Debug::Log("[RenderDX] Failed to create fence: 0x%08X\n", static_cast(hr)); + D3D11_BLEND_DESC blendDesc {}; + blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + if (FAILED(hr = Device->CreateBlendState(&blendDesc, BlendState.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create blend state: 0x%08X\n", static_cast(hr)); return false; } - FenceValues[FrameIndex] = 1; - FenceEvent = ::CreateEventW(nullptr, FALSE, FALSE, nullptr); - - if (!FenceEvent) { - Debug::Log("[RenderDX] Failed to create fence event: 0x%08X\n", ::GetLastError()); + D3D11_DEPTH_STENCIL_DESC depthStencilDesc {}; + depthStencilDesc.DepthEnable = FALSE; + depthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO; + depthStencilDesc.DepthFunc = D3D11_COMPARISON_ALWAYS; + depthStencilDesc.StencilEnable = FALSE; + if (FAILED(hr = Device->CreateDepthStencilState(&depthStencilDesc, DepthStencilState.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create depth stencil state: 0x%08X\n", static_cast(hr)); return false; } - Debug::Log("[RenderDX] Fence objects created successfully.\n"); + Debug::Log("[RenderDX] Surface pipeline created successfully.\n"); return true; } bool DXRenderer::CreateFixedSurfaceGpuResources() { HRESULT hr = S_OK; - const UINT sourceRowBytes = RenderWidth * sizeof(std::uint16_t); - SurfaceUploadRowPitch = (sourceRowBytes + D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1) & ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1); // Align up - SurfaceUploadBufferSize = static_cast(SurfaceUploadRowPitch) * static_cast(RenderHeight); - - D3D12_HEAP_PROPERTIES defaultHeap {}; - defaultHeap.Type = D3D12_HEAP_TYPE_DEFAULT; - defaultHeap.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; - defaultHeap.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; - defaultHeap.CreationNodeMask = 1; - defaultHeap.VisibleNodeMask = 1; + UINT formatSupport = 0; + if (FAILED(hr = Device->CheckFormatSupport(DXGI_FORMAT_B5G6R5_UNORM, &formatSupport)) || !(formatSupport & D3D11_FORMAT_SUPPORT_TEXTURE2D) || !(formatSupport & D3D11_FORMAT_SUPPORT_SHADER_SAMPLE)) { + Debug::Log("[RenderDX] B5G6R5 texture sampling is not supported by the D3D11 device: 0x%08X\n", static_cast(hr)); + return false; + } - D3D12_RESOURCE_DESC textureDesc {}; - textureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; - textureDesc.Alignment = 0; + D3D11_TEXTURE2D_DESC textureDesc {}; textureDesc.Width = RenderWidth; textureDesc.Height = RenderHeight; - textureDesc.DepthOrArraySize = 1; textureDesc.MipLevels = 1; + textureDesc.ArraySize = 1; textureDesc.Format = DXGI_FORMAT_B5G6R5_UNORM; textureDesc.SampleDesc.Count = 1; textureDesc.SampleDesc.Quality = 0; - textureDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; - textureDesc.Flags = D3D12_RESOURCE_FLAG_NONE; - - SurfaceTextureState = D3D12_RESOURCE_STATE_COPY_DEST; - - if (FAILED(hr = Device->CreateCommittedResource( - &defaultHeap, - D3D12_HEAP_FLAG_NONE, - &textureDesc, - SurfaceTextureState, - nullptr, - IID_PPV_ARGS(&SurfaceTexture) - ))) { - Debug::Log("[RenderDX] Failed to create surface texture resource: 0x%08X\n", static_cast(hr)); - return false; - } - - D3D12_HEAP_PROPERTIES uploadHeap {}; - uploadHeap.Type = D3D12_HEAP_TYPE_UPLOAD; - uploadHeap.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; - uploadHeap.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; - uploadHeap.CreationNodeMask = 1; - uploadHeap.VisibleNodeMask = 1; - - D3D12_RESOURCE_DESC uploadDesc {}; - uploadDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; - uploadDesc.Alignment = 0; - uploadDesc.Width = SurfaceUploadBufferSize; - uploadDesc.Height = 1; - uploadDesc.DepthOrArraySize = 1; - uploadDesc.MipLevels = 1; - uploadDesc.Format = DXGI_FORMAT_UNKNOWN; - uploadDesc.SampleDesc.Count = 1; - uploadDesc.SampleDesc.Quality = 0; - uploadDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; - uploadDesc.Flags = D3D12_RESOURCE_FLAG_NONE; - - for (UINT i = 0; i < kFrameCount; ++i) { - if (FAILED(hr = Device->CreateCommittedResource( - &uploadHeap, - D3D12_HEAP_FLAG_NONE, - &uploadDesc, - D3D12_RESOURCE_STATE_GENERIC_READ, - nullptr, - IID_PPV_ARGS(&SurfaceUploadBuffers[i]) - ))) { - Debug::Log("[RenderDX] Failed to create surface upload buffer %u: 0x%08X\n", i, static_cast(hr)); - return false; - } + textureDesc.Usage = D3D11_USAGE_DYNAMIC; + textureDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + textureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - D3D12_RANGE readRange {}; - readRange.Begin = 0; - readRange.End = 0; - - if (FAILED(SurfaceUploadBuffers[i]->Map(0, &readRange, reinterpret_cast(&SurfaceUploadMapped[i])))) { - Debug::Log("[RenderDX] Failed to map surface upload buffer %u: 0x%08X\n", i, static_cast(hr)); - return false; - } + if (FAILED(hr = Device->CreateTexture2D(&textureDesc, nullptr, SurfaceTexture.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create surface texture: 0x%08X\n", static_cast(hr)); + return false; } - D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc {}; + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc {}; srvDesc.Format = DXGI_FORMAT_B5G6R5_UNORM; - srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; - srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; srvDesc.Texture2D.MostDetailedMip = 0; srvDesc.Texture2D.MipLevels = 1; - srvDesc.Texture2D.PlaneSlice = 0; - srvDesc.Texture2D.ResourceMinLODClamp = 0.0f; - Device->CreateShaderResourceView( - SurfaceTexture.Get(), - &srvDesc, - SrvHeap->GetCPUDescriptorHandleForHeapStart() - ); + if (FAILED(hr = Device->CreateShaderResourceView(SurfaceTexture.Get(), &srvDesc, SurfaceShaderResourceView.ReleaseAndGetAddressOf()))) { + Debug::Log("[RenderDX] Failed to create surface shader resource view: 0x%08X\n", static_cast(hr)); + return false; + } return true; } @@ -1058,7 +835,7 @@ void DXRenderer::UpdateViewportAndScissor() { RenderViewportWidth = static_cast(WindowWidth); RenderViewportHeight = static_cast(WindowHeight); - if (DXRenderOptions::Config().PreserveAspectRatio && RenderWidth > 0 && RenderHeight > 0 && WindowWidth > 0 && WindowHeight > 0) { + if (RenderOptions::Config().PreserveAspectRatio && RenderWidth > 0 && RenderHeight > 0 && WindowWidth > 0 && WindowHeight > 0) { const float scale = std::min( static_cast(WindowWidth) / static_cast(RenderWidth), static_cast(WindowHeight) / static_cast(RenderHeight) @@ -1104,178 +881,69 @@ void DXRenderer::UpdateViewportAndScissor() { } bool DXRenderer::WaitForGpu() { - HRESULT hr = S_OK; - - if (!CommandQueue || !Fence || !FenceEvent) { - return true; // If we don't have the necessary objects, we can't wait, but also can't report an error. - } - - const UINT64 currentFenceValue = FenceValues[FrameIndex]; - if (FAILED(hr = CommandQueue->Signal(Fence.Get(), currentFenceValue))) { - Debug::Log("[RenderDX] Failed to signal command queue for GPU synchronization: 0x%08X\n", static_cast(hr)); - return false; - } + if (!DeviceContext) + return true; - if (FAILED(hr = Fence->SetEventOnCompletion(currentFenceValue, FenceEvent))) { - Debug::Log("[RenderDX] Failed to set event on fence completion for GPU synchronization: 0x%08X\n", static_cast(hr)); - return false; - } - - DWORD waitResult = ::WaitForSingleObjectEx(FenceEvent, INFINITE, FALSE); - if (waitResult != WAIT_OBJECT_0) { - Debug::Log("[RenderDX] Wait for GPU synchronization event failed with error code: 0x%08X\n", ::GetLastError()); - return false; - } - - ++FenceValues[FrameIndex]; + DeviceContext->ClearState(); + DeviceContext->Flush(); return true; } -Microsoft::WRL::ComPtr DXRenderer::CompileShader(std::string_view source, std::string_view entryPoint, std::string_view target) { - Microsoft::WRL::ComPtr shaderBlob; - Microsoft::WRL::ComPtr errorBlob; - - UINT compileFlags = 0; - HRESULT hr = FP_D3DCompile(source.data(), source.length(), nullptr, nullptr, nullptr, entryPoint.data(), target.data(), compileFlags, 0, &shaderBlob, &errorBlob); - - if (FAILED(hr)) { - if (errorBlob) - Debug::Log("[RenderDX] Shader compilation error: %s\n", static_cast(errorBlob->GetBufferPointer())); - else - Debug::Log("[RenderDX] Unknown shader compilation error.\n"); - } - - return shaderBlob; -} - -bool DXRenderer::PopulateCommandListForCPUSurface(const void* pixels, int source_pitch) { - HRESULT hr = S_OK; - - if (FAILED(hr = CommandAllocators[FrameIndex]->Reset())) { - Debug::Log("[RenderDX] Failed to reset command allocator for populating command list: 0x%08X\n", static_cast(hr)); - return false; - } - - if (FAILED(hr = CommandList->Reset(CommandAllocators[FrameIndex].Get(), PipelineState.Get()))) { - Debug::Log("[RenderDX] Failed to reset command list for populating commands: 0x%08X\n", static_cast(hr)); +bool DXRenderer::RenderSurface() { + if (!DeviceContext || !RenderTargetView || !SurfaceShaderResourceView) return false; - } - - UploadSurfaceToGpu(pixels, source_pitch); - - CommandList->RSSetViewports(1, &Viewport); - CommandList->RSSetScissorRects(1, &ScissorRect); - D3D12_RESOURCE_BARRIER backBufferToRenderTarget {}; - backBufferToRenderTarget.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; - backBufferToRenderTarget.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; - backBufferToRenderTarget.Transition.pResource = RenderTargets[FrameIndex].Get(); - backBufferToRenderTarget.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; - backBufferToRenderTarget.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; - backBufferToRenderTarget.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; - CommandList->ResourceBarrier(1, &backBufferToRenderTarget); - - D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = RtvHeap->GetCPUDescriptorHandleForHeapStart(); - rtvHandle.ptr += static_cast(FrameIndex) * static_cast(RtvDescriptorSize); - CommandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr); + auto pRenderTargetView = RenderTargetView.Get(); + DeviceContext->OMSetRenderTargets(1, &pRenderTargetView, nullptr); const float clearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; - CommandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); - CommandList->SetDescriptorHeaps(1, SrvHeap.GetAddressOf()); - CommandList->SetGraphicsRootSignature(RootSignature.Get()); - CommandList->SetGraphicsRootDescriptorTable(0, SrvHeap->GetGPUDescriptorHandleForHeapStart()); - CommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - CommandList->DrawInstanced(3, 1, 0, 0); - - D3D12_RESOURCE_BARRIER backBufferToPresent {}; - backBufferToPresent.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; - backBufferToPresent.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; - backBufferToPresent.Transition.pResource = RenderTargets[FrameIndex].Get(); - backBufferToPresent.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; - backBufferToPresent.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; - backBufferToPresent.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; - - CommandList->ResourceBarrier(1, &backBufferToPresent); - - if (FAILED(hr = CommandList->Close())) { - Debug::Log("[RenderDX] Failed to close command list after populating commands: 0x%08X\n", static_cast(hr)); - return false; - } - - return true; -} - -void DXRenderer::UploadSurfaceToGpu(const void* pixels, int source_pitch) { - auto dstBase = SurfaceUploadMapped[FrameIndex]; - const auto* srcBase = static_cast(pixels); - const UINT sourceRowBytes = RenderWidth * static_cast(sizeof(std::uint16_t)); - for (int y = 0; y < RenderHeight; ++y) { - auto* dstRow = dstBase + static_cast(y) * SurfaceUploadRowPitch; - const auto* srcRow = srcBase + static_cast(y) * source_pitch; - std::memcpy(dstRow, srcRow, sourceRowBytes); - } + DeviceContext->ClearRenderTargetView(pRenderTargetView, clearColor); - TransitionSurfaceTexture(SurfaceTextureState, D3D12_RESOURCE_STATE_COPY_DEST); + DeviceContext->RSSetViewports(1, &Viewport); + DeviceContext->RSSetScissorRects(1, &ScissorRect); + DeviceContext->RSSetState(RasterizerState.Get()); - D3D12_TEXTURE_COPY_LOCATION dstLocation {}; - dstLocation.pResource = SurfaceTexture.Get(); - dstLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; - dstLocation.SubresourceIndex = 0; + const float blendFactor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + DeviceContext->OMSetBlendState(BlendState.Get(), blendFactor, 0xFFFFFFFF); + DeviceContext->OMSetDepthStencilState(DepthStencilState.Get(), 0); - D3D12_TEXTURE_COPY_LOCATION srcLocation {}; - srcLocation.pResource = SurfaceUploadBuffers[FrameIndex].Get(); - srcLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; - srcLocation.PlacedFootprint.Offset = 0; - srcLocation.PlacedFootprint.Footprint.Format = DXGI_FORMAT_B5G6R5_UNORM; - srcLocation.PlacedFootprint.Footprint.Width = RenderWidth; - srcLocation.PlacedFootprint.Footprint.Height = RenderHeight; - srcLocation.PlacedFootprint.Footprint.Depth = 1; - srcLocation.PlacedFootprint.Footprint.RowPitch = SurfaceUploadRowPitch; + DeviceContext->IASetInputLayout(nullptr); + DeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + DeviceContext->VSSetShader(VertexShader.Get(), nullptr, 0); + DeviceContext->PSSetShader(PixelShader.Get(), nullptr, 0); - CommandList->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, nullptr); + auto pSampler = SamplerState.Get(); + DeviceContext->PSSetSamplers(0, 1, &pSampler); - TransitionSurfaceTexture(SurfaceTextureState, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); -} + auto pShaderResourceView = SurfaceShaderResourceView.Get(); + DeviceContext->PSSetShaderResources(0, 1, &pShaderResourceView); + DeviceContext->Draw(3, 0); -void DXRenderer::TransitionSurfaceTexture(D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after) { - if (before == after) { - return; - } + ID3D11ShaderResourceView* pNullShaderResourceView = nullptr; + DeviceContext->PSSetShaderResources(0, 1, &pNullShaderResourceView); - D3D12_RESOURCE_BARRIER barrier {}; - barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; - barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; - barrier.Transition.pResource = SurfaceTexture.Get(); - barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; - barrier.Transition.StateBefore = before; - barrier.Transition.StateAfter = after; - - CommandList->ResourceBarrier(1, &barrier); - - SurfaceTextureState = after; + return true; } -bool DXRenderer::MoveToNextFrame() { - HRESULT hr = S_OK; +bool DXRenderer::UploadSurfaceToGpu(const void* pPixels, int sourcePitch) { + if (!DeviceContext || !SurfaceTexture) + return false; - const UINT64 currentFenceValue = FenceValues[FrameIndex]; - if (FAILED(hr = CommandQueue->Signal(Fence.Get(), currentFenceValue))) { - Debug::Log("[RenderDX] Failed to signal command queue for moving to next frame: 0x%08X\n", static_cast(hr)); + D3D11_MAPPED_SUBRESOURCE mapped {}; + HRESULT hr = DeviceContext->Map(SurfaceTexture.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); + if (FAILED(hr)) { + Debug::Log("[RenderDX] Failed to map surface texture: 0x%08X\n", static_cast(hr)); return false; } - FrameIndex = SwapChain->GetCurrentBackBufferIndex(); - if (Fence->GetCompletedValue() < FenceValues[FrameIndex]) { - if (FAILED(hr = Fence->SetEventOnCompletion(FenceValues[FrameIndex], FenceEvent))) { - Debug::Log("[RenderDX] Failed to set event on fence completion for moving to next frame: 0x%08X\n", static_cast(hr)); - return false; - } - DWORD waitResult = ::WaitForSingleObjectEx(FenceEvent, INFINITE, FALSE); - if (waitResult != WAIT_OBJECT_0) { - Debug::Log("[RenderDX] Wait for fence event failed while moving to next frame with error code: 0x%08X\n", ::GetLastError()); - return false; - } + auto dstBase = static_cast(mapped.pData); + const auto* srcBase = static_cast(pPixels); + const UINT sourceRowBytes = RenderWidth * static_cast(sizeof(std::uint16_t)); + for (int y = 0; y < RenderHeight; ++y) { + auto* dstRow = dstBase + static_cast(y) * mapped.RowPitch; + const auto* srcRow = srcBase + static_cast(y) * sourcePitch; + std::memcpy(dstRow, srcRow, sourceRowBytes); } - FenceValues[FrameIndex] = currentFenceValue + 1; + DeviceContext->Unmap(SurfaceTexture.Get(), 0); return true; } diff --git a/src/Render/Renderer.h b/src/Render/Renderer.h index cb89f04fe7..cdd0afb489 100644 --- a/src/Render/Renderer.h +++ b/src/Render/Renderer.h @@ -3,38 +3,27 @@ #include #include -#include -#include +#include #include -#include -#include -#include - -#include - -#ifdef DEBUG #define DXRENDER_DEBUG 0 -#else -#define DXRENDER_DEBUG 1 -#endif class DXRenderer { public: static DXRenderer& Instance(); - bool CreateMainWindow(HINSTANCE instance, int cmd_show, int width, int height, WNDPROC proc); + bool CreateMainWindow(HINSTANCE instance, int cmdShow, int width, int height, WNDPROC proc); void DestroyMainWindow(); bool IsRendererReady(); - bool CreateRenderer(int width, int height, int bits_per_pixel); + bool CreateRenderer(int width, int height, int bitsPerPixel); void DestroyRenderer(); bool ResizeWindow(int width, int height); void ToggleFullscreen(); - bool UploadSurfaceToTexture(void* surface_data, int source_pitch); + bool UploadSurfaceToTexture(void* pSurfaceData, int sourcePitch); void SetRenderScale(bool scale); bool Present(); @@ -55,42 +44,32 @@ class DXRenderer { bool LoadImports(); void UnloadImports(); + bool CreateFactory(UINT dxgiFactoryFlags); bool CreateDevice(); - bool CreateCommandQueue(); bool CreateSwapChain(); - bool CreateRtvHeap(); + bool CreateFlipSwapChain(IDXGIFactory2* pFactory2, DXGI_SWAP_EFFECT swapEffect); + bool CreateLegacySwapChain(); bool CreateRenderTargetViews(); - bool CreateSrvHeap(); bool CreateSurfacePipeline(); - bool CreateCommandObjects(); - bool CreateFenceObjects(); bool CreateFixedSurfaceGpuResources(); void UpdateViewportAndScissor(); bool WaitForGpu(); - Microsoft::WRL::ComPtr CompileShader(std::string_view source, std::string_view entryPoint, std::string_view target); - bool PopulateCommandListForCPUSurface(const void* pixels, int source_pitch); - void UploadSurfaceToGpu(const void* pixels, int source_pitch); - void TransitionSurfaceTexture(D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after); - bool MoveToNextFrame(); - - HMODULE D3D12Lib { nullptr }; -#if DXRENDER_DEBUG - decltype(&D3D12GetDebugInterface) FP_D3D12GetDebugInterface { nullptr }; -#endif - decltype(&D3D12CreateDevice) FP_D3D12CreateDevice { nullptr }; - decltype(&D3D12SerializeRootSignature) FP_D3D12SerializeRootSignature { nullptr }; - - HMODULE DXGILib; - decltype(&CreateDXGIFactory2) FP_CreateDXGIFactory2 { nullptr }; - - HMODULE D3DCompilerLib { nullptr }; - decltype(&D3DCompile) FP_D3DCompile { nullptr }; - - HWND Hwnd { nullptr }; - int WindowWidth { 0 }; - int WindowHeight { 0 }; + bool RenderSurface(); + bool UploadSurfaceToGpu(const void* pPixels, int sourcePitch); + + HMODULE D3D11Lib { nullptr }; // Loaded d3d11.dll handle. + decltype(&D3D11CreateDevice) D3D11CreateDeviceProc { nullptr }; // D3D11CreateDevice entry point. + + HMODULE DXGILib { nullptr }; // Loaded dxgi.dll handle. + decltype(&CreateDXGIFactory2) CreateDXGIFactory2Proc { nullptr }; // CreateDXGIFactory2 entry point. + decltype(&CreateDXGIFactory1) CreateDXGIFactory1Proc { nullptr }; // CreateDXGIFactory1 entry point. + decltype(&CreateDXGIFactory) CreateDXGIFactoryProc { nullptr }; // CreateDXGIFactory entry point. + + HWND Hwnd { nullptr }; // Main game window handle. + int WindowWidth { 0 }; // Current window width. + int WindowHeight { 0 }; // Current window height. float RenderViewportX { 0.0f }; // Current render viewport left in client coordinates. float RenderViewportY { 0.0f }; // Current render viewport top in client coordinates. float RenderViewportWidth { 0.0f }; // Current render viewport width in client coordinates. @@ -98,45 +77,32 @@ class DXRenderer { RECT WindowedRect {}; // Saved window rectangle before borderless fullscreen. LONG_PTR WindowedStyle { 0 }; // Saved window style before borderless fullscreen. LONG_PTR WindowedExStyle { 0 }; // Saved extended window style before borderless fullscreen. - int RenderWidth { 0 }; - int RenderHeight { 0 }; - UINT RenderPitch { 0 }; - bool ScaleRender { true }; - bool Windowed { true }; + int RenderWidth { 0 }; // Source render surface width. + int RenderHeight { 0 }; // Source render surface height. + UINT RenderPitch { 0 }; // Source render pitch. + bool ScaleRender { true }; // Whether the source surface is scaled to the window. + bool Windowed { true }; // Whether the window is in windowed mode. bool HasWindowedState { false }; // Whether windowed placement has been saved. - UINT FrameIndex { 0 }; - UINT RtvDescriptorSize { 0 }; - - Microsoft::WRL::ComPtr Factory; - Microsoft::WRL::ComPtr Device; - Microsoft::WRL::ComPtr CommandQueue; - Microsoft::WRL::ComPtr SwapChain; - Microsoft::WRL::ComPtr RtvHeap; - - static constexpr UINT kFrameCount = 2; - std::array, kFrameCount> RenderTargets {}; - - std::array, kFrameCount> CommandAllocators {}; - Microsoft::WRL::ComPtr CommandList; - - Microsoft::WRL::ComPtr Fence; - std::array FenceValues {}; - HANDLE FenceEvent; - - D3D12_VIEWPORT Viewport {}; - D3D12_RECT ScissorRect {}; + Microsoft::WRL::ComPtr Factory; // Base DXGI factory used for adapter and swap-chain creation. + Microsoft::WRL::ComPtr Device; // D3D11 device. + Microsoft::WRL::ComPtr DeviceContext; // Immediate D3D11 context. + Microsoft::WRL::ComPtr SwapChain; // Window swap chain. + UINT SwapChainBufferCount { 0 }; // Current swap-chain back-buffer count. - Microsoft::WRL::ComPtr SurfaceTexture; - D3D12_RESOURCE_STATES SurfaceTextureState { D3D12_RESOURCE_STATE_COPY_DEST }; + static constexpr UINT FrameCount = 2; + Microsoft::WRL::ComPtr RenderTargetView; // Back-buffer render target. - std::array, kFrameCount> SurfaceUploadBuffers {}; - std::array SurfaceUploadMapped {}; + D3D11_VIEWPORT Viewport {}; // Current render viewport. + D3D11_RECT ScissorRect {}; // Current render scissor rectangle. - UINT SurfaceUploadRowPitch { 0 }; - UINT64 SurfaceUploadBufferSize { 0 }; + Microsoft::WRL::ComPtr SurfaceTexture; // Dynamic RGB565 source texture. + Microsoft::WRL::ComPtr SurfaceShaderResourceView; // Source texture SRV. - Microsoft::WRL::ComPtr SrvHeap; - Microsoft::WRL::ComPtr RootSignature; - Microsoft::WRL::ComPtr PipelineState; + Microsoft::WRL::ComPtr VertexShader; // Fullscreen triangle vertex shader. + Microsoft::WRL::ComPtr PixelShader; // Surface sampling pixel shader. + Microsoft::WRL::ComPtr SamplerState; // Point sampler for pixel-perfect scaling. + Microsoft::WRL::ComPtr RasterizerState; // Rasterizer state with scissor enabled. + Microsoft::WRL::ComPtr BlendState; // Opaque blend state. + Microsoft::WRL::ComPtr DepthStencilState; // Disabled depth/stencil state. }; diff --git a/src/Render/Surface.cpp b/src/Render/Surface.cpp index 11d56e2cbf..7283f8b522 100644 --- a/src/Render/Surface.cpp +++ b/src/Render/Surface.cpp @@ -5,168 +5,164 @@ #include #include -class DXSurfaceImpl -{ -public: - DXSurfaceImpl(int width, int height); - ~DXSurfaceImpl(); - - int Pitch; - std::unique_ptr Buffer; -}; - -void DXSurface::CTOR(int width, int height) -{ - this->Width = width; - this->Height = height; - this->LockLevel = 0; - this->BytesPerPixel = 2; - ImplRef() = new DXSurfaceImpl(width, height); -} - -void DXSurface::DTOR() -{ - if (ImplRef()) - { - delete ImplRef(); - ImplRef() = nullptr; +#include + +void DXSurface::CTOR(int width, int height) { + Width = width; + Height = height; + LockLevel = 0; + BytesPerPixel = 2; // 16-bit RGB565 color + *InternalGetPitch() = (width * 2 + 256 - 1) & ~(256 - 1); // Keep rows aligned for texture uploads. + *InternalGetBuffer() = YRMemory::Allocate(*InternalGetPitch() * height); +} + +void DXSurface::DTOR() { + if (*InternalGetBuffer()) { + YRMemory::Deallocate(*InternalGetBuffer()); + *InternalGetBuffer() = nullptr; } } -DXSurface::DXSurface(int width, int height) : DSurface { noinit_t{} } -{ +DXSurface::DXSurface(int width, int height) : DSurface { noinit_t{} } { CTOR(width, height); } -DXSurface::~DXSurface() -{ +DXSurface::~DXSurface() { DTOR(); } -bool DXSurface::CopyFromWhole(Surface* pSrc, bool trans, bool same_copy_cpu) -{ - JMP_THIS(0x7BBAF0); +bool DXSurface::CopyFromWhole(Surface* pSrc, bool transparent, bool) { + auto sourceRect = RectangleStruct { 0, 0, pSrc->Width, pSrc->Height }; + auto destRect = RectangleStruct { 0, 0, Width, Height }; + return DXSurface::CopyFromPart(&destRect, pSrc, &sourceRect, transparent, false); } -bool DXSurface::CopyFromPart(RectangleStruct* pClipRect, Surface* pSrc, RectangleStruct* pSrcRect, bool trans, bool same_copy_cpu) -{ - JMP_THIS(0x7BBB90); +bool DXSurface::CopyFromPart(RectangleStruct* pClipRect, Surface* pSrc, RectangleStruct* pSrcRect, bool transparent, bool) { + auto sourceWindow = RectangleStruct { 0, 0, pSrc->Width, pSrc->Height }; + auto destWindow = RectangleStruct { 0, 0, Width, Height }; + return DXSurface::CopyFrom(&destWindow, pClipRect, pSrc, &sourceWindow, pSrcRect, transparent, false); } -bool DXSurface::CopyFrom(RectangleStruct* pClipRect, RectangleStruct* pClipRect2, Surface* pSrc, RectangleStruct* pDestRect, RectangleStruct* pSrcRect, bool trans, bool same_copy_cpu) -{ +bool DXSurface::CopyFrom(RectangleStruct* pClipRect, RectangleStruct* pClipRect2, Surface* pSrc, RectangleStruct* pDestRect, RectangleStruct* pSrcRect, bool transparent, bool) { JMP_THIS(0x7BBCF0); } -bool DXSurface::FillRectEx(RectangleStruct* pClipRect, RectangleStruct* pFillRect, COLORREF nColor) -{ - JMP_THIS(0x7BB050); +bool DXSurface::FillRectEx(RectangleStruct* pClipRect, RectangleStruct* pFillRect, COLORREF nColor) { + if (pFillRect->Width <= 0 || pFillRect->Height <= 0) + return false; + + RectangleStruct rect { 0, 0, Width, Height }; + RectangleStruct windowRect = Drawing::Intersect(*pClipRect, rect); + + rect = *pFillRect; + rect.X += pClipRect->X; + rect.Y += pClipRect->Y; + RectangleStruct clippedRect = Drawing::Intersect(windowRect, rect); + if (clippedRect.Width <= 0 || clippedRect.Height <= 0) + return false; + + auto pBuffer = RawLock(clippedRect.X, clippedRect.Y); + if (!pBuffer) + return false; + + const int pitch = GetPitch(); + for (int y = 0; y < clippedRect.Height; ++y) { + std::fill(reinterpret_cast(pBuffer), reinterpret_cast(pBuffer) + clippedRect.Width, static_cast(nColor)); + pBuffer = reinterpret_cast(pBuffer) + pitch; + } + return true; } -bool DXSurface::FillRect(RectangleStruct* pFillRect, COLORREF nColor) -{ - JMP_THIS(0x7BB020); +bool DXSurface::FillRect(RectangleStruct* pFillRect, COLORREF nColor) { + auto window = RectangleStruct { 0, 0, Width, Height }; + return DXSurface::FillRectEx(&window, pFillRect, nColor); } -bool DXSurface::Fill(COLORREF nColor) -{ - JMP_THIS(0x7BBAB0); +bool DXSurface::Fill(COLORREF nColor) { + auto window = RectangleStruct { 0, 0, Width, Height }; + auto clip = RectangleStruct { 0, 0, Width, Height }; + return DXSurface::FillRectEx(&clip, &window, nColor); } -bool DXSurface::FillRectTrans(RectangleStruct* pClipRect, ColorStruct* pColor, int nOpacity) -{ +bool DXSurface::FillRectTrans(RectangleStruct* pClipRect, ColorStruct* pColor, int nOpacity) { JMP_THIS(0x4BB830); } -bool DXSurface::DrawEllipse(int XOff, int YOff, int CenterX, int CenterY, RectangleStruct Rect, COLORREF nColor) -{ +bool DXSurface::DrawEllipse(int xOffset, int yOffset, int centerX, int centerY, RectangleStruct rect, COLORREF nColor) { JMP_THIS(0x7BB350); } -bool DXSurface::SetPixel(Point2D* pPoint, COLORREF nColor) -{ - JMP_THIS(0x7BAEB0); +bool DXSurface::SetPixel(Point2D* pPoint, COLORREF nColor) { + auto pPixel = RawLock(pPoint->X, pPoint->Y); + reinterpret_cast(pPixel)[0] = static_cast(nColor); + return true; } -COLORREF DXSurface::GetPixel(Point2D* pPoint) -{ - JMP_THIS(0x7BAE60); +COLORREF DXSurface::GetPixel(Point2D* pPoint) { + auto pPixel = RawLock(pPoint->X, pPoint->Y); + return reinterpret_cast(pPixel)[0]; } -bool DXSurface::DrawLineEx(RectangleStruct* pClipRect, Point2D* pStart, Point2D* pEnd, COLORREF nColor) -{ +bool DXSurface::DrawLineEx(RectangleStruct* pClipRect, Point2D* pStart, Point2D* pEnd, COLORREF nColor) { JMP_THIS(0x7BA610); } -bool DXSurface::DrawLine(Point2D* pStart, Point2D* pEnd, COLORREF nColor) -{ - JMP_THIS(0x7BA5E0); +bool DXSurface::DrawLine(Point2D* pStart, Point2D* pEnd, COLORREF nColor) { + auto window = RectangleStruct { 0, 0, Width, Height }; + return DXSurface::DrawLineEx(&window, pStart, pEnd, nColor); } -bool DXSurface::DrawLineColor(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, COLORREF nColor, int startZ, int endZ, bool bUnk) -{ +bool DXSurface::DrawLineColor(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, COLORREF nColor, int startZ, int endZ, bool bUnk) { JMP_THIS(0x4BFD30); } -bool DXSurface::DrawMultiplyingLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, DWORD dwMultiplier, DWORD dwUnk1, DWORD dwUnk2, bool bUnk) -{ +bool DXSurface::DrawMultiplyingLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, DWORD dwMultiplier, DWORD dwUnk1, DWORD dwUnk2, bool bUnk) { JMP_THIS(0x4BBCA0); } -bool DXSurface::DrawSubtractiveLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pColor, DWORD dwUnk1, DWORD dwUnk2, bool bUnk1, bool bUnk2, bool bUkn3, bool bUkn4, float fUkn) -{ +bool DXSurface::DrawSubtractiveLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pColor, DWORD dwUnk1, DWORD dwUnk2, bool bUnk1, bool bUnk2, bool bUkn3, bool bUkn4, float fUkn) { JMP_THIS(0x4BC750); } -bool DXSurface::DrawRGBMultiplyingLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pColor, float Intensity, int zSource, int zTarget) -{ +bool DXSurface::DrawRGBMultiplyingLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pColor, float Intensity, int zSource, int zTarget) { JMP_THIS(0x4BDF00); } -bool DXSurface::PlotLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, bool(__fastcall* fpDrawCallback)(int*)) -{ +bool DXSurface::PlotLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, bool(__fastcall* AddRedrawPoint)(int*)) { JMP_THIS(0x7BAB90); } -bool DXSurface::DrawDashedLine(Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset) -{ +bool DXSurface::DrawDashedLine(Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset) { JMP_THIS(0x7BA8C0); } -bool DXSurface::DrawDashedLine_(Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset, bool bUkn) -{ +bool DXSurface::DrawDashedLine_(Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset, bool bUkn) { JMP_THIS(0x4C0750); } -bool DXSurface::DrawLine_(Point2D* pStart, Point2D* pEnd, int nColor, bool bUnk) -{ +bool DXSurface::DrawLine_(Point2D* pStart, Point2D* pEnd, int nColor, bool bUnk) { JMP_THIS(0x4C0E30); } -bool DXSurface::DrawRectEx(RectangleStruct* pClipRect, RectangleStruct* pDrawRect, int nColor) -{ +bool DXSurface::DrawRectEx(RectangleStruct* pClipRect, RectangleStruct* pDrawRect, int nColor) { JMP_THIS(0x7BADC0); } -bool DXSurface::DrawRect(RectangleStruct* pDrawRect, DWORD dwColor) -{ - JMP_THIS(0x7BAD90); +bool DXSurface::DrawRect(RectangleStruct* pDrawRect, DWORD dwColor) { + auto window = RectangleStruct { 0, 0, Width, Height }; + return DXSurface::DrawRectEx(&window, pDrawRect, dwColor); } -void* DXSurface::Lock(int X, int Y) -{ - if (X >= 0 && Y >= 0) - { +void* DXSurface::Lock(int x, int y) { + if (x >= 0 && y >= 0) { ++LockLevel; - return Impl()->Buffer.get() + Y * Impl()->Pitch + X * GetBytesPerPixel(); + return RawLock(x, y); } return nullptr; } -bool DXSurface::Unlock() -{ - if (LockLevel > 0) - { +bool DXSurface::Unlock() { + if (LockLevel > 0) { --LockLevel; return true; } @@ -174,103 +170,92 @@ bool DXSurface::Unlock() return false; } -bool DXSurface::CanLock(DWORD dwUkn1, DWORD dwUkn2) -{ +bool DXSurface::CanLock(DWORD dwUkn1, DWORD dwUkn2) { return true; } -bool DXSurface::vt_entry_68(DWORD dwUnk1, DWORD dwUnk2) -{ +bool DXSurface::vt_entry_68(DWORD dwUnk1, DWORD dwUnk2) { return true; } -bool DXSurface::IsLocked() -{ +bool DXSurface::IsLocked() { return false; } -int DXSurface::GetBytesPerPixel() -{ +int DXSurface::GetBytesPerPixel() { return 2; } -int DXSurface::GetPitch() -{ - return Impl()->Pitch; +int DXSurface::GetPitch() { + return *InternalGetPitch(); } -RectangleStruct* DXSurface::GetRect(RectangleStruct* pRect) -{ +RectangleStruct* DXSurface::GetRect(RectangleStruct* pRect) { *pRect = { 0, 0, GetWidth(), GetHeight() }; return pRect; } -int DXSurface::GetWidth() -{ +int DXSurface::GetWidth() { return Width; } -int DXSurface::GetHeight() -{ +int DXSurface::GetHeight() { return Height; } -bool DXSurface::IsDSurface() -{ +bool DXSurface::IsDSurface() { return true; } -bool DXSurface::PutPixelClip(Point2D* pPoint, short nUkn, RectangleStruct* pRect) -{ - JMP_THIS(0x7BAF90); +bool DXSurface::PutPixelClip(Point2D* pPoint, short color, RectangleStruct* pRect) { + if (pPoint->X < pRect->X || pPoint->X >= pRect->X + pRect->Width || pPoint->Y < pRect->Y || pPoint->Y >= pRect->Y + pRect->Height) { + return false; + } + + auto pPixel = RawLock(pPoint->X, pPoint->Y); + reinterpret_cast(pPixel)[0] = static_cast(color); + return true; } -short DXSurface::GetPixelClip(Point2D* pPoint, RectangleStruct* pRect) -{ - JMP_THIS(0x7BAF10); +short DXSurface::GetPixelClip(Point2D* pPoint, RectangleStruct* pRect) { + if (pPoint->X < pRect->X || pPoint->X >= pRect->X + pRect->Width || pPoint->Y < pRect->Y || pPoint->Y >= pRect->Y + pRect->Height) { + return 0; + } + + auto pPixel = RawLock(pPoint->X, pPoint->Y); + return reinterpret_cast(pPixel)[0]; } -bool DXSurface::DrawGradientLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pStartColor, ColorStruct* pEndColor, float fStep, int nColor) -{ +bool DXSurface::DrawGradientLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pStartColor, ColorStruct* pEndColor, float fStep, int nColor) { JMP_THIS(0x4BF750); } -bool DXSurface::CanBlit() -{ +bool DXSurface::CanBlit() { return true; } -void* DXSurface::GetBuffer() -{ - return Impl()->Buffer.get(); +void* DXSurface::GetBuffer() { + return *InternalGetBuffer(); } -DXSurfaceImpl::DXSurfaceImpl(int width, int height) -{ - const int sourceRowBytes = width * 2; // 2 bytes per pixel for 16-bit color - Pitch = (sourceRowBytes + 256 - 1) & ~(256 - 1); // Align up to D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - Buffer.reset(new BYTE[Pitch * height]); +BYTE* DXSurface::RawLock(int x, int y) { + return reinterpret_cast(*InternalGetBuffer()) + y * *InternalGetPitch() + x * GetBytesPerPixel(); } -DXSurfaceImpl::~DXSurfaceImpl() {} - -static __forceinline unsigned int Build_Hicolor_Pixel(unsigned int r, unsigned int g, unsigned int b) -{ - return (r >> Drawing::RedShiftRight << Drawing::RedShiftLeft) | - (g >> Drawing::GreenShiftRight << Drawing::GreenShiftLeft) | - (b >> Drawing::BlueShiftRight << Drawing::BlueShiftLeft); +static __forceinline unsigned int BuildHicolorPixel(unsigned int red, unsigned int green, unsigned int blue) { + return (red >> Drawing::RedShiftRight << Drawing::RedShiftLeft) | + (green >> Drawing::GreenShiftRight << Drawing::GreenShiftLeft) | + (blue >> Drawing::BlueShiftRight << Drawing::BlueShiftLeft); } -DXSurface* __fastcall DXSurface::CreatePrimary() -{ +DXSurface* __fastcall DXSurface::CreatePrimary() { Drawing::AllowSoftwareBlitFills = false; Drawing::AllowSoftwareBlitStretch = false; - Debug::Log("[RenderDX] D3D12 surface created as primary surface.\n"); + Debug::Log("[RenderDX] D3D11 surface created as primary surface.\n"); auto surface = new DXSurface(Drawing::RenderWidth, Drawing::RenderHeight); - // RGB565 color shifts Drawing::RedShiftLeft = 11; Drawing::RedShiftRight = 3; Drawing::GreenShiftLeft = 5; @@ -278,9 +263,9 @@ DXSurface* __fastcall DXSurface::CreatePrimary() Drawing::BlueShiftLeft = 0; Drawing::BlueShiftRight = 3; Drawing::ColorMode = RGBMode::RGB565; - Drawing::HalfbrightMask = static_cast(Build_Hicolor_Pixel(127, 127, 127)); - Drawing::QuarterbrightMask = static_cast(Build_Hicolor_Pixel(63, 63, 63)); - Drawing::EighthbrightMask = static_cast(Build_Hicolor_Pixel(31, 31, 31)); + Drawing::HalfbrightMask = static_cast(BuildHicolorPixel(127, 127, 127)); + Drawing::QuarterbrightMask = static_cast(BuildHicolorPixel(63, 63, 63)); + Drawing::EighthbrightMask = static_cast(BuildHicolorPixel(31, 31, 31)); return surface; } diff --git a/src/Render/Surface.h b/src/Render/Surface.h index 51da806d2f..e1cd2f07ba 100644 --- a/src/Render/Surface.h +++ b/src/Render/Surface.h @@ -2,16 +2,14 @@ #include -// CPU render -class DXSurfaceImpl; class DXSurface : public DSurface { private: - DXSurfaceImpl* Impl() const { - return reinterpret_cast(Buffer); + void** InternalGetBuffer() { + return reinterpret_cast(reinterpret_cast(this) + 0x14); } - DXSurfaceImpl*& ImplRef() { - return reinterpret_cast(Buffer); + int* InternalGetPitch() { + return reinterpret_cast(reinterpret_cast(this) + 0x18); } public: @@ -24,30 +22,29 @@ class DXSurface : public DSurface { virtual ~DXSurface() override; - //Surface - virtual bool CopyFromWhole(Surface* pSrc, bool trans, bool same_copy_cpu) override; + virtual bool CopyFromWhole(Surface* pSrc, bool transparent, bool sameCopyCpu) override; virtual bool CopyFromPart( - RectangleStruct* pClipRect, //ignored and retrieved again... + RectangleStruct* pClipRect, Surface* pSrc, - RectangleStruct* pSrcRect, //desired source rect of pSrc ? - bool trans, - bool same_copy_cpu) override; + RectangleStruct* pSrcRect, + bool transparent, + bool sameCopyCpu) override; virtual bool CopyFrom( RectangleStruct* pClipRect, - RectangleStruct* pClipRect2, //again? hmm + RectangleStruct* pClipRect2, Surface* pSrc, - RectangleStruct* pDestRect, //desired dest rect of pSrc ? (stretched? clipped?) - RectangleStruct* pSrcRect, //desired source rect of pSrc ? - bool trans, - bool same_copy_cpu) override; + RectangleStruct* pDestRect, + RectangleStruct* pSrcRect, + bool transparent, + bool sameCopyCpu) override; virtual bool FillRectEx(RectangleStruct* pClipRect, RectangleStruct* pFillRect, COLORREF nColor) override; virtual bool FillRect(RectangleStruct* pFillRect, COLORREF nColor) override; virtual bool Fill(COLORREF nColor) override; - virtual bool FillRectTrans(RectangleStruct* pClipRect, ColorStruct* pColor, int Opacity) override; - virtual bool DrawEllipse(int XOff, int YOff, int CenterX, int CenterY, RectangleStruct Rect, COLORREF nColor) override; + virtual bool FillRectTrans(RectangleStruct* pClipRect, ColorStruct* pColor, int nOpacity) override; + virtual bool DrawEllipse(int xOffset, int yOffset, int centerX, int centerY, RectangleStruct rect, COLORREF nColor) override; virtual bool SetPixel(Point2D* pPoint, COLORREF nColor) override; virtual COLORREF GetPixel(Point2D* pPoint) override; virtual bool DrawLineEx(RectangleStruct* pClipRect, Point2D* pStart, Point2D* pEnd, COLORREF nColor) override; @@ -69,16 +66,13 @@ class DXSurface : public DSurface { RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, ColorStruct* pColor, float Intensity, int zSource, int zTarget) override; - virtual bool PlotLine( - RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, bool(__fastcall* fpDrawCallback)(int*)) override; - virtual bool DrawDashedLine( - Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset) override; - virtual bool DrawDashedLine_( - Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset, bool bUkn) override; + virtual bool PlotLine(RectangleStruct* pRect, Point2D* pStart, Point2D* pEnd, bool(__fastcall* AddRedrawPoint)(int*)) override; + virtual bool DrawDashedLine(Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset) override; + virtual bool DrawDashedLine_(Point2D* pStart, Point2D* pEnd, int nColor, bool* Pattern, int nOffset, bool bUkn) override; virtual bool DrawLine_(Point2D* pStart, Point2D* pEnd, int nColor, bool bUnk) override; virtual bool DrawRectEx(RectangleStruct* pClipRect, RectangleStruct* pDrawRect, int nColor) override; virtual bool DrawRect(RectangleStruct* pDrawRect, DWORD dwColor) override; - virtual void* Lock(int X, int Y) override; + virtual void* Lock(int x, int y) override; virtual bool Unlock() override; virtual bool CanLock(DWORD dwUkn1 = 0, DWORD dwUkn2 = 0) override; virtual bool vt_entry_68(DWORD dwUnk1, DWORD dwUnk2) override; @@ -96,4 +90,7 @@ class DXSurface : public DSurface { virtual bool CanBlit() override; void* GetBuffer(); + +private: + BYTE* RawLock(int x, int y); }; From f857e7f025da83a92d94c3f2e3bc0922a7caa952 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Mon, 18 May 2026 15:55:50 +0800 Subject: [PATCH 09/21] Update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 913d5a2244..810938c168 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ out .venv/ debug.log -Phobos.log \ No newline at end of file +Phobos.log +gamemd.* \ No newline at end of file From e80e9a49d8150eedf2448dd0b7195c447296ea86 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Mon, 18 May 2026 16:05:10 +0800 Subject: [PATCH 10/21] Rename hook files, split WWUI hooks --- Phobos.vcxproj | 9 +- src/Render/Hooks.cpp | 117 ---------------- .../{Hooks.Mouse.cpp => Mouse.Hooks.cpp} | 0 .../{Hooks.Options.cpp => Options.Hooks.cpp} | 0 .../{Hooks.Surface.cpp => Surface.Hooks.cpp} | 0 src/Render/WWUI.Hooks.cpp | 129 ++++++++++++++++++ 6 files changed, 134 insertions(+), 121 deletions(-) rename src/Render/{Hooks.Mouse.cpp => Mouse.Hooks.cpp} (100%) rename src/Render/{Hooks.Options.cpp => Options.Hooks.cpp} (100%) rename src/Render/{Hooks.Surface.cpp => Surface.Hooks.cpp} (100%) create mode 100644 src/Render/WWUI.Hooks.cpp diff --git a/Phobos.vcxproj b/Phobos.vcxproj index d218024677..f21f5037d3 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -267,12 +267,13 @@ - - - + + + + @@ -428,4 +429,4 @@ - + \ No newline at end of file diff --git a/src/Render/Hooks.cpp b/src/Render/Hooks.cpp index 02e66529f3..d8f075ba04 100644 --- a/src/Render/Hooks.cpp +++ b/src/Render/Hooks.cpp @@ -7,125 +7,8 @@ #include #include -#ifdef CALL -#undef CALL -#endif - DEFINE_FUNCTION_JUMP(LJMP, 0x777C30, RenderDX::CreateMainWindow); -static BOOL WINAPI ClientToScreenHook(HWND hWnd, LPPOINT lpPoint) { - return TRUE; -} - -DEFINE_PATCH_TYPED(void*, 0x7E14B8, ClientToScreenHook); - -static void __fastcall CenterWindowIn(HWND window, HWND parent) { - RECT parentRect; - ::GetClientRect(parent, &parentRect); - - if (parent == Game::hWnd) { - parentRect.right = Drawing::RenderWidth; - parentRect.bottom = Drawing::RenderHeight; - } - - ::ClientToScreen(parent, reinterpret_cast(&parentRect)); - ::ClientToScreen(parent, reinterpret_cast(&parentRect.right)); - parentRect.right -= parentRect.left; - parentRect.bottom -= parentRect.top; - - RECT rect; - ::GetClientRect(window, &rect); - ::ClientToScreen(window, reinterpret_cast(&rect)); - ::ClientToScreen(window, reinterpret_cast(&rect.right)); - rect.right -= rect.left; - rect.bottom -= rect.top; - int x = (parentRect.right - rect.right + 1) / 2; - int y = (parentRect.bottom - rect.bottom + 1) / 2; - - x = std::max(x, 0); - y = std::max(y, 0); - - ::SetWindowPos(window, nullptr, x, y, -1, -1, SWP_NOSIZE | SWP_NOZORDER); -} -DEFINE_FUNCTION_JUMP(LJMP, 0x777080, CenterWindowIn); - -static BOOL __fastcall MoveDialog(HWND window, int x, int y) { - int xPos; - int yPos; - - RECT screenRect; - screenRect.left = 0; - screenRect.top = 0; - screenRect.right = Drawing::RenderWidth; - screenRect.bottom = Drawing::RenderHeight; - - ::ClientToScreen(Game::hWnd, reinterpret_cast(&screenRect)); - ::ClientToScreen(Game::hWnd, reinterpret_cast(&screenRect.right)); - - RECT windowRect; - ::GetWindowRect(window, &windowRect); - - windowRect.right -= windowRect.left; - windowRect.bottom -= windowRect.top; - - if (x == -1) - xPos = windowRect.left - screenRect.left; - else - xPos = x; - windowRect.left = xPos; - - if (y == -1) - yPos = windowRect.top - screenRect.top; - else - yPos = y; - windowRect.top = yPos; - - return ::MoveWindow(window, windowRect.left, windowRect.top, windowRect.right, windowRect.bottom, FALSE); -} -DEFINE_FUNCTION_JUMP(LJMP, 0x623170, MoveDialog); - -static BOOL __fastcall WinDialogGetRectangle(HWND hWnd, LPRECT rect) { - BOOL result = ::GetWindowRect(hWnd, rect); - if (result) { - RECT client; - ::GetClientRect(Game::hWnd, &client); - ::ClientToScreen(Game::hWnd, reinterpret_cast(&client)); - rect->left -= client.left; - rect->right -= client.left; - rect->top -= client.top; - rect->bottom -= client.top; - } - return result; -} -DEFINE_FUNCTION_JUMP(LJMP, 0x775690, WinDialogGetRectangle); - -static BOOL __fastcall GetWindowRectHook(HWND hWnd, LPRECT rect) { - return ::GetWindowRect(hWnd, rect); -} -DEFINE_FUNCTION_JUMP(CALL, 0x610E77, GetWindowRectHook); - -static BOOL __fastcall MoveIngameWindowControls(HWND hWnd) { - if (!SessionClass::Instance.CurrentlyInGame) - return FALSE; - - auto parent = ::GetParent(hWnd); - - RECT rect; - RECT parentRect; - if (!parent || !::GetWindowRect(hWnd, &rect) || !::GetWindowRect(parent, &parentRect)) - return FALSE; - - int x = rect.left - parentRect.left + (parentRect.right - parentRect.left - 800) / 2; - int y = rect.top - parentRect.top + (parentRect.bottom - parentRect.top - 600) / 2; - if (x < 0) - x = 0; - if (y < 0) - y = 0; - - return ::MoveWindow(hWnd, x, y, rect.right - rect.left, rect.bottom - rect.top, FALSE); -} -DEFINE_FUNCTION_JUMP(LJMP, 0x60B7A0, MoveIngameWindowControls); - DEFINE_JUMP(LJMP, 0x4A4830, 0x4A4848); // Skip Wait_Blit DEFINE_JUMP(LJMP, 0x4A4780, 0x4A4825); // Skip Set_DD_Palette diff --git a/src/Render/Hooks.Mouse.cpp b/src/Render/Mouse.Hooks.cpp similarity index 100% rename from src/Render/Hooks.Mouse.cpp rename to src/Render/Mouse.Hooks.cpp diff --git a/src/Render/Hooks.Options.cpp b/src/Render/Options.Hooks.cpp similarity index 100% rename from src/Render/Hooks.Options.cpp rename to src/Render/Options.Hooks.cpp diff --git a/src/Render/Hooks.Surface.cpp b/src/Render/Surface.Hooks.cpp similarity index 100% rename from src/Render/Hooks.Surface.cpp rename to src/Render/Surface.Hooks.cpp diff --git a/src/Render/WWUI.Hooks.cpp b/src/Render/WWUI.Hooks.cpp new file mode 100644 index 0000000000..bb37268b62 --- /dev/null +++ b/src/Render/WWUI.Hooks.cpp @@ -0,0 +1,129 @@ +#include + +#include +#include +#include + +#ifdef CALL +#undef CALL +#endif + +static BOOL WINAPI ClientToScreenHook(HWND hWnd, LPPOINT lpPoint) +{ + return TRUE; +} +DEFINE_PATCH_TYPED(void*, 0x7E14B8, ClientToScreenHook); + +static void __fastcall CenterWindowIn(HWND window, HWND parent) +{ + RECT parentRect; + ::GetClientRect(parent, &parentRect); + + if (parent == Game::hWnd) + { + parentRect.right = Drawing::RenderWidth; + parentRect.bottom = Drawing::RenderHeight; + } + + ::ClientToScreen(parent, reinterpret_cast(&parentRect)); + ::ClientToScreen(parent, reinterpret_cast(&parentRect.right)); + parentRect.right -= parentRect.left; + parentRect.bottom -= parentRect.top; + + RECT rect; + ::GetClientRect(window, &rect); + ::ClientToScreen(window, reinterpret_cast(&rect)); + ::ClientToScreen(window, reinterpret_cast(&rect.right)); + rect.right -= rect.left; + rect.bottom -= rect.top; + int x = (parentRect.right - rect.right + 1) / 2; + int y = (parentRect.bottom - rect.bottom + 1) / 2; + + x = std::max(x, 0); + y = std::max(y, 0); + + ::SetWindowPos(window, nullptr, x, y, -1, -1, SWP_NOSIZE | SWP_NOZORDER); +} +DEFINE_FUNCTION_JUMP(LJMP, 0x777080, CenterWindowIn); + +static BOOL __fastcall MoveDialog(HWND window, int x, int y) +{ + int xPos; + int yPos; + + RECT screenRect; + screenRect.left = 0; + screenRect.top = 0; + screenRect.right = Drawing::RenderWidth; + screenRect.bottom = Drawing::RenderHeight; + + ::ClientToScreen(Game::hWnd, reinterpret_cast(&screenRect)); + ::ClientToScreen(Game::hWnd, reinterpret_cast(&screenRect.right)); + + RECT windowRect; + ::GetWindowRect(window, &windowRect); + + windowRect.right -= windowRect.left; + windowRect.bottom -= windowRect.top; + + if (x == -1) + xPos = windowRect.left - screenRect.left; + else + xPos = x; + windowRect.left = xPos; + + if (y == -1) + yPos = windowRect.top - screenRect.top; + else + yPos = y; + windowRect.top = yPos; + + return ::MoveWindow(window, windowRect.left, windowRect.top, windowRect.right, windowRect.bottom, FALSE); +} +DEFINE_FUNCTION_JUMP(LJMP, 0x623170, MoveDialog); + +static BOOL __fastcall WinDialogGetRectangle(HWND hWnd, LPRECT rect) +{ + BOOL result = ::GetWindowRect(hWnd, rect); + if (result) + { + RECT client; + ::GetClientRect(Game::hWnd, &client); + ::ClientToScreen(Game::hWnd, reinterpret_cast(&client)); + rect->left -= client.left; + rect->right -= client.left; + rect->top -= client.top; + rect->bottom -= client.top; + } + return result; +} +DEFINE_FUNCTION_JUMP(LJMP, 0x775690, WinDialogGetRectangle); + +static BOOL __fastcall GetWindowRectHook(HWND hWnd, LPRECT rect) +{ + return ::GetWindowRect(hWnd, rect); +} +DEFINE_FUNCTION_JUMP(CALL, 0x610E77, GetWindowRectHook); + +static BOOL __fastcall MoveIngameWindowControls(HWND hWnd) +{ + if (!SessionClass::Instance.CurrentlyInGame) + return FALSE; + + auto parent = ::GetParent(hWnd); + + RECT rect; + RECT parentRect; + if (!parent || !::GetWindowRect(hWnd, &rect) || !::GetWindowRect(parent, &parentRect)) + return FALSE; + + int x = rect.left - parentRect.left + (parentRect.right - parentRect.left - 800) / 2; + int y = rect.top - parentRect.top + (parentRect.bottom - parentRect.top - 600) / 2; + if (x < 0) + x = 0; + if (y < 0) + y = 0; + + return ::MoveWindow(hWnd, x, y, rect.right - rect.left, rect.bottom - rect.top, FALSE); +} +DEFINE_FUNCTION_JUMP(LJMP, 0x60B7A0, MoveIngameWindowControls); From 781ab10dcd647ba583c180db528c00e5bac424f7 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Tue, 19 May 2026 01:32:30 +0800 Subject: [PATCH 11/21] Fix bink movie not calling present --- src/Render/Hooks.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Render/Hooks.cpp b/src/Render/Hooks.cpp index d8f075ba04..0526b8e7d0 100644 --- a/src/Render/Hooks.cpp +++ b/src/Render/Hooks.cpp @@ -38,7 +38,7 @@ DEFINE_HOOK(0x690228, DXRenderUpdateScreenScoreClassCallBackDelay, 0x6) { return 0; } -DEFINE_NAKED_HOOK(0x5C0477, DXRenderUpdateScreenMovieBlitToScreen) { +DEFINE_NAKED_HOOK(0x5C0477, DXRenderUpdateScreenVQMovieBlitToScreen) { __asm { call dword ptr[edx + 8] mov ecx, dword ptr ds:[0x887308] @@ -51,6 +51,12 @@ DEFINE_NAKED_HOOK(0x5C0477, DXRenderUpdateScreenMovieBlitToScreen) { } } +DEFINE_HOOK(0x432FF1, DXRenderUpdateScreenBinkMovieBlitToScreen, 0x7) +{ + RenderDX::UpdateScreen(DSurface::Primary); + return 0; +} + static LRESULT CALLBACK OwnerDrawWindowProcedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { auto result = reinterpret_cast(0x610CA0)(hWnd, message, wParam, lParam); if (message == WM_PAINT) From e642b025c5bb1bc08010c30495a3d56cc981a51e Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Thu, 28 May 2026 03:13:04 +0800 Subject: [PATCH 12/21] Replace game OwnerDraw implementations --- Phobos.vcxproj | 24 +- src/OwnerDraw/Button.cpp | 823 ++++++++++++++ src/OwnerDraw/ComboBox.cpp | 748 +++++++++++++ src/OwnerDraw/Edit.cpp | 1000 +++++++++++++++++ src/OwnerDraw/GroupBox.cpp | 128 +++ src/OwnerDraw/Input.cpp | 194 ++++ src/OwnerDraw/ListBox.cpp | 1489 +++++++++++++++++++++++++ src/OwnerDraw/OwnerDraw.Hooks.cpp | 22 + src/OwnerDraw/OwnerDraw.Internal.h | 110 ++ src/OwnerDraw/OwnerDraw.cpp | 1643 ++++++++++++++++++++++++++++ src/OwnerDraw/OwnerDraw.h | 24 + src/OwnerDraw/Progress.cpp | 87 ++ src/OwnerDraw/ScrollBar.cpp | 482 ++++++++ src/OwnerDraw/Slider.cpp | 434 ++++++++ src/OwnerDraw/Static.cpp | 586 ++++++++++ src/OwnerDraw/Tab.cpp | 291 +++++ src/Phobos.INI.cpp | 67 ++ src/Phobos.h | 26 + src/Render/Hooks.cpp | 14 - src/Render/WWUI.h | 3 + 20 files changed, 8179 insertions(+), 16 deletions(-) create mode 100644 src/OwnerDraw/Button.cpp create mode 100644 src/OwnerDraw/ComboBox.cpp create mode 100644 src/OwnerDraw/Edit.cpp create mode 100644 src/OwnerDraw/GroupBox.cpp create mode 100644 src/OwnerDraw/Input.cpp create mode 100644 src/OwnerDraw/ListBox.cpp create mode 100644 src/OwnerDraw/OwnerDraw.Hooks.cpp create mode 100644 src/OwnerDraw/OwnerDraw.Internal.h create mode 100644 src/OwnerDraw/OwnerDraw.cpp create mode 100644 src/OwnerDraw/OwnerDraw.h create mode 100644 src/OwnerDraw/Progress.cpp create mode 100644 src/OwnerDraw/ScrollBar.cpp create mode 100644 src/OwnerDraw/Slider.cpp create mode 100644 src/OwnerDraw/Static.cpp create mode 100644 src/OwnerDraw/Tab.cpp create mode 100644 src/Render/WWUI.h diff --git a/Phobos.vcxproj b/Phobos.vcxproj index f21f5037d3..3b7b63e02b 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -264,7 +264,21 @@ - + + + + + + + + + + + + + + + @@ -273,6 +287,7 @@ + @@ -396,7 +411,10 @@ - + + + + @@ -404,6 +422,8 @@ + + diff --git a/src/OwnerDraw/Button.cpp b/src/OwnerDraw/Button.cpp new file mode 100644 index 0000000000..e77a564835 --- /dev/null +++ b/src/OwnerDraw/Button.cpp @@ -0,0 +1,823 @@ +#include "OwnerDraw.Internal.h" + +constexpr UINT OwnerDrawButtonTimerId = 0; +constexpr UINT OwnerDrawButtonTimerInterval = 1000; +constexpr int OwnerDrawButtonTextStyle = 5; +constexpr int OwnerDrawButtonTextAlign = 12; +constexpr BYTE OwnerDrawButtonDisabledOverlayAlpha = 0x80; + +static COLORREF MakeOwnerDrawButtonSideTextColor(BYTE red, WORD greenBlue) +{ + const BYTE green = static_cast(greenBlue & 0xFF); + const BYTE blue = static_cast((greenBlue >> 8) & 0xFF); + const int surfaceColor = Drawing::RGB_To_Int(red, green, blue); + + BYTE outputRed = 0; + BYTE outputGreen = 0; + BYTE outputBlue = 0; + Drawing::Int_To_RGB(surfaceColor, outputRed, outputGreen, outputBlue); + + return static_cast( + outputRed + | (static_cast(outputGreen) << 8) + | ((static_cast(outputBlue) | 0x200) << 16)); +} + +static COLORREF GetDisabledOwnerDrawButtonTextColor() +{ + if (!SessionClass::Instance.CurrentlyInGame || !ScenarioClass::Instance) + return Phobos::UI::ColorDisabledButton; + + switch (ScenarioClass::Instance->PlayerSideIndex) + { + case 0: + return MakeOwnerDrawButtonSideTextColor( + OwnerDraw::ButtonDisabledSide0Red, + OwnerDraw::ButtonDisabledSide0GreenBlue); + + case 1: + return MakeOwnerDrawButtonSideTextColor( + OwnerDraw::ButtonDisabledSide1Red, + OwnerDraw::ButtonDisabledSide1GreenBlue); + + default: + return MakeOwnerDrawButtonSideTextColor( + OwnerDraw::ButtonDisabledSideOtherRed, + OwnerDraw::ButtonDisabledSideOtherGreenBlue); + } +} + +static void EnsureOwnerDrawButtonCache(OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect) +{ + if (data.CacheSurface || !DSurface::Alternate) + return; + + const int width = clientRect.right + 1; + const int height = clientRect.bottom + 1; + if (width <= 0 || height <= 0) + return; + + data.CacheSurface = GameCreate(width, height); + if (!data.CacheSurface) + return; + + ++OwnerDraw::CachedSurfaceCount; + + RectangleStruct destRect { 0, 0, width, height }; + RectangleStruct sourceRect { ownerRect.left, ownerRect.top, width, height }; + CopySurfacePart(data.CacheSurface, destRect, DSurface::Alternate, sourceRect); +} + +static void RestoreOwnerDrawButtonCache(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect) +{ + if (!data.CacheSurface || !DSurface::Alternate) + return; + + const int width = clientRect.right + 1; + const int height = clientRect.bottom + 1; + if (width <= 0 || height <= 0) + return; + + RectangleStruct destRect { ownerRect.left, ownerRect.top, width, height }; + RectangleStruct sourceRect { 0, 0, width, height }; + CopySurfacePart(DSurface::Alternate, destRect, data.CacheSurface, sourceRect); + ::InvalidateRect(hWnd, nullptr, FALSE); +} + +static bool DrawOwnerDrawButtonShape( + OwnerDrawDialogElement& data, + const RectangleStruct& controlRect, + int drawItemState, + LONG windowStyle, + COLORREF& textColor) +{ + ConvertClass* pConvert = nullptr; + SHPStruct* pShape = nullptr; + int frame = 0; + + switch (data.LayoutBand) + { + case 1: + pConvert = OwnerDraw::GetSmallButtonAnimConvert(); + pShape = OwnerDraw::SmallButtonAnimShape; + frame = 2; + if (drawItemState & 1) + frame = 4; + else if (data.AsButton().AlternateFrame()) + frame = 3; + break; + + case 2: + pConvert = OwnerDraw::GetSideButtonConvert(); + pShape = OwnerDraw::SideButtonShape; + if (drawItemState & 1) + frame = 1; + else if (data.AsButton().AlternateFrame()) + frame = 2; + break; + + case 3: + pConvert = OwnerDraw::GetCloseButtonConvert(); + pShape = OwnerDraw::CloseButtonShape; + if (drawItemState & 1) + frame = 1; + else if (data.AsButton().AlternateFrame()) + frame = 2; + break; + + default: + break; + } + + if (windowStyle & WS_DISABLED) + textColor = GetDisabledOwnerDrawButtonTextColor(); + + if (!pConvert || !pShape || !DSurface::Alternate) + return false; + + Point2D position { controlRect.X, controlRect.Y }; + RectangleStruct bounds = DSurface::Alternate->GetRect(); + CC_Draw_Shape( + DSurface::Alternate, + pConvert, + pShape, + frame, + &position, + &bounds, + BlitterFlags::bf_400, + 0, + 0, + ZGradient::Ground, + 1000, + 0, + nullptr, + 0, + 0, + 0); + + return true; +} + +static void DrawOwnerDrawButtonImage( + OwnerDrawDialogElement& data, + const RectangleStruct& controlRect, + int drawItemState) +{ + auto pImage = data.ControlImage; + if (!pImage) + return; + + if ((drawItemState & 1) && data.StateImageSurface) + pImage = data.StateImageSurface; + + RectangleStruct sourceRect { 0, 0, controlRect.Width, controlRect.Height }; + CopySurfacePart(DSurface::Alternate, controlRect, pImage, sourceRect); +} + +static int SelectOwnerDrawButtonSliceIndex(int height) +{ + return height >= 30 ? 1 : 0; +} + +static void DrawOwnerDrawButtonSlices( + HWND hWnd, + OwnerDrawDialogElement& data, + const RECT& clientRect, + const RECT& ownerRect, + RectangleStruct& drawRect, + int drawItemState, + LONG windowStyle) +{ + const bool pressed = (drawItemState & 1) != 0; + char variant = pressed ? 'd' : 'u'; + + if (windowStyle & WS_DISABLED) + { + variant = 'u'; + } + else if (variant == 'd' && OwnerDraw::ButtonSliceVariant == 'u') + { + VocClass::PlayGlobal(RulesClass::Instance->GenericClick, 0x2000, 1.0f); + } + + OwnerDraw::ButtonSliceVariant = variant; + + const int sliceHeights[2] { 24, 30 }; + const int leftSliceWidths[2] { 7, 10 }; + const int rightSliceWidths[2] { 7, 10 }; + const int sliceIndex = SelectOwnerDrawButtonSliceIndex(drawRect.Height); + const int sliceHeight = sliceHeights[sliceIndex]; + const int leftSliceWidth = leftSliceWidths[sliceIndex]; + const int rightSliceWidth = rightSliceWidths[sliceIndex]; + + RestoreOwnerDrawButtonCache(hWnd, data, clientRect, ownerRect); + + drawRect.Y += (drawRect.Height - sliceHeight) / 2; + if (pressed) + drawRect.Y += 2; + + char filename[32] {}; + + std::snprintf(filename, sizeof(filename), "b%c%c_li%d.pcx", variant, 'e', sliceHeight); + if (auto pLeft = GetPCXSurface(filename)) + { + drawRect.Height = pLeft->GetHeight(); + RectangleStruct destRect { drawRect.X, drawRect.Y, leftSliceWidth, sliceHeight }; + RectangleStruct sourceRect { 0, 0, leftSliceWidth, sliceHeight }; + CopySurfacePart(DSurface::Alternate, destRect, pLeft, sourceRect); + } + else + { + drawRect.Height = sliceHeight; + } + + std::snprintf(filename, sizeof(filename), "b%c%c_mi%d.pcx", variant, 'e', sliceHeight); + if (auto pMiddle = GetPCXSurface(filename)) + { + RectangleStruct middleRect + { + drawRect.X + leftSliceWidth, + drawRect.Y, + drawRect.Width - leftSliceWidth - rightSliceWidth, + pMiddle->GetHeight() + }; + + if (middleRect.Width > 0 && middleRect.Height > 0) + BlitTiledPCX(middleRect, DSurface::Alternate, pMiddle, 0, 0); + } + + std::snprintf(filename, sizeof(filename), "b%c%c_ri%d.pcx", variant, 'e', sliceHeight); + if (auto pRight = GetPCXSurface(filename)) + { + const int rightHeight = pRight->GetHeight(); + RectangleStruct destRect + { + drawRect.X + drawRect.Width - rightSliceWidth, + drawRect.Y, + rightSliceWidth, + rightHeight + }; + RectangleStruct sourceRect { 0, 0, rightSliceWidth, rightHeight }; + CopySurfacePart(DSurface::Alternate, destRect, pRight, sourceRect); + } +} + +static void DrawOwnerDrawButtonText( + OwnerDrawDialogElement& data, + const RectangleStruct& drawRect, + int drawItemState, + COLORREF textColor) +{ + if (data.ControlImage || !data.TextBuffer) + return; + + RECT textRect + { + drawRect.X, + drawRect.Y + 1, + drawRect.X + drawRect.Width - 2, + drawRect.Y + drawRect.Height + }; + + if (drawItemState & 1) + { + textRect.left = drawRect.X + 2; + textRect.top += 4; + } + + OwnerDraw::DrawWideText( + DSurface::Alternate, + data.TextBuffer, + &textRect, + data.AsButton().Font(), + textColor, + OwnerDrawButtonTextStyle, + OwnerDrawButtonTextAlign, + 0, + 0, + 0); +} + +static LRESULT PaintOwnerDrawButton(HWND hWnd, OwnerDrawDialogElement& data, LONG windowStyle) +{ + if (data.SkipDraw) + { + ::ValidateRect(hWnd, nullptr); + return 0; + } + + RECT ownerRect {}; + RECT clientRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + ::GetClientRect(hWnd, &clientRect); + + const int width = ownerRect.right - ownerRect.left; + const int height = ownerRect.bottom - ownerRect.top; + RectangleStruct controlRect { ownerRect.left, ownerRect.top, width, height }; + RectangleStruct drawRect = controlRect; + + if (DSurface::Alternate) + { + EnsureOwnerDrawButtonCache(data, clientRect, ownerRect); + + COLORREF textColor = Phobos::UI::ColorTextButton; + const int drawItemState = data.AsButton().DrawItemState(); + if (data.LayoutBand) + { + DrawOwnerDrawButtonShape(data, controlRect, drawItemState, windowStyle, textColor); + } + else if (data.ControlImage) + { + DrawOwnerDrawButtonImage(data, controlRect, drawItemState); + } + else + { + DrawOwnerDrawButtonSlices(hWnd, data, clientRect, ownerRect, drawRect, drawItemState, windowStyle); + } + + DrawOwnerDrawButtonText(data, drawRect, drawItemState, textColor); + + if (!data.LayoutBand && (windowStyle & WS_DISABLED)) + BlendFillRect(controlRect, DSurface::Alternate, 0, OwnerDrawButtonDisabledOverlayAlpha); + } + + ::ValidateRect(hWnd, nullptr); + return 0; +} + +constexpr int CheckboxArtSize = 18; +constexpr int CheckboxTextOffset = 26; +constexpr int CheckboxTextStyle = 4; +constexpr int CheckboxTextAlign = 12; + +static const char* SelectCheckboxArtName(OwnerDrawDialogElement& data) +{ + const bool checked = data.AsCheckbox().CheckState() == BST_CHECKED; + + if (data.AsCheckbox().UseExtendedArt()) + { + if (checked) + return data.AsCheckbox().ArtVariant() ? "cce_i.pcx" : "cce_il.pcx"; + + return data.AsCheckbox().ArtVariant() ? "cce_ir.pcx" : "cue_i.pcx"; + } + + return checked ? "cce_i.pcx" : "cue_i.pcx"; +} + +static LRESULT PaintCheckboxCtrl(HWND hWnd, OwnerDrawDialogElement& data, LONG windowStyle) +{ + if (!DSurface::Alternate) + { + ::ValidateRect(hWnd, nullptr); + return 0; + } + + RECT clientRect {}; + RECT textRect {}; + RECT ownerRect {}; + ::GetClientRect(hWnd, &clientRect); + OwnerDraw::GetRectangle(hWnd, &textRect); + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + const RectangleStruct artDest + { + ownerRect.left, + ownerRect.top, + CheckboxArtSize, + CheckboxArtSize + }; + + if (auto pArt = GetPCXSurface(SelectCheckboxArtName(data))) + { + RectangleStruct sourceRect { 0, 0, pArt->GetWidth(), pArt->GetHeight() }; + CopySurfacePart(DSurface::Alternate, artDest, pArt, sourceRect); + } + + if (windowStyle & WS_DISABLED) + BlendFillRect(artDest, DSurface::Alternate, 0, OwnerDraw::DisabledOverlayAlpha); + + if (data.TextBuffer) + { + textRect.left += CheckboxTextOffset; + const COLORREF textColor = (windowStyle & WS_DISABLED) + ? Phobos::UI::ColorDisabledCheckbox + : Phobos::UI::ColorTextCheckbox; + + OwnerDraw::DrawWideText( + DSurface::Alternate, + data.TextBuffer, + &textRect, + data.AsCheckbox().Font(), + textColor, + CheckboxTextStyle, + CheckboxTextAlign, + 0, + 0, + 0); + } + + ::ValidateRect(hWnd, nullptr); + return 0; +} + +static bool IsInsideCheckboxArt(LPARAM lParam) +{ + return LOWORD(lParam) < CheckboxArtSize && HIWORD(lParam) < CheckboxArtSize; +} + +static void NotifyCheckboxClicked(HWND hWnd, int checkState) +{ + if (RulesClass::Instance) + VocClass::PlayGlobal(RulesClass::Instance->GUICheckboxSound, 0x2000, 1.0f); + + if (const HWND parentHwnd = ::GetParent(hWnd)) + { + const WPARAM command = static_cast( + (::GetWindowLongA(hWnd, GWL_ID) & 0xFFFF) + | ((checkState & 0xFFFF) << 16)); + + ::SendMessageA(parentHwnd, WM_COMMAND, command, reinterpret_cast(hWnd)); + } +} + +constexpr int RadioTextStyle = 5; +constexpr int RadioTextAlign = 12; +constexpr BYTE RadioDisabledOverlayAlpha = 0x80; + +static void EnsureRadioCache(OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect) +{ + if (data.CacheSurface || !DSurface::Alternate) + return; + + const int width = clientRect.right + 1; + const int height = clientRect.bottom + 1; + if (width <= 0 || height <= 0) + return; + + data.CacheSurface = GameCreate(width, height); + if (!data.CacheSurface) + return; + + ++OwnerDraw::CachedSurfaceCount; + + RectangleStruct destRect { 0, 0, width, height }; + RectangleStruct sourceRect { ownerRect.left, ownerRect.top, width, height }; + CopySurfacePart(data.CacheSurface, destRect, DSurface::Alternate, sourceRect); +} + +static void RestoreRadioCache(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect) +{ + if (!data.CacheSurface || !DSurface::Alternate) + return; + + const int width = clientRect.right + 1; + const int height = clientRect.bottom + 1; + if (width <= 0 || height <= 0) + return; + + RectangleStruct destRect { ownerRect.left, ownerRect.top, width, height }; + RectangleStruct sourceRect { 0, 0, width, height }; + CopySurfacePart(DSurface::Alternate, destRect, data.CacheSurface, sourceRect); + ::InvalidateRect(hWnd, nullptr, FALSE); +} + +static int SelectRadioSliceHeight(int controlHeight) +{ + return controlHeight >= 30 ? 30 : 24; +} + +static void DrawRadioImage(OwnerDrawDialogElement& data, const RectangleStruct& controlRect, int selected) +{ + auto pImage = data.ControlImage; + if (selected && data.StateImageSurface) + pImage = data.StateImageSurface; + + if (!pImage) + return; + + RectangleStruct sourceRect { 0, 0, controlRect.Width, controlRect.Height }; + CopySurfacePart(DSurface::Alternate, controlRect, pImage, sourceRect); +} + +static void DrawRadioSlices( + HWND hWnd, + OwnerDrawDialogElement& data, + const RECT& clientRect, + const RECT& ownerRect, + const RectangleStruct& controlRect, + LONG windowStyle, + int selected) +{ + char variant = selected ? 'd' : 'u'; + if (windowStyle & WS_DISABLED) + variant = 'u'; + + const int sliceHeight = SelectRadioSliceHeight(controlRect.Height); + const int leftSliceWidth = 7; + const int rightSliceWidth = 10; + + RestoreRadioCache(hWnd, data, clientRect, ownerRect); + + const int sliceY = controlRect.Y + (controlRect.Height - sliceHeight) / 2 + (selected ? 2 : 0); + char filename[32] {}; + + std::snprintf(filename, sizeof(filename), "b%c%c_li%d.pcx", variant, 'e', sliceHeight); + if (auto pLeft = GetPCXSurface(filename)) + { + RectangleStruct destRect { controlRect.X, sliceY, leftSliceWidth, sliceHeight }; + RectangleStruct sourceRect { 0, 0, leftSliceWidth, sliceHeight }; + CopySurfacePart(DSurface::Alternate, destRect, pLeft, sourceRect); + } + + std::snprintf(filename, sizeof(filename), "b%c%c_mi%d.pcx", variant, 'e', sliceHeight); + if (auto pMiddle = GetPCXSurface(filename)) + { + RectangleStruct middleRect + { + controlRect.X + leftSliceWidth, + sliceY, + controlRect.Width - rightSliceWidth, + pMiddle->GetHeight() + }; + + BlitTiledPCX(middleRect, DSurface::Alternate, pMiddle, 0, 0); + } + + std::snprintf(filename, sizeof(filename), "b%c%c_ri%d.pcx", variant, 'e', sliceHeight); + if (auto pRight = GetPCXSurface(filename)) + { + const int rightHeight = pRight->GetHeight(); + RectangleStruct destRect + { + controlRect.X + controlRect.Width - rightSliceWidth, + sliceY, + rightSliceWidth, + rightHeight + }; + RectangleStruct sourceRect { 0, 0, rightSliceWidth, rightHeight }; + CopySurfacePart(DSurface::Alternate, destRect, pRight, sourceRect); + } + + if (data.TextBuffer) + { + RECT textRect + { + controlRect.X, + sliceY + 1, + controlRect.X + controlRect.Width - 2, + sliceY + controlRect.Height - 2 + }; + + if (selected) + { + textRect.left += 2; + textRect.top += 4; + } + + OwnerDraw::DrawWideText( + DSurface::Alternate, + data.TextBuffer, + &textRect, + data.AsRadio().Font(), + Phobos::UI::ColorTextRadio, + RadioTextStyle, + RadioTextAlign, + 0, + 0, + 0); + } +} + +static LRESULT PaintRadioCtrl(HWND hWnd, OwnerDrawDialogElement& data, LONG windowStyle) +{ + if (!DSurface::Alternate) + { + ::ValidateRect(hWnd, nullptr); + return 0; + } + + RECT ownerRect {}; + RECT clientRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + ::GetClientRect(hWnd, &clientRect); + + const int width = ownerRect.right - ownerRect.left; + const int height = ownerRect.bottom - ownerRect.top; + RectangleStruct controlRect { ownerRect.left, ownerRect.top, width, height }; + + EnsureRadioCache(data, clientRect, ownerRect); + + const int selected = data.AsRadio().CheckState() & 1; + if (data.ControlImage) + { + DrawRadioImage(data, controlRect, selected); + } + else + { + DrawRadioSlices(hWnd, data, clientRect, ownerRect, controlRect, windowStyle, selected); + } + + if (windowStyle & WS_DISABLED) + BlendFillRect(controlRect, DSurface::Alternate, 0, RadioDisabledOverlayAlpha); + + ::ValidateRect(hWnd, nullptr); + return 0; +} + +LRESULT CALLBACK WWUI::OwnerDrawCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + auto forwardOriginal = [&]() -> LRESULT + { + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + }; + + auto pData = FindOwnerDrawData(hWnd); + if (!pData) + return forwardOriginal(); + + auto& data = *pData; + + switch (message) + { + case WM_ACTIVATE: + case WM_KILLFOCUS: + case WM_MOUSEACTIVATE: + return 0; + + case WM_PAINT: + return PaintOwnerDrawButton(hWnd, data, ::GetWindowLongA(hWnd, GWL_STYLE)); + + case WM_TIMER: + data.AsButton().AlternateFrame() = !data.AsButton().AlternateFrame(); + ::InvalidateRect(hWnd, nullptr, TRUE); + return forwardOriginal(); + + case WM_LBUTTONDOWN: + case WM_LBUTTONDBLCLK: + if (data.SkipDraw) + return 0; + + VocClass::PlayGlobal(RulesClass::Instance->GUIMainButtonSound, 0x2000, 1.0f); + return forwardOriginal(); + + case WW_BUTTON_SETANIMATED: + if (lParam == 1) + { + if (!data.AsButton().TimerActive()) + { + data.AsButton().TimerActive() = true; + ::SetTimer(hWnd, OwnerDrawButtonTimerId, OwnerDrawButtonTimerInterval, nullptr); + } + } + else if (data.AsButton().TimerActive()) + { + data.AsButton().TimerActive() = false; + data.AsButton().AlternateFrame() = false; + ::KillTimer(hWnd, OwnerDrawButtonTimerId); + ::InvalidateRect(hWnd, nullptr, TRUE); + } + + return forwardOriginal(); + + default: + return forwardOriginal(); + } +} + +LRESULT CALLBACK WWUI::CheckboxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto pData = FindOwnerDrawData(hWnd); + if (!pData) + return 0; + + auto& data = *pData; + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + auto forwardOriginal = [&]() -> LRESULT + { + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + }; + + switch (message) + { + case BM_GETCHECK: + return data.AsCheckbox().CheckState(); + + case BM_SETCHECK: + data.AsCheckbox().CheckState() = static_cast(wParam); + ::InvalidateRect(hWnd, nullptr, FALSE); + return 0; + + case WM_SETFOCUS: + case WM_KILLFOCUS: + ::InvalidateRect(hWnd, nullptr, FALSE); + return forwardOriginal(); + + case WM_PAINT: + if (!data.AsCheckbox().UseNativePaint()) + return PaintCheckboxCtrl(hWnd, data, ::GetWindowLongA(hWnd, GWL_STYLE)); + + return forwardOriginal(); + + case WM_LBUTTONDOWN: + case WM_LBUTTONDBLCLK: + if (!IsInsideCheckboxArt(lParam)) + return 0; + + data.AsCheckbox().CheckState() = data.AsCheckbox().CheckState() == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED; + ::InvalidateRect(hWnd, nullptr, FALSE); + NotifyCheckboxClicked(hWnd, data.AsCheckbox().CheckState()); + return 0; + + case WW_INITDIALOG: + data.AsCheckbox().CheckState() = static_cast( + CallSelectedHandler(pOriginalWndProc, hWnd, BM_GETCHECK, 0, 0)); + return forwardOriginal(); + + case WW_CHECKBOX_ENABLEEXTENDEDART: + { + const bool enabled = lParam != 0; + const bool oldArtVariant = data.AsCheckbox().ArtVariant(); + data.AsCheckbox().UseExtendedArt() = enabled; + if (oldArtVariant != enabled) + ::InvalidateRect(hWnd, nullptr, FALSE); + + return forwardOriginal(); + } + + case WW_CHECKBOX_SETARTVARIANT: + { + const bool variant = lParam != 0; + const bool oldArtVariant = data.AsCheckbox().ArtVariant(); + data.AsCheckbox().ArtVariant() = variant; + if (oldArtVariant != variant) + ::InvalidateRect(hWnd, nullptr, FALSE); + + return forwardOriginal(); + } + + case WW_CHECKBOX_GETARTVARIANT: + return data.AsCheckbox().ArtVariant(); + + default: + return forwardOriginal(); + } +} + +LRESULT CALLBACK WWUI::RadioCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto pData = FindOwnerDrawData(hWnd); + if (!pData) + return 0; + + auto& data = *pData; + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + auto forwardOriginal = [&]() -> LRESULT + { + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + }; + + switch (message) + { + case BM_GETCHECK: + return data.AsRadio().CheckState(); + + case BM_SETCHECK: + data.AsRadio().CheckState() = static_cast(wParam); + ::InvalidateRect(hWnd, nullptr, TRUE); + return forwardOriginal(); + + case WM_PAINT: + return PaintRadioCtrl(hWnd, data, ::GetWindowLongA(hWnd, GWL_STYLE)); + + case WM_LBUTTONDOWN: + case WM_LBUTTONDBLCLK: + if (data.AsRadio().CheckState()) + return 0; + + data.AsRadio().CheckState() = BST_CHECKED; + ::InvalidateRect(hWnd, nullptr, TRUE); + + if (RulesClass::Instance) + VocClass::PlayGlobal(RulesClass::Instance->GenericClick, 0x2000, 1.0f); + + return forwardOriginal(); + + case WM_LBUTTONUP: + ::LockWindowUpdate(::GetParent(hWnd)); + { + const LRESULT result = forwardOriginal(); + ::LockWindowUpdate(nullptr); + return result; + } + + case WW_INITDIALOG: + data.AsRadio().CheckState() = static_cast( + CallSelectedHandler(pOriginalWndProc, hWnd, BM_GETCHECK, 0, 0)); + return forwardOriginal(); + + default: + return forwardOriginal(); + } +} diff --git a/src/OwnerDraw/ComboBox.cpp b/src/OwnerDraw/ComboBox.cpp new file mode 100644 index 0000000000..d6499da79a --- /dev/null +++ b/src/OwnerDraw/ComboBox.cpp @@ -0,0 +1,748 @@ +#include "OwnerDraw.Internal.h" + +static COLORREF ComboBoxTextColor(bool disabled, bool alternatePalette) +{ + if (alternatePalette) + return disabled ? OwnerDraw::AltDisabledTextColor : OwnerDraw::AltComboTextColor; + + return disabled ? Phobos::UI::ColorDisabledCombobox : Phobos::UI::ColorTextCombobox; +} + +static void SyncComboDropSelectionColor() +{ + OwnerDraw::ListSelectionFillColor = Phobos::UI::ColorSelectionCombobox; +} + +constexpr int ComboBoxArrowWidth = 20; +constexpr int ComboBoxArrowLeftOffset = 19; +constexpr int ComboBoxMaxColorItems = 50; +constexpr int ComboBoxTextEntryInlineBytes = 0; +constexpr int ComboBoxEditListNotificationCode = 0x300; +constexpr int ComboBoxParentEditChangeNotificationCode = 5; + +static bool IsComboBoxDropDownList(HWND hWnd) +{ + return (::GetWindowLongA(hWnd, GWL_STYLE) & 3) == CBS_DROPDOWNLIST; +} + +static bool IsComboBoxDropDown(HWND hWnd) +{ + return (::GetWindowLongA(hWnd, GWL_STYLE) & 3) == CBS_DROPDOWN; +} + +int BitFontHeight(BitFont* pFont) +{ + if (!pFont) + pFont = BitFont::Instance; + + if (!pFont) + return 10; + + return pFont->field_1C; +} + +static void TrimComboTextToWidth(wchar_t* pText, size_t capacity, BitFont* pFont, int maxWidth) +{ + if (!pText || !capacity || maxWidth <= 0) + return; + + pText[capacity - 1] = L'\0'; + size_t length = std::wcslen(pText); + if (!length) + return; + + int textWidth = 0; + int textHeight = 0; + if (!pFont) + pFont = BitFont::Instance; + + while (length > 0 && pFont) + { + pFont->GetTextDimension(pText, &textWidth, &textHeight, 0); + if (textWidth <= maxWidth) + break; + + --length; + pText[length] = L'\0'; + if (length + 3 < capacity) + std::wcscat(pText, L"..."); + } +} + +static WWUIComboBoxItem* AllocateComboBoxItem(OwnerDrawDialogElement& data, const wchar_t* pText, bool isWide) +{ + if (!pText) + pText = L""; + + const size_t length = std::wcslen(pText); + const size_t bytes = sizeof(WWUIComboBoxItem) + (length + 1) * sizeof(wchar_t) + ComboBoxTextEntryInlineBytes; + auto pEntry = static_cast(YRMemory::Allocate(bytes)); + if (!pEntry) + return nullptr; + + pEntry->Next = data.AsComboBox().TextEntries(); + pEntry->ItemData = 0; + pEntry->Text = reinterpret_cast(reinterpret_cast(pEntry) + sizeof(WWUIComboBoxItem)); + pEntry->IsWideText = isWide ? 1 : 0; + std::wcscpy(pEntry->Text, pText); + data.AsComboBox().TextEntries() = pEntry; + return pEntry; +} + +static void RemoveComboBoxItem(OwnerDrawDialogElement& data, WWUIComboBoxItem* pEntry) +{ + if (!pEntry) + return; + + WWUIComboBoxItem* pPrevious = nullptr; + for (auto pCurrent = data.AsComboBox().TextEntries(); pCurrent; pCurrent = pCurrent->Next) + { + if (pCurrent != pEntry) + { + pPrevious = pCurrent; + continue; + } + + if (pPrevious) + pPrevious->Next = pCurrent->Next; + else + data.AsComboBox().TextEntries() = pCurrent->Next; + + YRMemory::Deallocate(pCurrent); + return; + } +} + +static WWUIComboBoxItem* GetComboBoxItem(WNDPROC pOriginalWndProc, HWND hWnd, int index) +{ + const auto result = CallSelectedHandler(pOriginalWndProc, hWnd, CB_GETITEMDATA, index, 0); + if (result == CB_ERR || !result) + return nullptr; + + return reinterpret_cast(result); +} + +static LRESULT ForwardComboTextMessageToEditList(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (IsComboBoxDropDownList(hWnd)) + return CallSelectedHandler(FindWindowProc(OwnerDraw::DialogProcs, hWnd), hWnd, message, wParam, lParam); + + LRESULT result = 0; + for (HWND child = ::GetWindow(hWnd, GW_CHILD); child; child = ::GetWindow(child, GW_HWNDNEXT)) + { + char className[16] {}; + ::GetClassNameA(child, className, sizeof(className)); + if (_strcmpi(className, "listbox")) + continue; + + if (message == WM_SETFOCUS) + { + ::SetFocus(child); + result = 0; + } + else + { + const UINT forwardedMessage = message == CB_LIMITTEXT ? EM_LIMITTEXT : message; + result = ::SendMessageA(child, forwardedMessage, wParam, lParam); + } + } + + return result; +} + +static int ResolveComboBorderColor(COLORREF color, bool disabledColor) +{ + if (color == static_cast(-1)) + return disabledColor ? 0 : -1; + + return ConvertRGBToSurfaceColor(color); +} + +static void DrawComboDropButton(const RectangleStruct& rect, bool dropped, bool alternatePalette) +{ + DrawScrollArrow(DSurface::Alternate, rect, dropped, dropped, alternatePalette); +} + +static void PaintComboBox(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect, WNDPROC pOriginalWndProc) +{ + if (!DSurface::Alternate) + return; + + auto pFont = data.AsComboBox().Font() ? data.AsComboBox().Font() : BitFont::Instance; + const bool dropped = ::SendMessageA(hWnd, CB_GETDROPPEDSTATE, 0, 0) != 0; + const int width = ownerRect.right - ownerRect.left; + const int height = ownerRect.bottom - ownerRect.top; + + RectangleStruct comboRect + { + ownerRect.left, + ownerRect.top, + width, + 24 + }; + + RectangleStruct localRect { 0, 0, width, height }; + RectangleStruct parentSourceRect = localRect; + OwnerDrawDialogElement* pParentData = nullptr; + if (const HWND parentHwnd = ::GetParent(hWnd)) + { + pParentData = FindOwnerDrawData(parentHwnd); + if (pParentData) + { + RECT parentRect {}; + OwnerDraw::GetRectangle(parentHwnd, &parentRect); + + if (pParentData->CacheSurface) + { + parentSourceRect.X = ownerRect.left - parentRect.left; + parentSourceRect.Y = ownerRect.top - parentRect.top; + } + } + } + EnsureScrollBarCache(data, pParentData, localRect, parentSourceRect); + + OwnerDraw::CopyDimmedBackground(&comboRect, hWnd, static_cast(data.Alpha)); + BlendFillRect(comboRect, DSurface::Alternate, 0, static_cast(data.Alpha)); + + const LONG style = ::GetWindowLongA(hWnd, GWL_STYLE); + const bool disabled = (style & WS_DISABLED) != 0; + const bool alternatePalette = data.AsComboBox().UseAlternatePalette(); + const COLORREF borderColor = alternatePalette + ? (disabled ? OwnerDraw::AltDisabledBorderColor : OwnerDraw::AltBorderColor) + : (disabled ? OwnerDraw::DisabledBorderColor : OwnerDraw::DefaultBorderColor); + + DrawBeveledBorder(DSurface::Alternate, comboRect, 2, ResolveComboBorderColor(borderColor, disabled)); + + RectangleStruct textAreaRect { comboRect.X, comboRect.Y, comboRect.Width - ComboBoxArrowWidth, comboRect.Height }; + RectangleStruct buttonRect { ownerRect.right - ComboBoxArrowLeftOffset, comboRect.Y + 1, comboRect.Width, comboRect.Height }; + DrawComboDropButton(buttonRect, dropped, alternatePalette); + + if (disabled) + BlendFillRect(comboRect, DSurface::Alternate, 0, static_cast(data.Alpha)); + + if ((style & 3) != CBS_DROPDOWNLIST) + { + ::ValidateRect(hWnd, nullptr); + return; + } + + const int selectedIndex = static_cast(CallSelectedHandler(pOriginalWndProc, hWnd, CB_GETCURSEL, 0, 0)); + wchar_t textBuffer[0x100] {}; + if (auto pItem = GetComboBoxItem(pOriginalWndProc, hWnd, selectedIndex)) + { + std::wcsncpy(textBuffer, pItem->Text ? pItem->Text : L"", std::size(textBuffer) - 1); + } + + COLORREF textColor = ComboBoxTextColor(disabled, alternatePalette); + if (data.AsComboBox().UseItemColorOverrides() + && selectedIndex >= 0 + && selectedIndex < ComboBoxMaxColorItems + && data.AsComboBox().ItemColorOverrides()[selectedIndex] >= 0) + { + textColor = static_cast(data.AsComboBox().ItemColorOverrides()[selectedIndex]); + auto fillRect = textAreaRect; + InsetSurfaceRect(fillRect, 2, 2); + DSurface::Alternate->FillRect(&fillRect, ConvertRGBToSurfaceColor(textColor)); + } + + TrimComboTextToWidth(textBuffer, std::size(textBuffer), pFont, clientRect.right - ComboBoxArrowWidth); + + RECT textRect + { + ownerRect.left + 2, + ownerRect.top, + ownerRect.right, + ownerRect.bottom + }; + + OwnerDraw::DrawWideText(DSurface::Alternate, textBuffer, &textRect, pFont, textColor, 4, 12, 0, 0, 0); + ::ValidateRect(hWnd, nullptr); +} + +static LRESULT AddOrInsertComboString( + OwnerDrawDialogElement& data, + WNDPROC pOriginalWndProc, + HWND hWnd, + UINT message, + WPARAM wParam, + LPARAM lParam, + bool wideText) +{ + char narrowText[5120] {}; + wchar_t wideBuffer[5120] {}; + + const LPARAM nativeTextParam = [&]() -> LPARAM + { + if (wideText) + { + const auto pWideText = reinterpret_cast(lParam); + std::wcsncpy(wideBuffer, pWideText ? pWideText : L"", std::size(wideBuffer) - 1); + WideToCharString(narrowText, std::size(narrowText), wideBuffer); + return reinterpret_cast(narrowText); + } + + const auto pText = reinterpret_cast(lParam); + std::strncpy(narrowText, pText ? pText : "", std::size(narrowText) - 1); + CharToWideString(wideBuffer, std::size(wideBuffer), narrowText); + return reinterpret_cast(narrowText); + }(); + + const bool add = message == WW_CB_ADDSTRINGA || message == WW_CB_ADDSTRINGW; + const UINT nativeMessage = add ? CB_ADDSTRING : CB_INSERTSTRING; + const WPARAM nativeIndex = add ? 0 : wParam; + const auto nativeResult = CallSelectedHandler(pOriginalWndProc, hWnd, nativeMessage, nativeIndex, nativeTextParam); + if (nativeResult == CB_ERR || nativeResult == CB_ERRSPACE) + return nativeResult; + + const int itemIndex = static_cast(nativeResult); + auto pEntry = AllocateComboBoxItem(data, wideBuffer, wideText); + if (!pEntry) + { + CallSelectedHandler(pOriginalWndProc, hWnd, CB_DELETESTRING, itemIndex, 0); + return CB_ERRSPACE; + } + + const auto setDataResult = CallSelectedHandler( + pOriginalWndProc, + hWnd, + CB_SETITEMDATA, + itemIndex, + reinterpret_cast(pEntry)); + + if (setDataResult == CB_ERR || setDataResult == CB_ERRSPACE) + { + CallSelectedHandler(pOriginalWndProc, hWnd, CB_DELETESTRING, itemIndex, 0); + RemoveComboBoxItem(data, pEntry); + return setDataResult; + } + + return itemIndex; +} + +static LRESULT FindComboString(OwnerDrawDialogElement& data, WNDPROC pOriginalWndProc, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, bool wideText, bool exact, bool select) +{ + (void)data; + (void)message; + + wchar_t needle[5120] {}; + if (wideText) + { + const auto pText = reinterpret_cast(lParam); + std::wcsncpy(needle, pText ? pText : L"", std::size(needle) - 1); + } + else + { + CharToWideString(needle, std::size(needle), reinterpret_cast(lParam)); + } + + const int count = static_cast(CallSelectedHandler(pOriginalWndProc, hWnd, CB_GETCOUNT, 0, 0)); + if (count == CB_ERR) + return 0; + + int index = static_cast(wParam); + if (index < 0) + index = 0; + + if (index >= count) + return CB_ERR; + + const size_t needleLength = std::wcslen(needle); + for (; index < count; ++index) + { + const auto pEntry = GetComboBoxItem(pOriginalWndProc, hWnd, index); + const wchar_t* pText = pEntry && pEntry->Text ? pEntry->Text : L""; + const bool match = exact + ? _wcsicmp(needle, pText) == 0 + : _wcsnicmp(needle, pText, needleLength) == 0; + + if (!match) + continue; + + if (select) + return ::SendMessageA(hWnd, CB_SETCURSEL, index, 0); + + return index; + } + + return CB_ERR; +} + +static LRESULT GetComboText(WNDPROC pOriginalWndProc, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, bool wideOutput) +{ + if (lParam && message != WW_CB_GETITEMTEXTFORMAT && message != CB_GETLBTEXTLEN) + { + if (wideOutput) + *reinterpret_cast(lParam) = L'\0'; + else + *reinterpret_cast(lParam) = '\0'; + } + + if (static_cast(wParam) == -1) + return 0; + + auto pEntry = GetComboBoxItem(pOriginalWndProc, hWnd, static_cast(wParam)); + if (!pEntry) + return 0; + + if (message == WW_CB_GETITEMTEXTFORMAT) + return pEntry->IsWideText; + + const wchar_t* pText = pEntry->Text ? pEntry->Text : L""; + const auto length = static_cast(std::wcslen(pText)); + + if (message == WW_CB_GETLBTEXTA || message == WW_CB_GETLBTEXTW) + { + if (lParam) + { + if (wideOutput) + std::wcscpy(reinterpret_cast(lParam), pText); + else + WideToCharString(reinterpret_cast(lParam), static_cast(length + 1), pText); + } + } + + return length < 0 ? 0 : length; +} + +static LRESULT SetComboSelection(OwnerDrawDialogElement& data, WNDPROC pOriginalWndProc, HWND hWnd, WPARAM wParam) +{ + const int selection = static_cast(wParam); + data.AsComboBox().CurrentSelection() = selection; + + if (selection == -1) + { + ::SendMessageA(hWnd, WW_SETTEXTA, 0, reinterpret_cast("")); + } + else if (auto pEntry = GetComboBoxItem(pOriginalWndProc, hWnd, selection)) + { + if (pEntry->IsWideText) + { + ::SendMessageA(hWnd, WW_SETTEXTW, 0, reinterpret_cast(pEntry->Text ? pEntry->Text : L"")); + } + else + { + char buffer[5120] {}; + WideToCharString(buffer, std::size(buffer), pEntry->Text ? pEntry->Text : L""); + ::SendMessageA(hWnd, WW_SETTEXTA, 0, reinterpret_cast(buffer)); + } + } + + if (IsComboBoxDropDown(hWnd)) + return 0; + + ::InvalidateRect(hWnd, nullptr, FALSE); + return CallSelectedHandler(pOriginalWndProc, hWnd, CB_SETCURSEL, wParam, 0); +} + +static void CloseComboDropDown(OwnerDrawDialogElement& data, HWND hWnd) +{ + const HWND dropHwnd = data.AsComboBox().DropDownHwnd(); + if (!dropHwnd) + return; + + ::ReleaseCapture(); + if (const HWND parentHwnd = ::GetParent(hWnd)) + ::SendMessageA(parentHwnd, WW_BRINGTOTOP, reinterpret_cast(dropHwnd), 0); + + ::DestroyWindow(dropHwnd); + CleanupDestroyedWindow(dropHwnd); + data.AsComboBox().DropDownHwnd() = nullptr; +} + +static LRESULT OpenComboDropDown(OwnerDrawDialogElement& data, HWND hWnd, const RECT& clientRect, const RECT& ownerRect) +{ + if (data.AsComboBox().DropDownHwnd()) + return 1; + + SyncComboDropSelectionColor(); + + const HWND parentHwnd = ::GetParent(hWnd); + if (!parentHwnd) + return 1; + + RECT parentRect {}; + OwnerDraw::GetRectangle(parentHwnd, &parentRect); + + RECT dropRect {}; + ::SendMessageA(hWnd, CB_GETDROPPEDCONTROLRECT, 0, reinterpret_cast(&dropRect)); + if (dropRect.bottom > parentRect.bottom) + dropRect.bottom = parentRect.bottom; + + int itemHeight = static_cast(::SendMessageA(hWnd, CB_GETITEMHEIGHT, 0, 0)); + if (itemHeight <= 0) + itemHeight = 1; + + int dropHeight = dropRect.bottom - dropRect.top; + int visibleByHeight = (dropHeight - 1) / itemHeight; + int itemCount = static_cast(::SendMessageA(hWnd, CB_GETCOUNT, 0, 0)); + if (itemCount < 1) + itemCount = 1; + + const int maxVisibleItems = data.AsComboBox().MaxVisibleDropItems(); + if (maxVisibleItems > 0 && itemCount >= maxVisibleItems) + { + dropHeight = maxVisibleItems * itemHeight + 1; + } + else if (visibleByHeight > itemCount) + { + dropHeight = itemCount * itemHeight + 1; + } + + if (ownerRect.bottom + dropHeight > parentRect.bottom) + dropHeight = parentRect.bottom - ownerRect.bottom; + + dropHeight -= dropHeight % itemHeight; + if (dropHeight <= 0) + dropHeight = itemHeight; + + const int x = ownerRect.left - parentRect.left; + const int y = ownerRect.top - parentRect.top + clientRect.bottom + 1; + const int width = clientRect.right - clientRect.left; + + const HWND dropHwnd = ::CreateWindowExA( + 0, + "ComboDropWin", + nullptr, + WS_CHILD, + x, + y, + width, + dropHeight, + parentHwnd, + nullptr, + Game::hInstance, + hWnd); + + if (!dropHwnd) + return 1; + + if (!FindOwnerDrawData(dropHwnd)) + { + OwnerDrawDialogElement dropData; + dropData.AsComboBox().Font() = BitFont::Instance; + dropData.ControlType = WWControlType::Default; + + OwnerDraw::Dialogs[dropHwnd] = dropData; + } + + SessionIpb::RegisterHwnd(dropHwnd); + SessionIpb::RegisterHwnd(dropHwnd); + + ::SendMessageA(dropHwnd, WW_DROPDOWN_INITIALIZE, 0, 0); + ::SendMessageA(parentHwnd, WW_BRINGTOTOP, reinterpret_cast(dropHwnd), 1); + ::SetCapture(dropHwnd); + ::ShowWindow(dropHwnd, SW_SHOWNORMAL); + data.AsComboBox().DropDownHwnd() = dropHwnd; + return 1; +} + +LRESULT CALLBACK WWUI::ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto pData = FindOwnerDrawData(hWnd); + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + if (!pData) + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + + auto& data = *pData; + + RECT clientRect {}; + ::GetClientRect(hWnd, &clientRect); + + RECT ownerRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + auto forwardOriginal = [&]() -> LRESULT + { + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + }; + + auto handleItemData = [&]() -> LRESULT + { + auto pEntry = GetComboBoxItem(pOriginalWndProc, hWnd, static_cast(wParam)); + if (!pEntry) + return CB_ERR; + + if (message == CB_GETITEMDATA || message == WW_GETITEMDATA) + return pEntry->ItemData; + + pEntry->ItemData = static_cast(lParam); + return reinterpret_cast(pEntry); + }; + + switch (message) + { + case WM_DESTROY: + ::SendMessageA(hWnd, CB_SHOWDROPDOWN, 0, 0); + return forwardOriginal(); + + case WM_PAINT: + PaintComboBox(hWnd, data, clientRect, ownerRect, pOriginalWndProc); + return 0; + + case WM_ERASEBKGND: + return 0; + + case WM_LBUTTONDOWN: + case WM_LBUTTONDBLCLK: + if (RulesClass::Instance) + VocClass::PlayGlobal(RulesClass::Instance->GUIComboOpenSound, 0x2000, 1.0f); + + if (LOWORD(lParam) > static_cast(clientRect.right - ComboBoxArrowWidth)) + { + const bool dropped = ::SendMessageA(hWnd, CB_GETDROPPEDSTATE, 0, 0) == 1; + ::PostMessageA(hWnd, CB_SHOWDROPDOWN, dropped ? 0 : 1, 0); + } + return 0; + + case WM_SETFOCUS: + case WM_SETTEXT: + case WM_GETTEXT: + case WM_GETTEXTLENGTH: + case CB_LIMITTEXT: + return ForwardComboTextMessageToEditList(hWnd, message, wParam, lParam); + + case WM_DELETEITEM: + if (const auto pDeleteItem = reinterpret_cast(lParam)) + { + RemoveComboBoxItem(data, reinterpret_cast(pDeleteItem->itemData)); + if (data.AsComboBox().CurrentSelection() == static_cast(pDeleteItem->itemID)) + data.AsComboBox().CurrentSelection() = -1; + } + return forwardOriginal(); + + case WM_COMMAND: + if (HIWORD(wParam) == ComboBoxEditListNotificationCode && !IsComboBoxDropDownList(hWnd)) + { + if (const HWND parentHwnd = ::GetParent(hWnd)) + { + const WPARAM command = static_cast( + (::GetDlgCtrlID(hWnd) & 0xFFFF) + | (ComboBoxParentEditChangeNotificationCode << 16)); + ::SendMessageA(parentHwnd, WM_COMMAND, command, reinterpret_cast(hWnd)); + } + return 0; + } + break; + + case CB_GETCURSEL: + return data.AsComboBox().CurrentSelection(); + + case CB_GETLBTEXTLEN: + return GetComboText(pOriginalWndProc, hWnd, message, wParam, lParam, true); + + case CB_SETCURSEL: + return SetComboSelection(data, pOriginalWndProc, hWnd, wParam); + + case CB_SHOWDROPDOWN: + if (wParam) + return OpenComboDropDown(data, hWnd, clientRect, ownerRect); + + CloseComboDropDown(data, hWnd); + return 1; + + case CB_GETITEMDATA: + case CB_SETITEMDATA: + case WW_GETITEMDATA: + case WW_SETITEMDATA: + return handleItemData(); + + case WW_INITDIALOG: + { + const int fontHeight = BitFontHeight(data.AsComboBox().Font()); + if (!data.AsComboBox().HeightInitialized() + || ::SendMessageA(hWnd, CB_GETITEMHEIGHT, 0, 0) != fontHeight + 6) + { + ::SendMessageA(hWnd, CB_SETITEMHEIGHT, static_cast(-1), fontHeight + 2); + ::SendMessageA(hWnd, CB_SETITEMHEIGHT, 0, fontHeight + 6); + data.AsComboBox().HeightInitialized() = 1; + } + + data.AsComboBox().CurrentSelection() = -1; + std::memset(data.AsComboBox().ItemColorOverrides(), 0xFF, sizeof(int) * ComboBoxMaxColorItems); + return 0; + } + + case WW_SETCOLOR: + if (wParam <= ComboBoxMaxColorItems) + data.AsComboBox().ItemColorOverrides()[wParam] = static_cast(lParam); + return forwardOriginal(); + + case WW_SETTEXTW: + case WW_GETTEXTW: + case WW_SETTEXTA: + case WW_GETTEXTA: + return ForwardComboTextMessageToEditList(hWnd, message, wParam, lParam); + + case WW_CB_FINDSTRINGA: + return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, false, false, false); + + case WW_CB_FINDSTRINGEXACTA: + return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, false, true, false); + + case WW_CB_SELECTSTRINGA: + return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, false, false, true); + + case WW_CB_FINDSTRINGW: + return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, true, false, false); + + case WW_CB_FINDSTRINGEXACTW: + return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, true, true, false); + + case WW_CB_SELECTSTRINGW: + return FindComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, true, false, true); + + case WW_CB_INSERTSTRINGA: + case WW_CB_ADDSTRINGA: + return AddOrInsertComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, false); + + case WW_CB_INSERTSTRINGW: + case WW_CB_ADDSTRINGW: + return AddOrInsertComboString(data, pOriginalWndProc, hWnd, message, wParam, lParam, true); + + case WW_CB_GETLBTEXTA: + return GetComboText(pOriginalWndProc, hWnd, message, wParam, lParam, false); + + case WW_CB_GETLBTEXTW: + return GetComboText(pOriginalWndProc, hWnd, message, wParam, lParam, true); + + case WW_CB_GETITEMTEXTFORMAT: + return GetComboText(pOriginalWndProc, hWnd, message, wParam, lParam, true); + + case WW_EDIT_ENTERPRESSED: + case WW_EDIT_TABNAV: + if (const HWND parentHwnd = ::GetParent(hWnd)) + ::SendMessageA(parentHwnd, message, wParam, lParam); + return forwardOriginal(); + + case WW_CB_ENABLEITEMCOLORS: + data.AsComboBox().UseItemColorOverrides() = lParam == 1; + return forwardOriginal(); + + case WW_CB_SETMAXVISIBLEDROPITEMS: + data.AsComboBox().MaxVisibleDropItems() = static_cast(lParam); + return forwardOriginal(); + + case WW_QUERYTOOLTIPHIT: + if (const HWND dropHwnd = data.AsComboBox().DropDownHwnd()) + { + RECT comboWindowRect {}; + RECT dropWindowRect {}; + ::GetWindowRect(hWnd, &comboWindowRect); + ::GetWindowRect(dropHwnd, &dropWindowRect); + + const int x = LOWORD(lParam) + comboWindowRect.left - dropWindowRect.left; + const int y = HIWORD(lParam) + comboWindowRect.top - dropWindowRect.top; + return ::SendMessageA(dropHwnd, WW_QUERYTOOLTIPHIT, 0, MAKELPARAM(x, y)); + } + return -1; + + case WW_CB_SETALTERNATEPALETTE: + data.AsComboBox().UseAlternatePalette() = lParam == 1; + return forwardOriginal(); + + default: + break; + } + + return forwardOriginal(); +} diff --git a/src/OwnerDraw/Edit.cpp b/src/OwnerDraw/Edit.cpp new file mode 100644 index 0000000000..751f432763 --- /dev/null +++ b/src/OwnerDraw/Edit.cpp @@ -0,0 +1,1000 @@ +#include "OwnerDraw.Internal.h" + +void CharToWideString(wchar_t* pBuffer, int capacity, const char* pText) +{ + if (!pBuffer || capacity <= 0) + return; + + pBuffer[0] = L'\0'; + if (!pText) + return; + + ::MultiByteToWideChar(CP_ACP, 0, pText, -1, pBuffer, capacity); + pBuffer[capacity - 1] = L'\0'; +} + +void WideToCharString(char* pBuffer, int capacity, const wchar_t* pText) +{ + if (!pBuffer || capacity <= 0) + return; + + pBuffer[0] = '\0'; + if (!pText) + return; + + ::WideCharToMultiByte(CP_ACP, 0, pText, -1, pBuffer, capacity, nullptr, nullptr); + pBuffer[capacity - 1] = '\0'; +} + +static UINT GetCurrentKeyboardCodePage() +{ + char buffer[7] {}; + const WORD language = LOWORD(::GetKeyboardLayout(0)); + const LCID locale = MAKELCID(language, SORT_DEFAULT); + + if (!::GetLocaleInfoA(locale, LOCALE_IDEFAULTANSICODEPAGE, buffer, static_cast(std::size(buffer)))) + return CP_ACP; + + const int codePage = std::atoi(buffer); + return codePage > 0 ? static_cast(codePage) : CP_ACP; +} + +static wchar_t LocalizeCharacter(char character) +{ + wchar_t result {}; + ::MultiByteToWideChar(GetCurrentKeyboardCodePage(), MB_USEGLYPHCHARS, &character, 1, &result, 1); + return result; +} + +static WideWstring* EnsureNewEditText(OwnerDrawDialogElement& data) +{ + if (!data.AsNewEdit().Text()) + { + auto pMemory = YRMemory::Allocate(sizeof(WideWstring)); + if (!pMemory) + return nullptr; + + data.AsNewEdit().Text() = new (pMemory) WideWstring(); + } + + return data.AsNewEdit().Text(); +} + +static const wchar_t* NewEditTextBuffer(OwnerDrawDialogElement& data) +{ + const auto pText = EnsureNewEditText(data); + return pText && pText->Buffer ? pText->Buffer : L""; +} + +static int NewEditTextLength(OwnerDrawDialogElement& data) +{ + const auto pText = EnsureNewEditText(data); + return pText ? static_cast(pText->GetLength()) : 0; +} + +static void SetNewEditText(OwnerDrawDialogElement& data, const wchar_t* pText) +{ + if (auto pTarget = EnsureNewEditText(data)) + *pTarget = pText ? pText : L""; +} + +static void TrimNewEditTextToLimit(OwnerDrawDialogElement& data) +{ + const int limit = data.AsNewEdit().TextLimit(); + if (limit <= 0) + return; + + if (NewEditTextLength(data) <= limit) + return; + + std::wstring value(NewEditTextBuffer(data), limit); + SetNewEditText(data, value.c_str()); + + if (data.AsNewEdit().CaretIndex() > limit) + data.AsNewEdit().CaretIndex() = limit; +} + +static bool RemoveNewEditTextRange(OwnerDrawDialogElement& data, int index, int length) +{ + std::wstring value(NewEditTextBuffer(data)); + if (index < 0 || length <= 0 || index >= static_cast(value.size())) + return false; + + length = std::min(length, static_cast(value.size()) - index); + value.erase(static_cast(index), static_cast(length)); + SetNewEditText(data, value.c_str()); + data.AsNewEdit().CaretIndex() = std::clamp(data.AsNewEdit().CaretIndex(), 0, static_cast(value.size())); + return true; +} + +static bool InsertNewEditCharacter(OwnerDrawDialogElement& data, wchar_t character) +{ + if (!character || character <= 0x1F) + return false; + + if (data.AsNewEdit().AsciiOnly() && character >= 0x100) + return false; + + if (data.AsNewEdit().RejectChars() && std::wcschr(data.AsNewEdit().RejectChars(), character)) + return false; + + std::wstring value(NewEditTextBuffer(data)); + if (data.AsNewEdit().TextLimit() > 0 && static_cast(value.size()) >= data.AsNewEdit().TextLimit()) + return false; + + int caretIndex = std::clamp(data.AsNewEdit().CaretIndex(), 0, static_cast(value.size())); + value.insert(value.begin() + caretIndex, character); + SetNewEditText(data, value.c_str()); + data.AsNewEdit().CaretIndex() = caretIndex + 1; + return true; +} + +static void NotifyNewEditTextChanged(HWND hWnd, HWND parentHwnd) +{ + if (!parentHwnd) + return; + + const int controlId = ::GetWindowLongA(hWnd, GWL_ID) & 0xFFFF; + ::SendMessageA(parentHwnd, WM_COMMAND, controlId | 0x03000000, reinterpret_cast(hWnd)); + ::SendMessageA(parentHwnd, WM_COMMAND, controlId | 0x04000000, reinterpret_cast(hWnd)); +} + +static void NotifyNewEditEnterPressed(HWND hWnd, HWND parentHwnd) +{ + if (parentHwnd) + ::SendMessageA(parentHwnd, WW_EDIT_ENTERPRESSED, 0, reinterpret_cast(hWnd)); +} + +static void NotifyNewEditMultilineEnter(HWND hWnd, HWND parentHwnd) +{ + if (!parentHwnd) + return; + + const int controlId = ::GetWindowLongA(hWnd, GWL_ID) & 0xFFFF; + ::SendMessageA(parentHwnd, WM_COMMAND, controlId | 0x05010000, reinterpret_cast(hWnd)); +} + +static bool IsComboBoxParent(HWND parentHwnd) +{ + if (!parentHwnd) + return false; + + char className[32] {}; + ::GetClassNameA(parentHwnd, className, static_cast(std::size(className))); + return std::strcmp(className, "ComboBox") == 0; +} + +static void InvalidateNewEdit(HWND hWnd, HWND parentHwnd) +{ + if (IsComboBoxParent(parentHwnd)) + ::InvalidateRect(parentHwnd, nullptr, FALSE); + + ::InvalidateRect(hWnd, nullptr, FALSE); +} + +static int NewEditTextWidth(BitFont* pFont, const wchar_t* pText) +{ + if (!pText || !pText[0]) + return 0; + + if (!pFont) + pFont = BitFont::Instance; + + if (!pFont) + return static_cast(std::wcslen(pText)) * 8; + + int textWidth = 0; + int textHeight = 0; + pFont->GetTextDimension(pText, &textWidth, &textHeight, 0); + return textWidth; +} + +static int NewEditFitCharacterCount(BitFont* pFont, const wchar_t* pText, int maxWidth) +{ + if (!pText || maxWidth <= 0) + return 0; + + const int length = static_cast(std::wcslen(pText)); + int fitCount = 0; + + for (int count = 1; count <= length; ++count) + { + std::wstring candidate(pText, pText + count); + if (NewEditTextWidth(pFont, candidate.c_str()) > maxWidth) + break; + + fitCount = count; + } + + return fitCount; +} + +static int PrintNewEditTextSegment( + DSurface* pSurface, + RectangleStruct& rect, + BitFont* pFont, + const std::wstring& text, + int start, + int end, + COLORREF color, + int animationPos) +{ + if (end <= start || rect.Width <= 0) + return 0; + + const std::wstring segment(text.begin() + start, text.begin() + end); + const int width = NewEditTextWidth(pFont, segment.c_str()); + + OwnerDraw::PrintTextFixedLength( + color, + pFont, + &rect, + segment.c_str(), + static_cast(segment.size()), + 0, + 0, + pSurface, + animationPos); + + rect.X += width; + rect.Width = std::max(rect.Width - width, 0); + return width; +} + +static void AnimatedNewEditTextPrint( + DSurface* pSurface, + RectangleStruct textRect, + const wchar_t* pText, + int caretIndex, + BitFont* pFont, + COLORREF textColor, + int& scrollStart, + bool hasFocus, + bool maskText, + bool fillBackground, + int animationPos, + int caretBlinkState) +{ + if (!pSurface || textRect.Width <= 0 || textRect.Height <= 0) + return; + + if (!pFont) + pFont = BitFont::Instance; + + const wchar_t* pSource = pText ? pText : L""; + const int sourceLength = static_cast(std::wcslen(pSource)); + caretIndex = std::clamp(caretIndex, 0, sourceLength); + + int compositionLength = 0; + int compositionCursor = 0; + bool composing = false; + if (hasFocus) + { + OwnerDraw::UpdateIMECompositionString(); + compositionLength = std::clamp(OwnerDraw::IMECompositionStringLength, 0, 0x100); + compositionCursor = std::clamp(OwnerDraw::IMECompositionCursorPos, 0, compositionLength); + composing = OwnerDraw::IMEComposing != 0; + } + + constexpr size_t DisplayBufferCapacity = 0x800; + std::wstring displayText(pSource); + if (displayText.size() >= DisplayBufferCapacity) + displayText.resize(DisplayBufferCapacity - 1); + + const int compositionStart = std::min(caretIndex, static_cast(displayText.size())); + if (compositionLength > 0) + { + displayText.insert( + displayText.begin() + compositionStart, + OwnerDraw::IMECompositionString, + OwnerDraw::IMECompositionString + compositionLength); + + if (displayText.size() >= DisplayBufferCapacity) + displayText.resize(DisplayBufferCapacity - 1); + } + + if (maskText) + std::fill(displayText.begin(), displayText.end(), L'*'); + + const int displayLength = static_cast(displayText.size()); + const int compositionEnd = std::min(compositionStart + compositionLength, displayLength); + int displayCaret = composing || compositionLength + ? compositionStart + compositionCursor + : std::min(caretIndex, displayLength); + displayCaret = std::clamp(displayCaret, 0, displayLength); + + scrollStart = std::clamp(scrollStart, 0, displayLength); + if (displayCaret < scrollStart + 5) + scrollStart = std::max(displayCaret - 5, 0); + + while (scrollStart < displayLength) + { + const int visibleCount = NewEditFitCharacterCount(pFont, displayText.c_str() + scrollStart, textRect.Width - 5); + if (visibleCount >= displayCaret - scrollStart) + break; + + ++scrollStart; + } + + if (fillBackground) + { + RectangleStruct fillRect + { + textRect.X - 1, + textRect.Y - 1, + NewEditTextWidth(pFont, displayText.c_str() + scrollStart) + 5, + textRect.Height + 2 + }; + pSurface->FillRect(&fillRect, 0); + } + + RectangleStruct drawRect = textRect; + int caretX = -1; + + auto drawRange = [&](int rangeStart, int rangeEnd, COLORREF color) + { + int visibleStart = std::max(rangeStart, scrollStart); + int visibleEnd = std::min(rangeEnd, displayLength); + if (visibleEnd <= visibleStart) + return; + + if (caretX < 0 && displayCaret >= visibleStart && displayCaret <= visibleEnd) + { + PrintNewEditTextSegment(pSurface, drawRect, pFont, displayText, visibleStart, displayCaret, color, animationPos); + caretX = drawRect.X; + PrintNewEditTextSegment(pSurface, drawRect, pFont, displayText, displayCaret, visibleEnd, color, animationPos); + } + else + { + PrintNewEditTextSegment(pSurface, drawRect, pFont, displayText, visibleStart, visibleEnd, color, animationPos); + } + }; + + drawRange(0, compositionStart, textColor); + drawRange(compositionStart, compositionEnd, OwnerDraw::ImeCompositionTextColor); + drawRange(compositionEnd, displayLength, textColor); + + if (caretX < 0) + { + if (displayCaret <= scrollStart) + caretX = textRect.X; + else if (displayCaret >= displayLength) + caretX = drawRect.X; + } + + if (hasFocus && caretX >= 0 && !caretBlinkState) + { + const WORD caretColor = static_cast(ConvertRGBToSurfaceColor(Phobos::UI::ColorCaret)); + Point2D start { caretX, textRect.Y }; + Point2D end { caretX, textRect.Y + textRect.Height - 2 }; + DrawAlphaLine(pSurface, start, end, caretColor, 0xFF); + + ++start.X; + ++end.X; + DrawAlphaLine(pSurface, start, end, caretColor, 0xFF); + } +} + +static void PaintNewEdit(HWND hWnd, OwnerDrawDialogElement& data, HWND parentHwnd) +{ + if (!DSurface::Alternate) + return; + + RECT ownerRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + const int width = ownerRect.right - ownerRect.left + 1; + const int height = ownerRect.bottom - ownerRect.top + 1; + if (width <= 0 || height <= 0) + return; + + RectangleStruct drawRect { ownerRect.left, ownerRect.top, width, height }; + OwnerDraw::CopyDimmedBackground(&drawRect, hWnd, static_cast(data.Alpha)); + + if (!IsComboBoxParent(parentHwnd)) + DrawBeveledBorder(DSurface::Alternate, drawRect, 2, -1); + + RectangleStruct textRect + { + ownerRect.left + 2, + ownerRect.top, + ownerRect.right - ownerRect.left + 1, + ownerRect.bottom - ownerRect.top + 1 + }; + + AnimatedNewEditTextPrint( + DSurface::Alternate, + textRect, + NewEditTextBuffer(data), + data.AsNewEdit().CaretIndex(), + data.AsNewEdit().Font(), + Phobos::UI::ColorTextEdit, + data.AsNewEdit().ScrollStart(), + data.HasFocus != 0, + ((data.AsNewEdit().StyleFlags() >> 5) & 1) != 0, + false, + 0, + data.AsNewEdit().CaretBlinkState()); + + ::ValidateRect(hWnd, nullptr); +} + +static size_t GetEditWideText(HWND hWnd, wchar_t* pWideText, int capacity, int* pSelectionEndChars) +{ + if (pSelectionEndChars) + *pSelectionEndChars = 0; + + if (!pWideText || capacity <= 0) + return 0; + + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + + char ansiText[0x400] {}; + CallSelectedHandler( + pOriginalWndProc, + hWnd, + WM_GETTEXT, + static_cast(std::size(ansiText)), + reinterpret_cast(ansiText)); + + if (pSelectionEndChars) + { + DWORD selectionStart = 0; + DWORD selectionEnd = 0; + const auto selection = static_cast(CallSelectedHandler( + pOriginalWndProc, + hWnd, + EM_GETSEL, + reinterpret_cast(&selectionStart), + reinterpret_cast(&selectionEnd))); + const auto selectionEndBytes = static_cast(HIWORD(selection)); + *pSelectionEndChars = static_cast(_mbsnccnt( + reinterpret_cast(ansiText), + selectionEndBytes)); + } + + pWideText[0] = L'\0'; + ::MultiByteToWideChar(CP_ACP, 0, ansiText, -1, pWideText, capacity); + pWideText[capacity - 1] = L'\0'; + + const size_t wideLength = std::wcslen(pWideText); + std::wstring normalized; + normalized.reserve(wideLength + 2); + + bool removedNewLine = false; + for (size_t i = 0; i < wideLength; ++i) + { + if (pWideText[i] == L'\r' || pWideText[i] == L'\n') + { + removedNewLine = true; + continue; + } + + normalized.push_back(pWideText[i]); + } + + if (removedNewLine) + normalized += L"\r\n"; + + std::wcsncpy(pWideText, normalized.c_str(), static_cast(capacity - 1)); + pWideText[capacity - 1] = L'\0'; + return std::wcslen(pWideText); +} + +static LRESULT ForwardEditSetText(OwnerDrawDialogElement& data, HWND hWnd, WNDPROC pOriginalWndProc) +{ + char ansiText[0x800] {}; + if (data.TextBuffer) + WideToCharString(ansiText, static_cast(std::size(ansiText)), data.TextBuffer); + + return CallSelectedHandler( + pOriginalWndProc, + hWnd, + WM_SETTEXT, + 0, + reinterpret_cast(ansiText)); +} + +static LRESULT CopyEditTextW(HWND hWnd, WPARAM capacityParam, LPARAM lParam) +{ + auto pBuffer = reinterpret_cast(lParam); + const int capacity = static_cast(capacityParam); + if (!pBuffer || capacity <= 0) + return 0; + + std::vector text(0x800); + GetEditWideText(hWnd, text.data(), static_cast(text.size()), nullptr); + + std::wcsncpy(pBuffer, text.data(), static_cast(capacity - 1)); + pBuffer[capacity - 1] = L'\0'; + return static_cast(std::wcslen(pBuffer)); +} + +static LRESULT CopyEditTextA(HWND hWnd, WPARAM capacityParam, LPARAM lParam) +{ + auto pBuffer = reinterpret_cast(lParam); + const int capacity = static_cast(capacityParam); + if (!pBuffer || capacity <= 0) + return 0; + + std::vector text(0x800); + GetEditWideText(hWnd, text.data(), static_cast(text.size()), nullptr); + WideToCharString(pBuffer, capacity, text.data()); + return static_cast(std::strlen(pBuffer)); +} + +static LRESULT AppendEditNewLine(HWND hWnd, WNDPROC pOriginalWndProc) +{ + const int textLength = static_cast(CallSelectedHandler(pOriginalWndProc, hWnd, WM_GETTEXTLENGTH, 0, 0)); + std::vector text(static_cast(std::max(textLength + 3, 3)), '\0'); + + CallSelectedHandler( + pOriginalWndProc, + hWnd, + WM_GETTEXT, + static_cast(text.size()), + reinterpret_cast(text.data())); + + const size_t copiedLength = std::strlen(text.data()); + if (copiedLength + 2 < text.size()) + std::memcpy(text.data() + copiedLength, "\r\n", 3); + + CallSelectedHandler(pOriginalWndProc, hWnd, WM_SETTEXT, 0, reinterpret_cast(text.data())); + + if (const HWND parentHwnd = ::GetParent(hWnd)) + { + const int controlId = ::GetWindowLongA(hWnd, GWL_ID) & 0xFFFF; + ::SendMessageA(parentHwnd, WM_COMMAND, controlId | 0x05010000, reinterpret_cast(hWnd)); + } + + return 0; +} + +static void PaintEdit(HWND hWnd, OwnerDrawDialogElement& data, HWND parentHwnd, UINT message) +{ + if (!DSurface::Alternate) + return; + + RECT ownerRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + if (message == WM_PAINT) + { + RECT updateRect {}; + if (::GetUpdateRect(hWnd, &updateRect, FALSE)) + { + updateRect.left += ownerRect.left; + updateRect.top += ownerRect.top; + updateRect.right += ownerRect.left; + updateRect.bottom += ownerRect.top; + } + } + + const int width = ownerRect.right - ownerRect.left + 1; + const int height = ownerRect.bottom - ownerRect.top + 1; + if (width <= 0 || height <= 0) + return; + + RectangleStruct drawRect { ownerRect.left, ownerRect.top, width, height }; + OwnerDraw::CopyDimmedBackground(&drawRect, hWnd, static_cast(data.Alpha)); + + if (!IsComboBoxParent(parentHwnd)) + DrawBeveledBorder(DSurface::Alternate, drawRect, 2, -1); + + std::vector text(0x1400); + int caretIndex = 0; + GetEditWideText(hWnd, text.data(), static_cast(text.size()), &caretIndex); + + RectangleStruct textRect { ownerRect.left, ownerRect.top, width, height }; + const bool maskText = ((::GetWindowLongA(hWnd, GWL_STYLE) >> 5) & 1) != 0; + + AnimatedNewEditTextPrint( + DSurface::Alternate, + textRect, + text.data(), + caretIndex, + data.AsEdit().TextFont(), + Phobos::UI::ColorText, + data.AsEdit().TextScrollStart(), + data.HasFocus != 0, + maskText, + false, + 0, + 0); + + ::ValidateRect(hWnd, nullptr); +} + +LRESULT CALLBACK WWUI::EditCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto pData = FindOwnerDrawData(hWnd); + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + auto forwardOriginal = [&]() -> LRESULT + { + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + }; + + if (!pData) + return forwardOriginal(); + + auto& data = *pData; + if (::GetFocus() == hWnd && !data.AsEdit().FocusRestoreReadyFlag()) + { + data.AsEdit().FocusRestorePendingFlag() = 1; + ::SetFocus(Game::hWnd); + } + + const LONG windowStyle = ::GetWindowLongA(hWnd, GWL_STYLE); + const HWND parentHwnd = ::GetParent(hWnd); + + if ((message == WM_KEYDOWN || message == WM_KEYUP) && wParam == VK_TAB) + return 0; + + switch (message) + { + case WW_INITDIALOG: + { + if (!parentHwnd) + return 0; + + RECT windowRect {}; + RECT clientRect {}; + RECT parentRect {}; + ::GetWindowRect(hWnd, &windowRect); + ::GetClientRect(hWnd, &clientRect); + ::GetWindowRect(parentHwnd, &parentRect); + + ::MoveWindow( + hWnd, + windowRect.left - parentRect.left + 1, + windowRect.top - parentRect.top + 1, + clientRect.right - 2, + clientRect.bottom - 2, + FALSE); + + if (::GetFocus() == hWnd) + { + data.AsEdit().FocusRestorePendingFlag() = 1; + ::SetFocus(Game::hWnd); + } + + if (windowStyle & WS_TABSTOP) + { + data.AsEdit().RestoreTabStopFlag() = 1; + ::SetWindowLongA(hWnd, GWL_STYLE, windowStyle & ~static_cast(WS_TABSTOP)); + } + + return 0; + } + + case WM_SETFOCUS: + ::SendMessageA(hWnd, EM_SETSEL, static_cast(-1), static_cast(-1)); + if (!data.AsEdit().FocusRestoreReadyFlag()) + ::PostMessageA(hWnd, WW_EDIT_DEFERFOCUSRESTORE, 0, 0); + + InvalidateNewEdit(hWnd, parentHwnd); + return forwardOriginal(); + + case WW_GETTEXTW: + return CopyEditTextW(hWnd, wParam, lParam); + + case WW_GETTEXTA: + return CopyEditTextA(hWnd, wParam, lParam); + + case WM_GETTEXTLENGTH: + { + std::vector text(0x800); + return static_cast(GetEditWideText(hWnd, text.data(), static_cast(text.size()), nullptr)); + } + + case WW_SETTEXTW: + case WW_SETTEXTA: + return ForwardEditSetText(data, hWnd, pOriginalWndProc); + + case WM_CHAR: + if (wParam == VK_RETURN) + { + if (windowStyle & ES_MULTILINE) + return AppendEditNewLine(hWnd, pOriginalWndProc); + + return forwardOriginal(); + } + + if (wParam == VK_TAB) + { + if (const HWND nextHwnd = ::GetNextDlgTabItem(parentHwnd, hWnd, FALSE)) + ::SetFocus(nextHwnd); + else + ::SetFocus(hWnd); + + return 0; + } + + return forwardOriginal(); + + case WW_EDIT_RESTOREFOCUS: + data.AsEdit().FocusRestoreReadyFlag() = 1; + if (data.AsEdit().FocusRestorePendingFlag()) + { + ::SetFocus(hWnd); + data.AsEdit().FocusRestorePendingFlag() = 0; + } + + if (data.AsEdit().RestoreTabStopFlag()) + ::SetWindowLongA(hWnd, GWL_STYLE, windowStyle | WS_TABSTOP); + + return 0; + + case WM_PAINT: + case WM_ERASEBKGND: + PaintEdit(hWnd, data, parentHwnd, message); + break; + + case WM_CONTEXTMENU: + return 1; + + case WM_MOUSEMOVE: + return 1; + + default: + break; + } + + switch (message) + { + case WM_KEYDOWN: + case WM_KEYUP: + case WM_SYSKEYDOWN: + case WM_SYSKEYUP: + case WM_SYSCHAR: + case WM_SYSDEADCHAR: + case WM_KILLFOCUS: + case WM_LBUTTONDOWN: + InvalidateNewEdit(hWnd, parentHwnd); + break; + + default: + break; + } + + return forwardOriginal(); +} + +LRESULT CALLBACK WWUI::NewEditCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_GETDLGCODE) + return DLGC_WANTALLKEYS; + + auto pData = FindOwnerDrawData(hWnd); + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + auto forwardOriginal = [&]() -> LRESULT + { + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + }; + + if (!pData) + return forwardOriginal(); + + auto& data = *pData; + EnsureNewEditText(data); + + const HWND parentHwnd = ::GetParent(hWnd); + + if ((message == WM_KEYDOWN || message == WM_KEYUP) && wParam == VK_TAB) + { + if (message == WM_KEYDOWN && parentHwnd) + { + const WPARAM shiftPressed = (::GetAsyncKeyState(VK_SHIFT) & 0x8000) ? 1 : 0; + ::SendMessageA(parentHwnd, WW_EDIT_TABNAV, shiftPressed, reinterpret_cast(hWnd)); + } + + return 0; + } + + auto copyWideText = [&]() -> LRESULT + { + const int capacity = static_cast(wParam); + auto pBuffer = reinterpret_cast(lParam); + if (!pBuffer || capacity <= 0) + return 0; + + const auto pText = NewEditTextBuffer(data); + std::wcsncpy(pBuffer, pText, capacity - 1); + pBuffer[capacity - 1] = L'\0'; + return static_cast(std::wcslen(pBuffer)); + }; + + auto copyAnsiText = [&]() -> LRESULT + { + const int capacity = static_cast(wParam); + auto pBuffer = reinterpret_cast(lParam); + if (!pBuffer || capacity <= 0) + return 0; + + WideToCharString(pBuffer, capacity, NewEditTextBuffer(data)); + return static_cast(std::strlen(pBuffer)); + }; + + auto handleInputCharacter = [&](wchar_t character, bool consumedInput) -> LRESULT + { + if (!character) + return consumedInput ? 0 : forwardOriginal(); + + if (InsertNewEditCharacter(data, character)) + NotifyNewEditTextChanged(hWnd, parentHwnd); + + return 0; + }; + + switch (message) + { + case WW_INITDIALOG: + { + if (!parentHwnd) + return 0; + + RECT windowRect {}; + RECT clientRect {}; + RECT parentRect {}; + ::GetWindowRect(hWnd, &windowRect); + ::GetClientRect(hWnd, &clientRect); + ::GetWindowRect(parentHwnd, &parentRect); + + ::SetWindowPos( + hWnd, + nullptr, + windowRect.left - parentRect.left + 1, + windowRect.top - parentRect.top + 1, + clientRect.right - 2, + clientRect.bottom - 2, + SWP_SHOWWINDOW); + return 0; + } + + case EM_LIMITTEXT: + data.AsNewEdit().TextLimit() = static_cast(wParam); + TrimNewEditTextToLimit(data); + return forwardOriginal(); + + case WW_GETTEXTW: + return copyWideText(); + + case WW_GETTEXTA: + return copyAnsiText(); + + case WM_GETTEXTLENGTH: + return NewEditTextLength(data); + + case WW_SETTEXTW: + case WW_SETTEXTA: + SetNewEditText(data, data.TextBuffer ? data.TextBuffer : L""); + data.AsNewEdit().CaretIndex() = 0; + data.AsNewEdit().ScrollStart() = 0; + TrimNewEditTextToLimit(data); + data.AsNewEdit().CaretIndex() = NewEditTextLength(data); + break; + + case WM_KEYDOWN: + if (wParam == VK_RETURN) + { + NotifyNewEditEnterPressed(hWnd, parentHwnd); + if (data.AsNewEdit().StyleFlags() & 4) + { + if (auto pText = EnsureNewEditText(data)) + *pText += L"\r\n"; + + NotifyNewEditMultilineEnter(hWnd, parentHwnd); + } + return 0; + } + break; + + case WM_SETFOCUS: + data.AsNewEdit().CaretBlinkState() = 0; + ::SetTimer(hWnd, 0, 1000, nullptr); + InvalidateNewEdit(hWnd, parentHwnd); + return forwardOriginal(); + + case WM_KILLFOCUS: + ::KillTimer(hWnd, 0); + InvalidateNewEdit(hWnd, parentHwnd); + return forwardOriginal(); + + case WM_TIMER: + data.AsNewEdit().CaretBlinkState() ^= 1; + ::InvalidateRect(hWnd, nullptr, FALSE); + return forwardOriginal(); + + case WM_PAINT: + case WM_ERASEBKGND: + PaintNewEdit(hWnd, data, parentHwnd); + break; + + case WM_CONTEXTMENU: + return 1; + + case WM_MOUSEMOVE: + return 1; + + default: + break; + } + + switch (message) + { + case WM_KEYDOWN: + case WM_KEYUP: + case WM_SYSKEYDOWN: + case WM_SYSKEYUP: + case WM_SYSCHAR: + case WM_SYSDEADCHAR: + case WM_LBUTTONDOWN: + InvalidateNewEdit(hWnd, parentHwnd); + break; + + default: + break; + } + + if (message == WM_CHAR) + { + if (wParam <= 0x1F) + return forwardOriginal(); + + return handleInputCharacter(LocalizeCharacter(static_cast(wParam)), true); + } + + if (message == WM_KEYDOWN) + { + bool textChanged = false; + switch (wParam) + { + case VK_BACK: + if (data.AsNewEdit().CaretIndex() > 0) + { + --data.AsNewEdit().CaretIndex(); + textChanged = RemoveNewEditTextRange(data, data.AsNewEdit().CaretIndex(), 1); + } + break; + + case VK_DELETE: + if (data.AsNewEdit().CaretIndex() < NewEditTextLength(data)) + textChanged = RemoveNewEditTextRange(data, data.AsNewEdit().CaretIndex(), 1); + break; + + case VK_END: + data.AsNewEdit().CaretIndex() = NewEditTextLength(data); + return 0; + + case VK_HOME: + data.AsNewEdit().CaretIndex() = 0; + return 0; + + case VK_LEFT: + if (data.AsNewEdit().CaretIndex() > 0) + --data.AsNewEdit().CaretIndex(); + return 0; + + case VK_RIGHT: + if (data.AsNewEdit().CaretIndex() < NewEditTextLength(data)) + ++data.AsNewEdit().CaretIndex(); + return 0; + + default: + return forwardOriginal(); + } + + if (textChanged) + NotifyNewEditTextChanged(hWnd, parentHwnd); + + return 0; + } + + if (message == WM_IME_CHAR) + return handleInputCharacter(OwnerDraw::ConvertIMECharToWide(static_cast(wParam), lParam), true); + + if (message == WW_EDIT_INPUTCHARW) + return handleInputCharacter(static_cast(wParam), true); + + return forwardOriginal(); +} diff --git a/src/OwnerDraw/GroupBox.cpp b/src/OwnerDraw/GroupBox.cpp new file mode 100644 index 0000000000..c2b9d45ae4 --- /dev/null +++ b/src/OwnerDraw/GroupBox.cpp @@ -0,0 +1,128 @@ +#include "OwnerDraw.Internal.h" + +static void DrawGroupBoxLine(int startX, int startY, int endX, int endY, int color) +{ + Point2D start { startX, startY }; + Point2D end { endX, endY }; + DSurface::Alternate->DrawLine(&start, &end, color); +} + +static void DrawGroupBoxPixel(int x, int y, int color) +{ + Point2D point { x, y }; + DSurface::Alternate->SetPixel(&point, color); +} + +static int MeasureGroupBoxCaption(BitFont* pFont, const wchar_t* pCaption, int maxWidth) +{ + if (!pFont) + pFont = BitFont::Instance; + + int width = 0; + int height = 0; + if (pFont) + pFont->GetTextDimension(pCaption, &width, &height, maxWidth); + + return width; +} + +static void DrawGroupBoxCaption(const RECT& groupRect, const wchar_t* pCaption) +{ + RectangleStruct captionRect + { + groupRect.left + 10, + groupRect.top, + groupRect.right - groupRect.left, + groupRect.bottom - groupRect.top + }; + + OwnerDraw::PrintTextFixedLength( + Phobos::UI::ColorTextGroupbox, + nullptr, + &captionRect, + pCaption, + static_cast(std::wcslen(pCaption)), + 0, + 0, + DSurface::Alternate, + 0); +} + +static void DrawGroupBoxFramePass( + const RECT& groupRect, + int topY, + int inset, + bool hasCaption, + int captionWidth, + int nearColor, + int farColor, + int cornerColor) +{ + const int left = groupRect.left + inset; + const int right = groupRect.right - inset - 1; + const int y = topY + inset; + const int bottom = groupRect.bottom - inset - 1; + + if (hasCaption) + { + DrawGroupBoxLine(left, y, groupRect.left + 8, y, nearColor); + DrawGroupBoxLine(groupRect.left + captionWidth + 12 + inset, y, right, y, nearColor); + } + else + { + DrawGroupBoxLine(left, y, right, y, nearColor); + } + + DrawGroupBoxLine(left, y + 1, left, bottom, nearColor); + DrawGroupBoxLine(left, bottom, right, bottom, farColor); + DrawGroupBoxLine(right, y, right, bottom - 1, farColor); + DrawGroupBoxPixel(right, y, cornerColor); + DrawGroupBoxPixel(left, bottom, cornerColor); +} + +static LRESULT PaintGroupBoxCtrl(HWND hWnd, OwnerDrawDialogElement& data) +{ + const wchar_t* const pCaption = data.TextBuffer ? data.TextBuffer : L""; + + RECT groupRect {}; + OwnerDraw::GetRectangle(hWnd, &groupRect); + + const int groupWidth = groupRect.right - groupRect.left; + const auto pFont = data.AsGroupBox().Font() ? data.AsGroupBox().Font() : BitFont::Instance; + const int topY = groupRect.top + BitFontHeight(pFont) / 2; + const int captionWidth = MeasureGroupBoxCaption(pFont, pCaption, groupWidth); + + DrawGroupBoxCaption(groupRect, pCaption); + + const int lightColor = ConvertRGBToSurfaceColor(Phobos::UI::ColorBorder1); + const int shadowColor = ConvertRGBToSurfaceColor(Phobos::UI::ColorBorder2); + const int averageColor = ConvertRGBToSurfaceColor(AverageColor(Phobos::UI::ColorBorder1, Phobos::UI::ColorBorder2)); + const bool hasCaption = std::wcslen(pCaption) != 0; + + DrawGroupBoxFramePass(groupRect, topY, 0, hasCaption, captionWidth, lightColor, shadowColor, averageColor); + DrawGroupBoxFramePass(groupRect, topY, 1, hasCaption, captionWidth, shadowColor, lightColor, averageColor); + + ::ValidateRect(hWnd, nullptr); + return 0; +} + +LRESULT CALLBACK WWUI::GroupBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_PAINT: + if (auto pData = FindOwnerDrawData(hWnd)) + return PaintGroupBoxCtrl(hWnd, *pData); + + return 0; + + case WM_ERASEBKGND: + return 1; + + case WM_NCPAINT: + return 0; + + default: + return CallSelectedHandler(FindWindowProc(OwnerDraw::DialogProcs, hWnd), hWnd, message, wParam, lParam); + } +} diff --git a/src/OwnerDraw/Input.cpp b/src/OwnerDraw/Input.cpp new file mode 100644 index 0000000000..338188e096 --- /dev/null +++ b/src/OwnerDraw/Input.cpp @@ -0,0 +1,194 @@ +#include "OwnerDraw.Internal.h" + +constexpr WORD InputHotkeyShift = HOTKEYF_SHIFT << 8; +constexpr WORD InputHotkeyControl = HOTKEYF_CONTROL << 8; +constexpr WORD InputHotkeyAlt = HOTKEYF_ALT << 8; +constexpr WORD InputHotkeyNoText = HOTKEYF_EXT << 8; + +static void AppendCString(char* pBuffer, size_t bufferSize, const char* pText) +{ + if (!pBuffer || !bufferSize || !pText) + return; + + const size_t used = std::strlen(pBuffer); + if (used >= bufferSize - 1) + return; + + std::strncat(pBuffer, pText, bufferSize - used - 1); +} + +static void AppendKeyName(char* pBuffer, size_t bufferSize, UINT virtualKey, bool appendSeparator) +{ + char keyName[32] {}; + const UINT scanCode = ::MapVirtualKeyA(virtualKey, MAPVK_VK_TO_VSC); + ::GetKeyNameTextA(static_cast((scanCode << 16) | 0x02000001), keyName, static_cast(std::size(keyName))); + + AppendCString(pBuffer, bufferSize, keyName); + if (appendSeparator) + AppendCString(pBuffer, bufferSize, "+"); +} + +static void BuildKeyboardKeyString(WORD keyCode, wchar_t* pOutText, size_t outTextSize) +{ + if (!pOutText || !outTextSize) + return; + + pOutText[0] = L'\0'; + if (keyCode & InputHotkeyNoText) + return; + + char keyText[500] {}; + if (keyCode & InputHotkeyAlt) + AppendKeyName(keyText, std::size(keyText), VK_MENU, true); + + if (keyCode & InputHotkeyControl) + AppendKeyName(keyText, std::size(keyText), VK_CONTROL, true); + + if (keyCode & InputHotkeyShift) + AppendKeyName(keyText, std::size(keyText), VK_SHIFT, true); + + AppendKeyName(keyText, std::size(keyText), LOBYTE(keyCode), false); + std::swprintf(pOutText, outTextSize, L"%hs", keyText); + pOutText[outTextSize - 1] = L'\0'; +} + +static int DrawWideTextBasic(Surface* pSurface, const wchar_t* pText, const RECT& textRect, BitFont* pFont, COLORREF color) +{ + if (!pSurface || !pText) + return 0; + + auto pDrawFont = pFont ? pFont : BitFont::Instance; + if (!pDrawFont || !BitText::Instance) + return 0; + + LTRBStruct bounds { textRect.left, textRect.top, textRect.right, textRect.bottom }; + pDrawFont->SetClipMode(true); + pDrawFont->SetRectangle(&bounds); + pDrawFont->SetColor(static_cast(ConvertRGBToSurfaceColor(color))); + + BitText::Instance->DrawText( + pDrawFont, + pSurface, + pText, + textRect.left, + textRect.top, + textRect.right - textRect.left, + textRect.bottom - textRect.top, + 0, + 0, + 0); + + return 0; +} + +static void EnsureInputCache(OwnerDrawDialogElement& data, const RectangleStruct& screenRect) +{ + if (data.CacheSurface || !DSurface::Alternate || screenRect.Width <= 0 || screenRect.Height <= 0) + return; + + data.CacheSurface = GameCreate(screenRect.Width, screenRect.Height); + if (!data.CacheSurface) + return; + + ++OwnerDraw::CachedSurfaceCount; + + RectangleStruct cacheRect { 0, 0, screenRect.Width, screenRect.Height }; + CopySurfacePart(data.CacheSurface, cacheRect, DSurface::Alternate, screenRect); +} + +static LRESULT PaintInputCtrl(HWND hWnd, OwnerDrawDialogElement& data) +{ + if (!DSurface::Alternate) + { + ::ValidateRect(hWnd, nullptr); + return 0; + } + + RECT ownerRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + RectangleStruct screenRect + { + ownerRect.left, + ownerRect.top, + ownerRect.right - ownerRect.left + 1, + ownerRect.bottom - ownerRect.top + 1 + }; + + EnsureInputCache(data, screenRect); + + wchar_t keyText[256] {}; + BuildKeyboardKeyString(static_cast(::SendMessageA(hWnd, WW_INPUT_GETKEY, 0, 0)), keyText, std::size(keyText)); + + if (data.CacheSurface) + { + RectangleStruct cacheRect { 0, 0, screenRect.Width, screenRect.Height }; + CopySurfacePart(DSurface::Alternate, screenRect, data.CacheSurface, cacheRect); + } + + DrawBeveledBorder(DSurface::Alternate, screenRect, 2, -1); + + if (std::wcslen(keyText)) + { + RECT textRect + { + ownerRect.left + 4, + ownerRect.top + 4, + ownerRect.right - 4, + ownerRect.bottom - 4 + }; + + DrawWideTextBasic(DSurface::Alternate, keyText, textRect, data.AsInput().Font(), OwnerDraw::PrimaryTextColor); + } + + ::ValidateRect(hWnd, nullptr); + return 0; +} + +LRESULT CALLBACK WWUI::InputCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + auto forwardOriginal = [&]() -> LRESULT + { + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + }; + + switch (message) + { + case WM_PAINT: + if (auto pData = FindOwnerDrawData(hWnd)) + return PaintInputCtrl(hWnd, *pData); + + return 0; + + case WM_ERASEBKGND: + return 1; + + case WM_NCPAINT: + return 0; + + default: + return forwardOriginal(); + } +} + +LRESULT CALLBACK WWUI::SysListViewCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + + RECT ownerRect {}; + (void)OwnerDraw::GetRectangle(hWnd, &ownerRect); + + RECT clientRect {}; + (void)::GetClientRect(hWnd, &clientRect); + + if (message == WM_CTLCOLOREDIT) + { + const auto hdc = reinterpret_cast(wParam); + ::SetTextColor(hdc, OwnerDraw::PrimaryTextColor); + ::SetBkMode(hdc, TRANSPARENT); + return reinterpret_cast(::GetStockObject(NULL_BRUSH)); + } + + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); +} diff --git a/src/OwnerDraw/ListBox.cpp b/src/OwnerDraw/ListBox.cpp new file mode 100644 index 0000000000..e4a37287a9 --- /dev/null +++ b/src/OwnerDraw/ListBox.cpp @@ -0,0 +1,1489 @@ +#include "OwnerDraw.Internal.h" + +static COLORREF ListBoxTextColor() +{ + return Phobos::UI::ColorTextList; +} + +static COLORREF ListBoxSelectionFillColor() +{ + return Phobos::UI::ColorSelectionList; +} + +static COLORREF ListBoxDisabledTextColor() +{ + return Phobos::UI::ColorDisabledList; +} + +constexpr int ListBoxScrollBarExtraWidth = 18; +constexpr int ListBoxTextEntryInlineBytes = 2; + +static int SignedLowWord(LPARAM value) +{ + return static_cast(LOWORD(value)); +} + +static int SignedHighWord(LPARAM value) +{ + return static_cast(HIWORD(value)); +} + +static WPARAM ListBoxCommand(HWND hWnd, int notificationCode) +{ + return static_cast( + (::GetWindowLongA(hWnd, GWL_ID) & 0xFFFF) + | ((notificationCode & 0xFFFF) << 16)); +} + +static void NotifyListBoxSelectionChanged(HWND hWnd) +{ + if (const HWND parentHwnd = ::GetParent(hWnd)) + ::SendMessageA(parentHwnd, WM_COMMAND, ListBoxCommand(hWnd, LBN_SELCHANGE), reinterpret_cast(hWnd)); +} + +static void PostListBoxDoubleClick(HWND hWnd) +{ + if (const HWND parentHwnd = ::GetParent(hWnd)) + ::PostMessageA(parentHwnd, WM_COMMAND, ListBoxCommand(hWnd, LBN_DBLCLK), reinterpret_cast(hWnd)); +} + +static WWUIIntArray* CreateIntArray() +{ + auto pArray = static_cast(YRMemory::Allocate(sizeof(WWUIIntArray))); + if (pArray) + *pArray = {}; + + return pArray; +} + +static void DeleteIntArray(WWUIIntArray*& pArray) +{ + if (!pArray) + return; + + YRMemory::Deallocate(pArray->Items); + YRMemory::Deallocate(pArray); + pArray = nullptr; +} + +static void ResizeIntArrayStorage(WWUIIntArray& array, int capacity) +{ + if (capacity < 10) + capacity = 10; + + auto pItems = static_cast(YRMemory::Allocate(sizeof(int) * capacity)); + std::memset(pItems, 0, sizeof(int) * capacity); + + if (array.Items && array.Count > 0) + std::memcpy(pItems, array.Items, sizeof(int) * array.Count); + + YRMemory::Deallocate(array.Items); + array.Items = pItems; + array.Capacity = capacity; +} + +static void EnsureIntArraySize(WWUIIntArray& array, int count, int fillValue) +{ + if (count <= array.Count) + return; + + if (count > array.Capacity) + { + int capacity = array.Capacity; + do + { + capacity = std::max(capacity * 2, 10); + } + while (capacity < count); + + ResizeIntArrayStorage(array, capacity); + } + + for (int i = array.Count; i < count; ++i) + array.Items[i] = fillValue; + + array.Count = count; +} + +static void MaybeShrinkIntArray(WWUIIntArray& array) +{ + if (array.Capacity <= 10 || array.Count * 3 > array.Capacity) + return; + + ResizeIntArrayStorage(array, std::max(array.Capacity / 2, 10)); +} + +static void RemoveIntArrayItem(WWUIIntArray* pArray, int index) +{ + if (!pArray || index < 0 || index >= pArray->Count) + return; + + if (index < pArray->Count - 1) + { + std::memmove( + &pArray->Items[index], + &pArray->Items[index + 1], + sizeof(int) * (pArray->Count - index - 1)); + } + + --pArray->Count; + MaybeShrinkIntArray(*pArray); +} + +static void InsertIntArrayItem(WWUIIntArray* pArray, int index, int value) +{ + if (!pArray) + return; + + index = std::clamp(index, 0, pArray->Count); + EnsureIntArraySize(*pArray, pArray->Count + 1, value); + + if (index < pArray->Count - 1) + { + std::memmove( + &pArray->Items[index + 1], + &pArray->Items[index], + sizeof(int) * (pArray->Count - index - 1)); + } + + pArray->Items[index] = value; +} + +static void SetIntArrayValue(WWUIIntArray*& pArray, int index, int value, int fillValue) +{ + if (index < 0) + return; + + if (!pArray) + pArray = CreateIntArray(); + + if (!pArray) + return; + + EnsureIntArraySize(*pArray, index + 1, fillValue); + pArray->Items[index] = value; +} + +static int GetIntArrayValue(const WWUIIntArray* pArray, int index, int defaultValue) +{ + if (!pArray || index < 0 || index >= pArray->Count) + return defaultValue; + + return pArray->Items[index]; +} + +static void ConstructListBoxCell(WWUIListBoxCell& cell) +{ + new (&cell) WWUIListBoxCell(); + cell.Format = WWUIListBoxCellFormat::Empty; + cell.TextColor = static_cast(-1); + cell.Image = nullptr; + cell.Value = -1; +} + +static void ResetListBoxCell(WWUIListBoxCell& cell) +{ + cell.~WWUIListBoxCell(); + ConstructListBoxCell(cell); +} + +static WWUIListBoxCell* AllocateListBoxCells(int capacity) +{ + auto pItems = static_cast(YRMemory::Allocate(sizeof(WWUIListBoxCell) * capacity)); + for (int i = 0; i < capacity; ++i) + ConstructListBoxCell(pItems[i]); + + return pItems; +} + +static void DeleteListBoxCells(WWUIListBoxCell*& pItems, int capacity) +{ + if (!pItems) + return; + + for (int i = 0; i < capacity; ++i) + pItems[i].~WWUIListBoxCell(); + + YRMemory::Deallocate(pItems); + pItems = nullptr; +} + +static void ResizeListBoxCellStorage(WWUIListBoxColumn& column, int capacity) +{ + if (capacity < 10) + capacity = 10; + + auto pItems = AllocateListBoxCells(capacity); + for (int i = 0; i < column.CellCount; ++i) + pItems[i] = column.Cells[i]; + + DeleteListBoxCells(column.Cells, column.CellCapacity); + column.Cells = pItems; + column.CellCapacity = capacity; +} + +static void EnsureListBoxCellCount(WWUIListBoxColumn& column, int count, WWUIListBoxCellFormat defaultFormat) +{ + if (count <= column.CellCount) + return; + + if (count > column.CellCapacity) + { + int capacity = column.CellCapacity; + do + { + capacity = std::max(capacity * 2, 10); + } + while (capacity < count); + + ResizeListBoxCellStorage(column, capacity); + } + + for (int i = column.CellCount; i < count; ++i) + { + auto& cell = column.Cells[i]; + ResetListBoxCell(cell); + cell.Format = defaultFormat; + } + + column.CellCount = count; +} + +static void ClearListBoxColumnCells(WWUIListBoxColumn& column, bool releaseStorage) +{ + if (!column.Cells) + { + column.CellCount = 0; + column.CellCapacity = 0; + return; + } + + for (int i = 0; i < column.CellCapacity; ++i) + ResetListBoxCell(column.Cells[i]); + + column.CellCount = 0; + + if (releaseStorage) + { + DeleteListBoxCells(column.Cells, column.CellCapacity); + column.CellCapacity = 0; + } +} + +static void DeleteListBoxColumns(WWUIListBoxColumnArray*& pColumns) +{ + if (!pColumns) + return; + + for (int i = 0; i < pColumns->Count; ++i) + ClearListBoxColumnCells(pColumns->Items[i], true); + + YRMemory::Deallocate(pColumns->Items); + YRMemory::Deallocate(pColumns); + pColumns = nullptr; +} + +static WWUIListBoxColumnArray* CreateListBoxColumnArray() +{ + auto pArray = static_cast(YRMemory::Allocate(sizeof(WWUIListBoxColumnArray))); + if (pArray) + *pArray = {}; + + return pArray; +} + +static void ResizeListBoxColumnStorage(WWUIListBoxColumnArray& columns, int capacity) +{ + if (capacity < 10) + capacity = 10; + + auto pItems = static_cast(YRMemory::Allocate(sizeof(WWUIListBoxColumn) * capacity)); + std::memset(pItems, 0, sizeof(WWUIListBoxColumn) * capacity); + + if (columns.Items && columns.Count > 0) + std::memcpy(pItems, columns.Items, sizeof(WWUIListBoxColumn) * columns.Count); + + YRMemory::Deallocate(columns.Items); + columns.Items = pItems; + columns.Capacity = capacity; +} + +static WWUIListBoxColumn* FindListBoxColumn(WWUIListBoxColumnArray* pColumns, int x) +{ + if (!pColumns) + return nullptr; + + for (int i = 0; i < pColumns->Count; ++i) + { + if (pColumns->Items[i].X == x) + return &pColumns->Items[i]; + } + + return nullptr; +} + +static WWUIListBoxColumn* FindListBoxColumnAtX(WWUIListBoxColumnArray* pColumns, int x) +{ + if (!pColumns) + return nullptr; + + WWUIListBoxColumn* pBest = nullptr; + for (int i = 0; i < pColumns->Count; ++i) + { + auto& column = pColumns->Items[i]; + if (column.X <= x && (!pBest || column.X > pBest->X)) + pBest = &column; + } + + return pBest; +} + +static WWUIListBoxTextEntry* AllocateListBoxTextEntry(OwnerDrawDialogElement& data, const wchar_t* pText, bool isWide) +{ + if (!pText) + pText = L""; + + const size_t length = std::wcslen(pText); + const size_t bytes = sizeof(WWUIListBoxTextEntry) + (length + 1) * sizeof(wchar_t) + ListBoxTextEntryInlineBytes; + auto pEntry = static_cast(YRMemory::Allocate(bytes)); + if (!pEntry) + return nullptr; + + pEntry->Next = data.AsListBox().TextEntries(); + pEntry->ItemData = 0; + pEntry->Text = reinterpret_cast(reinterpret_cast(pEntry) + sizeof(WWUIListBoxTextEntry)); + pEntry->IsWide = isWide ? 1 : 0; + std::wcscpy(pEntry->Text, pText); + data.AsListBox().TextEntries() = pEntry; + return pEntry; +} + +static void RemoveListBoxTextEntry(OwnerDrawDialogElement& data, WWUIListBoxTextEntry* pEntry) +{ + if (!pEntry) + return; + + WWUIListBoxTextEntry* pPrevious = nullptr; + for (auto pCurrent = data.AsListBox().TextEntries(); pCurrent; pCurrent = pCurrent->Next) + { + if (pCurrent != pEntry) + { + pPrevious = pCurrent; + continue; + } + + if (pPrevious) + pPrevious->Next = pCurrent->Next; + else + data.AsListBox().TextEntries() = pCurrent->Next; + + YRMemory::Deallocate(pCurrent); + return; + } +} + +static void ClearListBoxTextEntries(OwnerDrawDialogElement& data) +{ + auto pEntry = data.AsListBox().TextEntries(); + while (pEntry) + { + auto pNext = pEntry->Next; + YRMemory::Deallocate(pEntry); + pEntry = pNext; + } + + data.AsListBox().TextEntries() = nullptr; +} + +static WWUIListBoxTextEntry* GetListBoxTextEntry(WNDPROC pOriginalWndProc, HWND hWnd, int index) +{ + const auto result = CallSelectedHandler(pOriginalWndProc, hWnd, LB_GETITEMDATA, index, 0); + if (result == LB_ERR || !result) + return nullptr; + + return reinterpret_cast(result); +} + +static void RemoveListBoxRow(OwnerDrawDialogElement& data, int index) +{ + RemoveIntArrayItem(data.AsListBox().ItemData(), index); + RemoveIntArrayItem(data.AsListBox().SelectionStates(), index); + + if (auto pColumns = data.AsListBox().Columns()) + { + for (int i = 0; i < pColumns->Count; ++i) + { + auto& column = pColumns->Items[i]; + if (index < 0 || index >= column.CellCount) + continue; + + for (int cellIndex = index; cellIndex < column.CellCount - 1; ++cellIndex) + column.Cells[cellIndex] = column.Cells[cellIndex + 1]; + + ResetListBoxCell(column.Cells[column.CellCount - 1]); + --column.CellCount; + + if (column.CellCapacity > 10 && column.CellCount * 3 <= column.CellCapacity) + ResizeListBoxCellStorage(column, std::max(column.CellCapacity / 2, 10)); + } + } +} + +static void InsertListBoxRow(OwnerDrawDialogElement& data, int index) +{ + InsertIntArrayItem(data.AsListBox().ItemData(), index, -1); + InsertIntArrayItem(data.AsListBox().SelectionStates(), index, 0); + + if (auto pColumns = data.AsListBox().Columns()) + { + for (int i = 0; i < pColumns->Count; ++i) + { + auto& column = pColumns->Items[i]; + const int insertIndex = std::clamp(index, 0, column.CellCount); + EnsureListBoxCellCount( + column, + column.CellCount + 1, + i == 0 ? WWUIListBoxCellFormat::ItemText : WWUIListBoxCellFormat::Empty); + + for (int cellIndex = column.CellCount - 1; cellIndex > insertIndex; --cellIndex) + column.Cells[cellIndex] = column.Cells[cellIndex - 1]; + + ResetListBoxCell(column.Cells[insertIndex]); + column.Cells[insertIndex].Format = i == 0 ? WWUIListBoxCellFormat::ItemText : WWUIListBoxCellFormat::Empty; + } + } +} + +static void ClearListBoxRows(OwnerDrawDialogElement& data, bool destroyColumns) +{ + DeleteIntArray(data.AsListBox().ItemData()); + DeleteIntArray(data.AsListBox().SelectionStates()); + data.AsListBox().TopIndex() = 0; + data.AsListBox().CurrentSelection() = -1; + + if (destroyColumns) + { + DeleteListBoxColumns(data.AsListBox().Columns()); + } + else if (auto pColumns = data.AsListBox().Columns()) + { + for (int i = 0; i < pColumns->Count; ++i) + ClearListBoxColumnCells(pColumns->Items[i], false); + } +} + +static void PaintListBoxCellText( + HWND hWnd, + BitFont* pFont, + RectangleStruct rect, + const wchar_t* pText, + COLORREF textColor, + int maxWidth) +{ + if (!pText) + pText = L""; + + wchar_t buffer[512] {}; + std::wcsncpy(buffer, pText, std::size(buffer) - 1); + + int textWidth = 0; + int textHeight = 0; + auto pMeasureFont = pFont ? pFont : BitFont::Instance; + + if (pMeasureFont) + { + size_t length = std::wcslen(buffer); + while (length > 0) + { + pMeasureFont->GetTextDimension(buffer, &textWidth, &textHeight, maxWidth); + if (textWidth <= maxWidth) + break; + + --length; + buffer[length] = L'\0'; + if (length > 3) + std::wcscat(buffer, L"..."); + } + } + + OwnerDraw::PrintTextFixedLength( + textColor, + pFont, + &rect, + buffer, + static_cast(std::wcslen(buffer)), + 0, + 0, + nullptr, + 0); +} + +static void DrawListBoxProgressCell(RectangleStruct rect, int value) +{ + if (!DSurface::Alternate) + return; + + WORD color = static_cast(ConvertRGBToSurfaceColor(RGB(0, 0, 192))); + if (value < 0) + { + color = static_cast(ConvertRGBToSurfaceColor(RGB(0, 0, 192))); + value = 1000; + } + else if (value < 300) + { + color = static_cast(ConvertRGBToSurfaceColor(RGB(0, 192, 0))); + } + else if (value < 500) + { + color = static_cast(ConvertRGBToSurfaceColor(RGB(192, 192, 0))); + } + else + { + color = static_cast(ConvertRGBToSurfaceColor(RGB(192, 0, 0))); + } + + BlendGradientRect(rect, DSurface::Alternate, color, (value << 16) / 1000); +} + +static void PaintListBox(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect, WNDPROC pOriginalWndProc) +{ + if (data.SkipDraw) + { + ::ValidateRect(hWnd, nullptr); + return; + } + + if (!DSurface::Alternate) + return; + + RectangleStruct drawRect + { + ownerRect.left, + ownerRect.top, + clientRect.right - clientRect.left, + clientRect.bottom - clientRect.top + }; + + OwnerDraw::CopyDimmedBackground(&drawRect, hWnd, static_cast(data.Alpha)); + DrawBeveledBorder(DSurface::Alternate, drawRect, 2, -1); + + RECT updateRect {}; + if (!::GetUpdateRect(hWnd, &updateRect, FALSE)) + return; + + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + int itemIndex = static_cast(::SendMessageA(hWnd, LB_GETTOPINDEX, 0, 0)); + const LONG style = ::GetWindowLongA(hWnd, GWL_STYLE); + const int selectionColor = ConvertRGBToSurfaceColor(ListBoxSelectionFillColor()); + auto pFont = data.AsListBox().Font(); + + while (itemIndex >= 0 && itemIndex < itemCount) + { + RECT itemRect {}; + if (::SendMessageA(hWnd, LB_GETITEMRECT, itemIndex, reinterpret_cast(&itemRect)) == LB_ERR) + break; + + if (clientRect.top + itemRect.bottom > clientRect.bottom) + break; + + wchar_t itemText[512] {}; + if (auto pEntry = GetListBoxTextEntry(pOriginalWndProc, hWnd, itemIndex)) + std::wcsncpy(itemText, pEntry->Text ? pEntry->Text : L"", std::size(itemText) - 1); + + const bool selected = ::SendMessageA(hWnd, LB_GETSEL, itemIndex, 0) > 0; + RectangleStruct rowRect + { + ownerRect.left + itemRect.left, + ownerRect.top + itemRect.top, + itemRect.right - itemRect.left, + itemRect.bottom - itemRect.top + }; + + if (selected) + DSurface::Alternate->FillRect(&rowRect, selectionColor); + + if (auto pColumns = data.AsListBox().Columns()) + { + for (int columnIndex = 0; columnIndex < pColumns->Count; ++columnIndex) + { + auto& column = pColumns->Items[columnIndex]; + if (itemIndex < 0 || itemIndex >= column.CellCount || !column.Cells) + continue; + + auto& cell = column.Cells[itemIndex]; + if (cell.Format == WWUIListBoxCellFormat::Empty) + continue; + + RectangleStruct cellRect + { + ownerRect.left + itemRect.left + column.X, + ownerRect.top + itemRect.top, + column.Width ? column.Width : rowRect.Width, + rowRect.Height + }; + + if (cellRect.Width <= 0) + cellRect.Width = 0xFFFF; + + const int availableWidth = std::min( + cellRect.Width, + static_cast(ownerRect.right - cellRect.X)); + + switch (cell.Format) + { + case WWUIListBoxCellFormat::Text: + case WWUIListBoxCellFormat::ItemText: + { + COLORREF textColor = cell.TextColor == static_cast(-1) + ? ListBoxTextColor() + : cell.TextColor; + + if (style & WS_DISABLED) + textColor = ListBoxDisabledTextColor(); + + const wchar_t* pText = cell.Format == WWUIListBoxCellFormat::Text + ? GetWideTextBuffer(cell.PrimaryText) + : itemText; + + PaintListBoxCellText(hWnd, pFont, cellRect, pText, textColor, availableWidth); + break; + } + + case WWUIListBoxCellFormat::Image: + if (cell.Image) + { + RectangleStruct imageRect + { + cellRect.X, + cellRect.Y + (cellRect.Height - cell.Image->GetHeight()) / 2, + cell.Image->GetWidth(), + cell.Image->GetHeight() + }; + PCX::Instance.BlitToSurface(&imageRect, DSurface::Alternate, static_cast(cell.Image)); + } + break; + + case WWUIListBoxCellFormat::Progress: + { + RectangleStruct progressRect { cellRect.X, cellRect.Y, 32, 12 }; + DrawListBoxProgressCell(progressRect, cell.Value); + break; + } + + default: + break; + } + } + } + else + { + COLORREF textColor = GetIntArrayValue(data.AsListBox().ItemData(), itemIndex, ListBoxTextColor()); + if (textColor == static_cast(-1)) + textColor = ListBoxTextColor(); + + if (style & WS_DISABLED) + textColor = ListBoxDisabledTextColor(); + + RectangleStruct textRect + { + rowRect.X + 2, + rowRect.Y, + rowRect.Width, + rowRect.Height + }; + + OwnerDraw::PrintTextFixedLength( + textColor, + pFont, + &textRect, + itemText, + static_cast(std::wcslen(itemText)), + 0, + 0, + nullptr, + 0); + } + + ++itemIndex; + } + + ::ValidateRect(hWnd, &updateRect); + if (data.AsListBox().ScrollBarHwnd()) + ::InvalidateRect(data.AsListBox().ScrollBarHwnd(), nullptr, FALSE); +} + +static void SyncListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, int itemCount, int itemHeight) +{ + if (itemHeight <= 0) + itemHeight = 1; + + const int visibleItems = itemHeight ? (clientRect.bottom - clientRect.top) / itemHeight : 0; + int maxTopIndex = itemCount - visibleItems; + if (maxTopIndex < 0) + maxTopIndex = 0; + + if (data.AsListBox().TopIndex() > maxTopIndex) + data.AsListBox().TopIndex() = maxTopIndex; + + if (data.AsListBox().ScrollBarHwnd() && reinterpret_cast(data.AsListBox().ScrollBarHwnd()) > 1) + { + SCROLLINFO scrollInfo {}; + scrollInfo.cbSize = sizeof(scrollInfo); + scrollInfo.fMask = SIF_RANGE | SIF_POS; + scrollInfo.nMin = 0; + scrollInfo.nMax = maxTopIndex; + scrollInfo.nPos = data.AsListBox().TopIndex(); + ::SendMessageA(data.AsListBox().ScrollBarHwnd(), SBM_SETSCROLLINFO, 0, reinterpret_cast(&scrollInfo)); + } +} + +static void UpdateListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect) +{ + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1); + const bool needsScrollbar = itemCount * itemHeight > clientRect.bottom - clientRect.top; + const int scrollBarWidth = 2 * OwnerDraw::ControlInsetPx + ListBoxScrollBarExtraWidth; + + SyncListBoxScrollBar(hWnd, data, clientRect, itemCount, itemHeight); + + if (needsScrollbar) + { + if (!data.AsListBox().ScrollBarHwnd()) + { + data.AsListBox().ScrollBarHwnd() = reinterpret_cast(1); + + const HWND parentHwnd = ::GetParent(hWnd); + RECT parentRect {}; + RECT listRect {}; + OwnerDraw::GetRectangle(parentHwnd, &parentRect); + OwnerDraw::GetRectangle(hWnd, &listRect); + + const int x = listRect.left - parentRect.left - scrollBarWidth + clientRect.right + 1; + const int y = listRect.top - parentRect.top + clientRect.top; + const int height = listRect.bottom - listRect.top; + + data.AsListBox().ScrollBarHwnd() = ::CreateWindowExA( + 0, + "Scrollbar", + nullptr, + WS_CHILD | WS_VISIBLE | SBS_VERT | WS_TABSTOP, + x, + y, + scrollBarWidth, + height, + parentHwnd, + nullptr, + reinterpret_cast(Phobos::hInstance), + nullptr); + + data.AsListBox().ScrollBarWidth() = scrollBarWidth; + OwnerDraw::RegisterChildControlProc(data.AsListBox().ScrollBarHwnd(), 0); + + if (auto pScrollData = FindOwnerDrawData(data.AsListBox().ScrollBarHwnd())) + { + pScrollData->AsScrollBar().NotifyHwnd() = hWnd; + pScrollData->AsScrollBar().Disabled() = false; + } + + SyncListBoxScrollBar(hWnd, data, clientRect, itemCount, itemHeight); + + ::SetWindowPos( + hWnd, + nullptr, + 0, + 0, + listRect.right - listRect.left - scrollBarWidth, + listRect.bottom - listRect.top, + SWP_NOMOVE | SWP_NOZORDER); + + ::ShowWindow(data.AsListBox().ScrollBarHwnd(), SW_SHOW); + ::BringWindowToTop(data.AsListBox().ScrollBarHwnd()); + ::InvalidateRect(data.AsListBox().ScrollBarHwnd(), nullptr, FALSE); + ::UpdateWindow(data.AsListBox().ScrollBarHwnd()); + } + + return; + } + + if (!data.AsListBox().ScrollBarHwnd() || data.NeedsControlImage) + return; + + const HWND scrollBarHwnd = data.AsListBox().ScrollBarHwnd(); + ::DestroyWindow(scrollBarHwnd); + CleanupDestroyedWindow(scrollBarHwnd); + data.AsListBox().ScrollBarHwnd() = nullptr; + + RECT listRect {}; + ::GetWindowRect(hWnd, &listRect); + ::SetWindowPos( + hWnd, + nullptr, + 0, + 0, + listRect.right - listRect.left + scrollBarWidth, + listRect.bottom - listRect.top, + SWP_NOMOVE | SWP_NOZORDER); + + data.AsListBox().ScrollBarWidth() = 0; +} + +LRESULT CALLBACK WWUI::ListBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto pData = FindOwnerDrawData(hWnd); + if (!pData) + return 0; + + auto& data = *pData; + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + + RECT clientRect {}; + ::GetClientRect(hWnd, &clientRect); + + RECT ownerRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + const int inset = OwnerDraw::ControlInsetPx; + clientRect.right -= 2 * inset; + clientRect.bottom -= 2 * inset; + ownerRect.left += inset; + ownerRect.top += inset; + ownerRect.right -= inset; + ownerRect.bottom -= inset; + + const bool updateScrollBar = message != LB_GETCOUNT + && message != LB_GETITEMHEIGHT + && message != WM_VSCROLL; + + if (updateScrollBar) + { + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1); + SyncListBoxScrollBar(hWnd, data, clientRect, itemCount, itemHeight); + } + + auto finish = [&](LRESULT result) -> LRESULT + { + if (updateScrollBar) + UpdateListBoxScrollBar(hWnd, data, clientRect); + + return result; + }; + + auto forwardOriginal = [&]() -> LRESULT + { + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + }; + + auto setSelection = [&](int index, int selected) + { + SetIntArrayValue(data.AsListBox().SelectionStates(), index, selected ? 1 : 0, 0); + }; + + auto playClick = []() + { + if (RulesClass::Instance) + VocClass::PlayGlobal(RulesClass::Instance->GenericClick, 0x2000, 1.0f); + }; + + auto addOrInsertString = [&](bool wideText, bool insert) -> LRESULT + { + char narrowText[5120] {}; + wchar_t wideBuffer[5120] {}; + + const LPARAM nativeTextParam = [&]() -> LPARAM + { + if (wideText) + { + const auto pWideText = reinterpret_cast(lParam); + std::wcsncpy(wideBuffer, pWideText ? pWideText : L"", std::size(wideBuffer) - 1); + WideToCharString(narrowText, std::size(narrowText), wideBuffer); + return reinterpret_cast(narrowText); + } + + const auto pText = reinterpret_cast(lParam); + std::strncpy(narrowText, pText ? pText : "", std::size(narrowText) - 1); + CharToWideString(wideBuffer, std::size(wideBuffer), narrowText); + return reinterpret_cast(narrowText); + }(); + + const UINT nativeMessage = insert ? LB_INSERTSTRING : LB_ADDSTRING; + const WPARAM nativeIndex = insert ? wParam : 0; + const auto nativeResult = CallSelectedHandler(pOriginalWndProc, hWnd, nativeMessage, nativeIndex, nativeTextParam); + if (nativeResult == LB_ERR || nativeResult == LB_ERRSPACE) + return nativeResult; + + const int itemIndex = static_cast(nativeResult); + auto pEntry = AllocateListBoxTextEntry(data, wideBuffer, wideText); + if (!pEntry) + { + CallSelectedHandler(pOriginalWndProc, hWnd, LB_DELETESTRING, itemIndex, 0); + return LB_ERRSPACE; + } + + const auto setDataResult = CallSelectedHandler( + pOriginalWndProc, + hWnd, + LB_SETITEMDATA, + itemIndex, + reinterpret_cast(pEntry)); + + if (setDataResult == LB_ERR) + { + RemoveListBoxTextEntry(data, pEntry); + CallSelectedHandler(pOriginalWndProc, hWnd, LB_DELETESTRING, itemIndex, 0); + return LB_ERR; + } + + InsertListBoxRow(data, itemIndex); + return itemIndex; + }; + + auto findString = [&](bool wideText, bool exact, bool select) -> LRESULT + { + wchar_t needle[5120] {}; + if (wideText) + { + const auto pText = reinterpret_cast(lParam); + std::wcsncpy(needle, pText ? pText : L"", std::size(needle) - 1); + } + else + { + CharToWideString(needle, std::size(needle), reinterpret_cast(lParam)); + } + + const int count = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + int index = static_cast(wParam); + if (index < 0) + index = 0; + + if (index >= count) + return LB_ERR; + + const size_t needleLength = std::wcslen(needle); + for (; index < count; ++index) + { + const auto pEntry = GetListBoxTextEntry(pOriginalWndProc, hWnd, index); + const wchar_t* pText = pEntry && pEntry->Text ? pEntry->Text : L""; + const bool match = exact + ? _wcsicmp(needle, pText) == 0 + : _wcsnicmp(needle, pText, needleLength) == 0; + + if (!match) + continue; + + if (select) + ::SendMessageA(hWnd, LB_SETCURSEL, index, 0); + + return index; + } + + return LB_ERR; + }; + + switch (message) + { + case WM_SIZE: + if (data.AsListBox().ScrollBarHwnd() && reinterpret_cast(data.AsListBox().ScrollBarHwnd()) > 1) + { + const HWND parentHwnd = ::GetParent(hWnd); + RECT parentRect {}; + RECT listRect {}; + OwnerDraw::GetRectangle(parentHwnd, &parentRect); + OwnerDraw::GetRectangle(hWnd, &listRect); + ::MoveWindow( + data.AsListBox().ScrollBarHwnd(), + listRect.right - parentRect.left, + listRect.top - parentRect.top, + 2 * inset + ListBoxScrollBarExtraWidth, + listRect.bottom - listRect.top, + TRUE); + } + + if (data.CacheSurface + && (LOWORD(lParam) != data.CacheSurface->GetWidth() || HIWORD(lParam) != data.CacheSurface->GetHeight())) + { + DeleteSurfaceObject(data.CacheSurface); + --OwnerDraw::CachedSurfaceCount; + } + + return finish(forwardOriginal()); + + case WM_PAINT: + PaintListBox(hWnd, data, clientRect, ownerRect, pOriginalWndProc); + return finish(0); + + case WM_ERASEBKGND: + return 0; + + case WM_DELETEITEM: + if (lParam) + { + const auto pDeleteItem = reinterpret_cast(lParam); + RemoveListBoxTextEntry(data, reinterpret_cast(pDeleteItem->itemData)); + } + return 1; + + case WM_SETFONT: + { + TEXTMETRICA metrics {}; + if (const HDC hdc = ::GetDC(hWnd)) + { + ::GetTextMetricsA(hdc, &metrics); + ::ReleaseDC(hWnd, hdc); + } + + ::SendMessageA(hWnd, LB_SETITEMHEIGHT, static_cast(-1), LOWORD(metrics.tmHeight + 2)); + data.AsListBox().SavedFont() = static_cast(wParam); + return 0; + } + + case WM_VSCROLL: + if (data.AsListBox().ScrollBarHwnd()) + { + const auto position = ::SendMessageA(data.AsListBox().ScrollBarHwnd(), SBM_GETPOS, 0, 0); + if (position != ::SendMessageA(hWnd, LB_GETTOPINDEX, 0, 0)) + ::SendMessageA(hWnd, LB_SETTOPINDEX, position, 0); + } + return 0; + + case WM_RBUTTONDOWN: + if (::GetWindowLongA(hWnd, GWL_STYLE) & LBS_MULTIPLESEL) + { + if (auto pSelections = data.AsListBox().SelectionStates()) + { + for (int i = 0; i < pSelections->Count; ++i) + pSelections->Items[i] = 0; + } + + data.AsListBox().CurrentSelection() = -1; + NotifyListBoxSelectionChanged(hWnd); + ::InvalidateRect(hWnd, nullptr, FALSE); + return 0; + } + break; + + case WM_LBUTTONDBLCLK: + PostListBoxDoubleClick(hWnd); + return 0; + + case WM_LBUTTONDOWN: + { + const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1); + const int itemIndex = data.AsListBox().TopIndex() + SignedHighWord(lParam) / itemHeight; + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + if (itemIndex < 0 || itemIndex >= itemCount) + return 0; + + const LONG style = ::GetWindowLongA(hWnd, GWL_STYLE); + ::SetFocus(hWnd); + const auto previousImageState = ::SendMessageA(hWnd, WW_SETHASIMAGE, 0, 1); + + if (style & LBS_MULTIPLESEL) + { + const bool selected = ::SendMessageA(hWnd, LB_GETSEL, itemIndex, 0) == 0; + playClick(); + ::SendMessageA(hWnd, LB_SETSEL, selected, itemIndex); + } + else if (!(style & LBS_NOSEL)) + { + playClick(); + ::SendMessageA(hWnd, LB_SETCURSEL, itemIndex, 0); + } + + ::SendMessageA(hWnd, WW_SETHASIMAGE, 0, previousImageState); + ::InvalidateRect(hWnd, nullptr, FALSE); + NotifyListBoxSelectionChanged(hWnd); + return 0; + } + + case LB_SETSEL: + { + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + int index = static_cast(lParam); + if (index < -1) + return LB_ERR; + + if (index >= itemCount) + index = itemCount - 1; + + if (!data.AsListBox().SelectionStates()) + data.AsListBox().SelectionStates() = CreateIntArray(); + + if (index == -1) + { + if (data.AsListBox().SelectionStates()) + { + EnsureIntArraySize(*data.AsListBox().SelectionStates(), itemCount, 0); + for (int i = 0; i < data.AsListBox().SelectionStates()->Count; ++i) + data.AsListBox().SelectionStates()->Items[i] = wParam ? 1 : 0; + } + } + else + { + setSelection(index, wParam ? 1 : 0); + } + + NotifyListBoxSelectionChanged(hWnd); + ::InvalidateRect(hWnd, nullptr, FALSE); + return finish(0); + } + + case LB_GETSEL: + return GetIntArrayValue(data.AsListBox().SelectionStates(), static_cast(wParam), 0); + + case LB_GETSELCOUNT: + { + int count = 0; + if (auto pSelections = data.AsListBox().SelectionStates()) + { + for (int i = 0; i < pSelections->Count; ++i) + { + if (pSelections->Items[i]) + ++count; + } + } + return count; + } + + case LB_GETSELITEMS: + { + int written = 0; + auto pOut = reinterpret_cast(lParam); + if (pOut) + { + if (auto pSelections = data.AsListBox().SelectionStates()) + { + for (int i = 0; i < pSelections->Count && written < static_cast(wParam); ++i) + { + if (pSelections->Items[i]) + pOut[written++] = i; + } + } + } + return written; + } + + case LB_SELITEMRANGE: + { + int first = SignedLowWord(lParam); + int last = SignedHighWord(lParam); + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + if (first < 0 || last < first) + return LB_ERR; + + if (last >= itemCount) + last = itemCount - 1; + + if (!data.AsListBox().SelectionStates()) + data.AsListBox().SelectionStates() = CreateIntArray(); + + if (data.AsListBox().SelectionStates()) + { + EnsureIntArraySize(*data.AsListBox().SelectionStates(), last + 1, 0); + for (int i = first; i <= last; ++i) + data.AsListBox().SelectionStates()->Items[i] = wParam ? 1 : 0; + } + + NotifyListBoxSelectionChanged(hWnd); + return finish(0); + } + + case LB_SETCURSEL: + { + int selection = static_cast(wParam); + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + if (selection >= -1 && selection < itemCount) + { + if (data.AsListBox().CurrentSelection() != -1) + setSelection(data.AsListBox().CurrentSelection(), 0); + + data.AsListBox().CurrentSelection() = selection; + if (selection != -1) + setSelection(selection, 1); + } + + NotifyListBoxSelectionChanged(hWnd); + ::InvalidateRect(hWnd, nullptr, FALSE); + return finish(0); + } + + case LB_GETCURSEL: + return data.AsListBox().CurrentSelection(); + + case LB_GETTOPINDEX: + return data.AsListBox().TopIndex(); + + case LB_SETTOPINDEX: + { + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1); + const int visibleItems = (clientRect.bottom - clientRect.top) / itemHeight; + int topIndex = static_cast(wParam); + if (topIndex < 0) + topIndex = 0; + + if (itemCount - visibleItems <= 0) + topIndex = 0; + else if (topIndex > itemCount - visibleItems) + topIndex = itemCount - visibleItems; + + if (topIndex != data.AsListBox().TopIndex()) + { + data.AsListBox().TopIndex() = topIndex; + ::InvalidateRect(hWnd, nullptr, FALSE); + } + + return finish(0); + } + + case LB_GETITEMRECT: + { + const int index = static_cast(wParam); + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1); + const int visibleIndex = index - data.AsListBox().TopIndex(); + + if (index < data.AsListBox().TopIndex() || index >= itemCount || visibleIndex > (clientRect.bottom - clientRect.top) / itemHeight) + return LB_ERR; + + if (auto pRect = reinterpret_cast(lParam)) + { + pRect->left = clientRect.left; + pRect->top = visibleIndex * itemHeight; + pRect->right = clientRect.right - clientRect.left; + pRect->bottom = pRect->top + itemHeight; + } + return 0; + } + + case LB_GETITEMDATA: + case LB_SETITEMDATA: + case CB_GETITEMDATA: + case CB_SETITEMDATA: + { + const auto pEntry = GetListBoxTextEntry(pOriginalWndProc, hWnd, static_cast(wParam)); + if (!pEntry) + return LB_ERR; + + if (message == LB_GETITEMDATA || message == CB_GETITEMDATA) + return pEntry->ItemData; + + pEntry->ItemData = static_cast(lParam); + return reinterpret_cast(pEntry); + } + + case LB_DELETESTRING: + { + const int index = static_cast(wParam); + const auto pEntry = GetListBoxTextEntry(pOriginalWndProc, hWnd, index); + RemoveListBoxRow(data, index); + const auto result = CallSelectedHandler(pOriginalWndProc, hWnd, LB_DELETESTRING, wParam, lParam); + RemoveListBoxTextEntry(data, pEntry); + return finish(result); + } + + case LB_RESETCONTENT: + ClearListBoxRows(data, false); + ClearListBoxTextEntries(data); + NotifyListBoxSelectionChanged(hWnd); + return finish(CallSelectedHandler(pOriginalWndProc, hWnd, LB_RESETCONTENT, wParam, lParam)); + + case WM_NCDESTROY: + ClearListBoxRows(data, true); + ClearListBoxTextEntries(data); + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + + case LB_GETTEXTLEN: + case WW_GETTEXTW: + case WW_GETTEXTA: + case WW_LB_GETTEXTW: + case WW_LB_GETTEXTA: + case WW_LB_GETITEMTEXTFORMAT: + { + const auto pEntry = GetListBoxTextEntry(pOriginalWndProc, hWnd, static_cast(wParam)); + if (!pEntry) + return LB_ERR; + + if (message == WW_LB_GETITEMTEXTFORMAT) + return pEntry->IsWide; + + const wchar_t* pText = pEntry->Text ? pEntry->Text : L""; + const auto length = static_cast(std::wcslen(pText)); + if (message == LB_GETTEXTLEN) + return length; + + if (lParam) + { + if (message == WW_GETTEXTA || message == WW_LB_GETTEXTA) + WideToCharString(reinterpret_cast(lParam), static_cast(length + 1), pText); + else + std::wcscpy(reinterpret_cast(lParam), pText); + } + return length; + } + + case WW_LB_FINDSTRINGA: + return findString(false, false, false); + + case WW_LB_FINDSTRINGEXACTA: + return findString(false, true, false); + + case WW_LB_SELECTSTRINGA: + return findString(false, false, true); + + case WW_LB_FINDSTRINGW: + return findString(true, false, false); + + case WW_LB_FINDSTRINGEXACTW: + return findString(true, true, false); + + case WW_LB_SELECTSTRINGW: + return findString(true, false, true); + + case WW_LB_INSERTSTRINGA: + return finish(addOrInsertString(false, true)); + + case WW_LB_ADDSTRINGA: + return finish(addOrInsertString(false, false)); + + case WW_LB_INSERTSTRINGW: + return finish(addOrInsertString(true, true)); + + case WW_LB_ADDSTRINGW: + return finish(addOrInsertString(true, false)); + + case WW_QUERYTOOLTIPHIT: + { + const int x = SignedLowWord(lParam); + const int y = SignedHighWord(lParam); + if (x < clientRect.right && y < clientRect.bottom) + { + const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1); + const int itemIndex = data.AsListBox().TopIndex() + y / itemHeight; + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + if (itemIndex >= 0 && itemIndex < itemCount) + return itemIndex; + } + return -1; + } + + case WW_LB_GETSCROLLBARHWND: + return reinterpret_cast(data.AsListBox().ScrollBarHwnd()); + + case WW_LB_ADDCOLUMN: + { + if (!data.AsListBox().Columns()) + data.AsListBox().Columns() = CreateListBoxColumnArray(); + + auto pColumns = data.AsListBox().Columns(); + if (!pColumns) + return -1; + + const int x = static_cast(lParam); + if (FindListBoxColumn(pColumns, x)) + return x; + + if (pColumns->Count >= pColumns->Capacity) + ResizeListBoxColumnStorage(*pColumns, std::max(pColumns->Capacity * 2, 10)); + + auto& column = pColumns->Items[pColumns->Count++]; + column = {}; + column.X = x; + column.Width = static_cast(wParam); + return x; + } + + case WW_LB_REMOVECOLUMN: + { + auto pColumns = data.AsListBox().Columns(); + if (!pColumns) + return -1; + + const int x = static_cast(lParam); + for (int i = 0; i < pColumns->Count; ++i) + { + if (pColumns->Items[i].X != x) + continue; + + ClearListBoxColumnCells(pColumns->Items[i], true); + if (i < pColumns->Count - 1) + { + std::memmove( + &pColumns->Items[i], + &pColumns->Items[i + 1], + sizeof(WWUIListBoxColumn) * (pColumns->Count - i - 1)); + } + --pColumns->Count; + std::memset(&pColumns->Items[pColumns->Count], 0, sizeof(WWUIListBoxColumn)); + return x; + } + return -1; + } + + case WW_LB_SETCELLTEXT: + { + auto pColumns = data.AsListBox().Columns(); + const int columnX = LOWORD(wParam); + const int rowIndex = HIWORD(wParam); + auto pColumn = FindListBoxColumn(pColumns, columnX); + + if (!pColumn || rowIndex >= ::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)) + return -1; + + const auto defaultFormat = pColumn == &pColumns->Items[0] + ? WWUIListBoxCellFormat::ItemText + : WWUIListBoxCellFormat::Empty; + + EnsureListBoxCellCount(*pColumn, rowIndex + 1, defaultFormat); + auto& target = pColumn->Cells[rowIndex]; + ResetListBoxCell(target); + + if (const auto pSource = reinterpret_cast(lParam)) + target = *pSource; + + return columnX; + } + + case WW_LB_GETCELLTEXT: + { + const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); + const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1); + const int rowIndex = data.AsListBox().TopIndex() + SignedHighWord(wParam) / itemHeight; + const int x = SignedLowWord(wParam); + auto pColumn = FindListBoxColumnAtX(data.AsListBox().Columns(), x); + if (!pColumn || rowIndex < 0 || rowIndex >= itemCount || rowIndex >= pColumn->CellCount) + return 0; + + const auto& text = pColumn->Cells[rowIndex].SecondaryText; + if (lParam) + std::wcscpy(reinterpret_cast(lParam), GetWideTextBuffer(text)); + + return IsEmpty(text) ? 1 : 0; + } + + case WW_INITDIALOG: + { + data.AsListBox().CurrentSelection() = -1; + + int fontHeight = 10; + if (const auto pFont = data.AsListBox().Font() ? data.AsListBox().Font() : BitFont::Instance) + { + if (pFont->InternalPTR) + fontHeight = pFont->InternalPTR->FontHeight; + } + + ::SendMessageA(hWnd, LB_SETITEMHEIGHT, static_cast(-1), LOWORD(fontHeight + 2)); + return finish(0); + } + + case WW_SETCOLOR: + SetIntArrayValue(data.AsListBox().ItemData(), static_cast(wParam), static_cast(lParam), -1); + ::InvalidateRect(hWnd, nullptr, FALSE); + return finish(0); + + default: + break; + } + + return finish(forwardOriginal()); +} diff --git a/src/OwnerDraw/OwnerDraw.Hooks.cpp b/src/OwnerDraw/OwnerDraw.Hooks.cpp new file mode 100644 index 0000000000..d1f1ee1b80 --- /dev/null +++ b/src/OwnerDraw/OwnerDraw.Hooks.cpp @@ -0,0 +1,22 @@ +#include + +#include "OwnerDraw.h" + +DEFINE_PATCH_TYPED(void*, 0x60FF06, WWUI::OwnerDrawWindowProc); +DEFINE_FUNCTION_JUMP(LJMP, 0x610CA0, WWUI::OwnerDrawWindowProc); +DEFINE_FUNCTION_JUMP(LJMP, 0x612B70, WWUI::OwnerDrawCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x6163A0, WWUI::CheckboxCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x616980, WWUI::RadioCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x6137D0, WWUI::TabCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x614190, WWUI::EditCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x614B30, WWUI::NewEditCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x6153E0, WWUI::StaticCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x612A60, WWUI::SysListViewCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x617250, WWUI::ComboBoxCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x618D40, WWUI::ListBoxCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x61D950, WWUI::SliderCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x61E700, WWUI::GroupBoxCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x61ECA0, WWUI::InputCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x61D6D0, WWUI::ProgressCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x61C690, WWUI::ScrollBarCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x622B50, WWUI::OwnerDrawStandardWndProc); diff --git a/src/OwnerDraw/OwnerDraw.Internal.h b/src/OwnerDraw/OwnerDraw.Internal.h new file mode 100644 index 0000000000..63181ec0a9 --- /dev/null +++ b/src/OwnerDraw/OwnerDraw.Internal.h @@ -0,0 +1,110 @@ +#pragma once + +#include "OwnerDraw.h" + +#include "../Render/Functions.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +inline constexpr int DialogProcWindowLongIndex = 4; +inline constexpr int PaintStateExtraIndex = 61; +inline constexpr int SavedFontExtraIndex = 56; +inline constexpr int SavedBkModeExtraIndex = 57; +inline constexpr int SavedBkColorExtraIndex = 58; +inline constexpr int SavedTextColorExtraIndex = 59; + +WWWinData* FindOwnerDrawData(HWND hWnd); + +WNDPROC FindWindowProc(OwnerDraw::HwndProcDict& procs, HWND hWnd); + +bool IsEmpty(const WideWstring& text); + +const wchar_t* GetWideTextBuffer(const WideWstring& text); + +void DeleteSurfaceObject(Surface*& pSurface); + +void DeleteUnknownGameObject(void*& pObject); + +void InsetSurfaceRect(RectangleStruct& rect, int x, int y); + +int ConvertRGBToSurfaceColor(COLORREF color); + +COLORREF AverageColor(COLORREF first, COLORREF second); + +WORD BlendSurfacePixelTowardMasks(WORD destination, int alpha); + +void BlendFillRect(const RectangleStruct& rect, Surface* pSurface, WORD color, int alpha); + +void BlendGradientRect(const RectangleStruct& rect, Surface* pSurface, WORD color, int widthScale); + +bool DrawAlphaLine(DSurface* pSurface, Point2D start, Point2D end, WORD color, BYTE alpha); + +bool DrawAlphaBeveledRect( + DSurface* pSurface, + const RectangleStruct& rect, + bool raised, + int thickness, + BYTE leftAlpha, + BYTE topAlpha, + BYTE rightAlpha, + BYTE bottomAlpha) +; + +int DrawBeveledBorder(Surface* pSurface, const RectangleStruct& rect, int thickness, int color); + +BSurface* GetPCXSurface(const char* pFilename); + +bool BlitTiledPCX(const RectangleStruct& rect, Surface* pDestination, Surface* pSource, int offsetX, int offsetY); + +bool CopySurfacePart(Surface* pDestination, const RectangleStruct& toRect, Surface* pSource, const RectangleStruct& fromRect); + +void DrawPCXCopy(Surface* pDestination, const RectangleStruct& rect, BSurface* pPCX); + +LRESULT CallSelectedHandler(WNDPROC pSelectedWndProc, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + +void CleanupDestroyedWindow(HWND hWnd); + +void DrawScrollArrow(Surface* pSurface, const RectangleStruct& rect, bool isUp, bool pressed, bool useGreyArt); + +void EnsureScrollBarCache( + OwnerDrawDialogElement& data, + OwnerDrawDialogElement* pParentData, + const RectangleStruct& localRect, + const RectangleStruct& parentSourceRect) +; + +int BitFontHeight(BitFont* pFont); + +void CharToWideString(wchar_t* pBuffer, int capacity, const char* pText); + +void WideToCharString(char* pBuffer, int capacity, const wchar_t* pText); diff --git a/src/OwnerDraw/OwnerDraw.cpp b/src/OwnerDraw/OwnerDraw.cpp new file mode 100644 index 0000000000..9073b15b73 --- /dev/null +++ b/src/OwnerDraw/OwnerDraw.cpp @@ -0,0 +1,1643 @@ +#include "OwnerDraw.Internal.h" + +WWWinData* FindOwnerDrawData(HWND hWnd) +{ + if (!OwnerDraw::Dialogs.size()) + return nullptr; + + return OwnerDraw::Dialogs.try_get(hWnd); +} + +WNDPROC FindWindowProc(OwnerDraw::HwndProcDict& procs, HWND hWnd) +{ + if (!procs.size()) + return nullptr; + + if (const auto pProc = procs.try_get(hWnd)) + return *pProc; + + return nullptr; +} + +bool IsEmpty(const WideWstring& text) +{ + return text.GetLength() == 0; +} + +const wchar_t* GetWideTextBuffer(const WideWstring& text) +{ + return text.Buffer ? text.Buffer : L""; +} + +static WideWstring QueryTooltipText(HWND parentHwnd, HWND controlHwnd, LPARAM hitCode) +{ + OwnerDrawTooltipRequest request; + request.ControlHwnd = controlHwnd; + request.HitCode = hitCode; + + ::SendMessageA(parentHwnd, WW_GETTOOLTIPTEXT, 0, reinterpret_cast(&request)); + + return request.Text; +} + +static std::vector& ActiveWindowMessages() +{ + static std::vector messages; + return messages; +} + +static bool AllowsRecursiveMessage(UINT message) +{ + return message == WM_COMMAND + || message == WM_SYSKEYDOWN + || message == WM_SYSKEYUP + || message == WM_SYSCOMMAND + || message == WM_SYSCHAR; +} + +class WindowMessageGuardScope +{ +public: + WindowMessageGuardScope(HWND hWnd, UINT message) : + Key { message, hWnd } + { + } + + bool Enter() + { + auto& messages = ActiveWindowMessages(); + const auto it = std::find(messages.begin(), messages.end(), this->Key); + + if (it != messages.end()) + { + if (!AllowsRecursiveMessage(this->Key.Message)) + return false; + + messages.erase(it); + } + + messages.push_back(this->Key); + this->Active = true; + return true; + } + + void Release() + { + if (!this->Active) + return; + + auto& messages = ActiveWindowMessages(); + const auto it = std::find(messages.begin(), messages.end(), this->Key); + if (it != messages.end()) + messages.erase(it); + + this->Active = false; + } + + ~WindowMessageGuardScope() + { + this->Release(); + } + +private: + OwnerDrawWindowMessageKey Key; + bool Active { false }; +}; + +static HWND GetActiveWindowStackTop() +{ + const int count = OwnerDraw::ActiveWindowStackCount; + if (count <= 0 || !OwnerDraw::ActiveWindowStack) + return nullptr; + + return OwnerDraw::ActiveWindowStack[count - 1]; +} + +static void ResizeActiveWindowStack(int capacity) +{ + if (capacity < 10) + capacity = 10; + + auto pItems = static_cast(YRMemory::Allocate(sizeof(HWND) * capacity)); + std::memset(pItems, 0, sizeof(HWND) * capacity); + + const int copyCount = std::min(OwnerDraw::ActiveWindowStackCount, capacity); + if (OwnerDraw::ActiveWindowStack && copyCount > 0) + std::memcpy(pItems, OwnerDraw::ActiveWindowStack, sizeof(HWND) * copyCount); + + if (OwnerDraw::ActiveWindowStack) + YRMemory::Deallocate(OwnerDraw::ActiveWindowStack); + + OwnerDraw::ActiveWindowStack = pItems; + OwnerDraw::ActiveWindowStackCapacity = capacity; + + if (OwnerDraw::ActiveWindowStackCount > capacity) + OwnerDraw::ActiveWindowStackCount = capacity; +} + +static void EnsureActiveWindowStackCapacity(int required) +{ + if (required <= OwnerDraw::ActiveWindowStackCapacity) + return; + + int capacity = OwnerDraw::ActiveWindowStackCapacity * 2; + if (capacity < required) + capacity = required; + + ResizeActiveWindowStack(capacity); +} + +static void MaybeShrinkActiveWindowStack() +{ + const int capacity = OwnerDraw::ActiveWindowStackCapacity; + const int count = OwnerDraw::ActiveWindowStackCount; + + if (capacity <= 10 || count * 3 > capacity) + return; + + ResizeActiveWindowStack(std::max(capacity / 2, 10)); +} + +static void RemoveActiveWindow(HWND hWnd) +{ + for (int index = 0; index < OwnerDraw::ActiveWindowStackCount; ) + { + if (OwnerDraw::ActiveWindowStack[index] != hWnd) + { + ++index; + continue; + } + + const int last = OwnerDraw::ActiveWindowStackCount - 1; + if (index < last) + { + std::memmove( + &OwnerDraw::ActiveWindowStack[index], + &OwnerDraw::ActiveWindowStack[index + 1], + sizeof(HWND) * (last - index)); + } + + OwnerDraw::ActiveWindowStackCount = last; + MaybeShrinkActiveWindowStack(); + } +} + +static LRESULT BringOwnerDrawWindowToTop(HWND hWnd, WPARAM wParam, LPARAM lParam) +{ + const HWND previousTop = GetActiveWindowStackTop(); + const HWND target = wParam ? reinterpret_cast(wParam) : hWnd; + + RemoveActiveWindow(target); + + if (lParam) + { + EnsureActiveWindowStackCapacity(OwnerDraw::ActiveWindowStackCount + 1); + OwnerDraw::ActiveWindowStack[OwnerDraw::ActiveWindowStackCount++] = target; + + OwnerDraw::AboutToCallSetWindowPos = 1; + ::SetWindowPos(target, nullptr, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); + OwnerDraw::AboutToCallSetWindowPos = 0; + } + + return reinterpret_cast(previousTop); +} + +static bool IsActiveWindowMessageBlocked(HWND hWnd, UINT message) +{ + if (OwnerDraw::ActiveWindowStackCount <= 0) + return false; + + const HWND activeTop = GetActiveWindowStackTop(); + + bool belongsToActiveWindow = ::GetParent(hWnd) == nullptr; + if (::GetWindowLongA(hWnd, GWL_ID) <= 0) + belongsToActiveWindow = true; + + for (HWND walker = hWnd; walker; walker = ::GetParent(walker)) + { + if (walker == activeTop) + { + belongsToActiveWindow = true; + break; + } + } + + bool allowedOutsideActiveWindow = false; + if (message < WM_MOUSEFIRST || message > WM_MBUTTONDBLCLK) + allowedOutsideActiveWindow = true; + if (message >= WM_NCMOUSEMOVE && message <= WM_NCMBUTTONDBLCLK) + allowedOutsideActiveWindow = false; + if (message >= WM_KEYFIRST && message <= WM_KEYLAST) + allowedOutsideActiveWindow = false; + if (message == WM_SYSKEYUP || message == WM_SYSKEYDOWN || message == WM_SYSCOMMAND || message == WM_SYSCHAR) + allowedOutsideActiveWindow = true; + if (message == WW_UNKNOWN49B || message == WM_TIMER || message == WW_LB_GETCELLTEXT) + allowedOutsideActiveWindow = false; + + return !belongsToActiveWindow && !allowedOutsideActiveWindow; +} + +static bool HandleWindowPosChanging(HWND hWnd, LPARAM lParam, LRESULT& result) +{ + if (OwnerDraw::AboutToCallSetWindowPos == 1 || OwnerDraw::ActiveWindowStackCount <= 0) + return false; + + int index = -1; + for (int i = 0; i < OwnerDraw::ActiveWindowStackCount; ++i) + { + if (OwnerDraw::ActiveWindowStack[i] == hWnd) + { + index = i; + break; + } + } + + if (index < 0) + return false; + + auto pPosition = reinterpret_cast(lParam); + if (!pPosition) + { + result = 0; + return true; + } + + if (index == OwnerDraw::ActiveWindowStackCount - 1 + && pPosition->hwndInsertAfter + && !(pPosition->flags & SWP_NOZORDER)) + { + pPosition->hwndInsertAfter = nullptr; + } + else + { + pPosition->flags |= SWP_NOZORDER | SWP_NOOWNERZORDER; + } + + ::InvalidateRect(hWnd, nullptr, FALSE); + result = 0; + return true; +} + +using ScalarDeletingDestructor = void* (__thiscall*)(Surface*, unsigned int); +using GenericDeletingDestructor = void* (__thiscall*)(void*, unsigned int); + +void DeleteSurfaceObject(Surface*& pSurface) +{ + if (!pSurface) + return; + + const auto pDestructor = (*reinterpret_cast(pSurface))[0]; + pDestructor(pSurface, 1); + pSurface = nullptr; +} + +void DeleteUnknownGameObject(void*& pObject) +{ + if (!pObject) + return; + + const auto pDestructor = (*reinterpret_cast(pObject))[0]; + pDestructor(pObject, 1); + pObject = nullptr; +} + +static void RestoreAndClearTooltipIfNeeded(HWND hWnd, UINT message) +{ + auto& tooltip = OwnerDraw::TooltipBlitState; + + if (hWnd != tooltip.OwnerHwnd || !tooltip.Active) + return; + + if (message != WM_NCDESTROY && message != WM_SHOWWINDOW && message != WM_KILLFOCUS) + return; + + const bool restoredBackground = !tooltip.BackgroundRestored && tooltip.BackingSurface; + if (!tooltip.BackgroundRestored) + { + OwnerDraw::RestoreTooltipBackground(); + if (restoredBackground) + RenderDX::UpdateScreen(DSurface::Primary); + } + + DeleteSurfaceObject(tooltip.BackingSurface); + tooltip.Active = 0; + tooltip.BackgroundRestored = 0; +} + +void InsetSurfaceRect(RectangleStruct& rect, int x, int y) +{ + rect.X += x; + rect.Y += y; + rect.Width -= 2 * x; + rect.Height -= 2 * y; +} + +static void CopyAlternateToPrimary(const RectangleStruct& destRect, const RectangleStruct& sourceRect) +{ + if (!DSurface::Primary || !DSurface::Alternate) + return; + + DSurface::Primary->Lock(0, 0); + DSurface::Alternate->Lock(0, 0); + DSurface::Primary->CopyFromPart( + const_cast(&destRect), + DSurface::Alternate, + const_cast(&sourceRect), + false, + true); + DSurface::Alternate->Unlock(); + DSurface::Primary->Unlock(); +} + +int ConvertRGBToSurfaceColor(COLORREF color) +{ + if (color == static_cast(-1)) + return -1; + + return Drawing::RGB_To_Int(GetRValue(color), GetGValue(color), GetBValue(color)); +} + +COLORREF AverageColor(COLORREF first, COLORREF second) +{ + return RGB( + (GetRValue(first) + GetRValue(second)) / 2, + (GetGValue(first) + GetGValue(second)) / 2, + (GetBValue(first) + GetBValue(second)) / 2); +} + +static WORD BlendSurfacePixel(WORD destination, WORD source, int alpha) +{ + const int inverseAlpha = 255 - alpha; + const WORD redMask = OwnerDraw::ColorShiftRed; + const WORD greenMask = OwnerDraw::ColorShiftGreen; + const WORD blueMask = OwnerDraw::ColorShiftBlue; + + return static_cast( + ((((source & redMask) * alpha + (destination & redMask) * inverseAlpha) >> 8) & redMask) + | ((((source & greenMask) * alpha + (destination & greenMask) * inverseAlpha) >> 8) & greenMask) + | ((((source & blueMask) * alpha + (destination & blueMask) * inverseAlpha) >> 8) & blueMask)); +} + +WORD BlendSurfacePixelTowardMasks(WORD destination, int alpha) +{ + const int inverseAlpha = 255 - alpha; + const WORD redMask = OwnerDraw::ColorShiftRed; + const WORD greenMask = OwnerDraw::ColorShiftGreen; + const WORD blueMask = OwnerDraw::ColorShiftBlue; + + return static_cast( + (((redMask * alpha + (destination & redMask) * inverseAlpha) >> 8) & redMask) + | (((greenMask * alpha + (destination & greenMask) * inverseAlpha) >> 8) & greenMask) + | (((blueMask * alpha + (destination & blueMask) * inverseAlpha) >> 8) & blueMask)); +} + +void BlendFillRect(const RectangleStruct& rect, Surface* pSurface, WORD color, int alpha) +{ + if (!pSurface || alpha <= 0 || rect.Width <= 0 || rect.Height <= 0) + return; + + auto pPixels = static_cast(pSurface->Lock(0, 0)); + if (!pPixels) + return; + + const int pitch = pSurface->GetPitch() / 2; + const int left = std::max(rect.X, 0); + const int top = std::max(rect.Y, 0); + const int right = std::min(rect.X + rect.Width, pSurface->GetWidth()); + const int bottom = std::min(rect.Y + rect.Height, pSurface->GetHeight()); + + for (int y = top; y < bottom; ++y) + { + auto pLine = &pPixels[y * pitch + left]; + for (int x = left; x < right; ++x) + { + *pLine = BlendSurfacePixel(*pLine, color, alpha); + ++pLine; + } + } + + pSurface->Unlock(); +} + +void BlendGradientRect(const RectangleStruct& rect, Surface* pSurface, WORD color, int widthScale) +{ + if (!pSurface || rect.Width <= 0 || rect.Height <= 0) + return; + + int fillWidth = static_cast((static_cast(rect.Width) * widthScale) >> 16); + if (fillWidth < 0) + return; + + if (!fillWidth) + fillWidth = 1; + + auto pPixels = static_cast(pSurface->Lock(0, 0)); + if (!pPixels) + return; + + const int pitch = pSurface->GetPitch() / 2; + const int quarterHeight = rect.Height / 4; + bool useGradient = true; + + for (int y = 0; y < rect.Height; ++y) + { + if (y == 3 * quarterHeight) + useGradient = true; + + if (y == quarterHeight) + useGradient = false; + + auto pLine = &pPixels[(rect.Y + y) * pitch + rect.X]; + int alphaNumerator = 255; + + for (int x = 0; x < fillWidth; ++x) + { + if (useGradient) + { + const int alpha = (alphaNumerator / rect.Width) & 0xFF; + *pLine = BlendSurfacePixel(*pLine, color, alpha); + } + else + { + *pLine = color; + } + + ++pLine; + alphaNumerator += 255; + } + } + + pSurface->Unlock(); +} + +bool DrawAlphaLine(DSurface* pSurface, Point2D start, Point2D end, WORD color, BYTE alpha) +{ + if (!pSurface) + return false; + + if (start.Y == end.Y) + { + if (start.X > end.X) + std::swap(start.X, end.X); + + auto pPixels = static_cast(pSurface->Lock(start.X, start.Y)); + if (!pPixels) + return false; + + for (int x = start.X; x <= end.X; ++x) + { + *pPixels = BlendSurfacePixel(*pPixels, color, alpha); + ++pPixels; + } + + pSurface->Unlock(); + return true; + } + + if (start.X == end.X) + { + const int step = start.Y <= end.Y ? pSurface->GetPitch() : -pSurface->GetPitch(); + const int count = std::abs(end.Y - start.Y) + 1; + auto pPixelBytes = static_cast(pSurface->Lock(start.X, start.Y)); + if (!pPixelBytes) + return false; + + for (int i = 0; i < count; ++i) + { + auto pPixel = reinterpret_cast(pPixelBytes); + *pPixel = BlendSurfacePixel(*pPixel, color, alpha); + pPixelBytes += step; + } + + pSurface->Unlock(); + return true; + } + + return false; +} + +bool DrawAlphaBeveledRect( + DSurface* pSurface, + const RectangleStruct& rect, + bool raised, + int thickness, + BYTE leftAlpha, + BYTE topAlpha, + BYTE rightAlpha, + BYTE bottomAlpha) +{ + if (!pSurface || thickness <= 0) + return false; + + const WORD topLeftColor = raised ? 0xFFFF : 0; + const WORD bottomRightColor = raised ? 0 : 0xFFFF; + bool result = raised; + + for (int layer = 0; layer < thickness; ++layer) + { + Point2D start { rect.X + layer, rect.Y + layer }; + Point2D end { rect.X + rect.Width - layer - 2, rect.Y + layer }; + result = DrawAlphaLine(pSurface, start, end, topLeftColor, topAlpha); + + start = { rect.X + layer, rect.Y + rect.Height - layer - 1 }; + end = { rect.X + rect.Width - layer - 1, start.Y }; + result = DrawAlphaLine(pSurface, start, end, bottomRightColor, bottomAlpha); + + start = { rect.X + layer, rect.Y + layer + 1 }; + end = { start.X, rect.Y + rect.Height - layer - 1 }; + result = DrawAlphaLine(pSurface, start, end, topLeftColor, leftAlpha); + + start = { rect.X + rect.Width - layer - 1, rect.Y + layer }; + end = { start.X, rect.Y + rect.Height - layer - 2 }; + result = DrawAlphaLine(pSurface, start, end, bottomRightColor, rightAlpha); + } + + return result; +} + +int DrawBeveledBorder(Surface* pSurface, const RectangleStruct& rect, int thickness, int color) +{ + if (!pSurface || thickness <= 0) + return thickness - 1; + + int lineColor = color; + if (lineColor == -1 && OwnerDraw::DefaultBorderColor != static_cast(-1)) + lineColor = ConvertRGBToSurfaceColor(OwnerDraw::DefaultBorderColor); + + const int lightColor = ConvertRGBToSurfaceColor(OwnerDraw::BevelLightColor); + const int shadowColor = ConvertRGBToSurfaceColor(OwnerDraw::BevelShadowColor); + const int averageColor = ConvertRGBToSurfaceColor(AverageColor(OwnerDraw::BevelLightColor, OwnerDraw::BevelShadowColor)); + + const int leftBase = rect.X - thickness; + const int topBase = rect.Y - thickness; + const int rightBase = leftBase + rect.Width + 2 * thickness - 1; + const int bottomBase = topBase + rect.Height + 2 * thickness - 1; + + for (int layer = 0; layer < thickness; ++layer) + { + int topLeftColor = lineColor; + int bottomRightColor = lineColor; + + if (thickness == 2) + { + topLeftColor = layer == 0 ? lightColor : shadowColor; + bottomRightColor = layer == 0 ? shadowColor : lightColor; + } + + const int left = leftBase + layer; + const int top = topBase + layer; + const int right = rightBase - layer; + const int bottom = bottomBase - layer; + + Point2D start { left, top }; + Point2D end { right - 1, top }; + pSurface->DrawLine(&start, &end, topLeftColor); + + start = { left, top + 1 }; + end = { left, bottom }; + pSurface->DrawLine(&start, &end, topLeftColor); + + start = { left, bottom }; + end = { right, bottom }; + pSurface->DrawLine(&start, &end, bottomRightColor); + + start = { right, top }; + end = { right, bottom - 1 }; + pSurface->DrawLine(&start, &end, bottomRightColor); + + if (thickness == 2) + { + Point2D corner { right, top }; + pSurface->SetPixel(&corner, averageColor); + corner = { left, bottom }; + pSurface->SetPixel(&corner, averageColor); + } + } + + return 0; +} + +BSurface* GetPCXSurface(const char* pFilename) +{ + return PCX::Instance.GetSurface(pFilename, nullptr); +} + +bool BlitTiledPCX(const RectangleStruct& rect, Surface* pDestination, Surface* pSource, int offsetX, int offsetY) +{ + if (!pDestination || !pSource || rect.Width <= 0 || rect.Height <= 0) + return false; + + auto pDestPixels = static_cast(pDestination->Lock(0, 0)); + if (!pDestPixels) + return false; + + auto pSourcePixels = static_cast(pSource->Lock(0, 0)); + if (!pSourcePixels) + { + pDestination->Unlock(); + return false; + } + + const int destPitch = pDestination->GetPitch() / 2; + const int sourcePitch = pSource->GetPitch() / 2; + const int sourceWidth = pSource->GetWidth(); + const int sourceHeight = pSource->GetHeight(); + const int sourceStartX = offsetX + std::max((sourceWidth - rect.Width) / 2, 0); + int sourceY = offsetY + std::max((sourceHeight - rect.Height) / 2, 0); + + for (int y = 0; y < rect.Height; ++y) + { + int sourceX = sourceStartX; + auto pDestLine = &pDestPixels[rect.X + destPitch * (rect.Y + y)]; + for (int x = 0; x < rect.Width; ++x) + { + *pDestLine++ = pSourcePixels[sourcePitch * (sourceY % sourceHeight) + (sourceX % sourceWidth)]; + ++sourceX; + } + + ++sourceY; + } + + pSource->Unlock(); + pDestination->Unlock(); + return true; +} + +bool CopySurfacePart(Surface* pDestination, const RectangleStruct& toRect, Surface* pSource, const RectangleStruct& fromRect) +{ + if (!pDestination || !pSource) + return false; + + auto dest = toRect; + auto source = fromRect; + return pDestination->CopyFromPart(&dest, pSource, &source, false, true); +} + +void DrawPCXCopy(Surface* pDestination, const RectangleStruct& rect, BSurface* pPCX) +{ + if (!pPCX) + return; + + RectangleStruct sourceRect { 0, 0, pPCX->GetWidth(), pPCX->GetHeight() }; + RectangleStruct destRect { rect.X, rect.Y, pPCX->GetWidth(), pPCX->GetHeight() }; + CopySurfacePart(pDestination, destRect, pPCX, sourceRect); +} + +static void NotifyChildren(HWND hWnd, UINT message) +{ + OwnerDrawHWNDVector children {}; + ::EnumChildWindows(hWnd, OwnerDraw::CollectChildHwndProc, reinterpret_cast(&children)); + + for (int i = 0; i < children.Count; ++i) + ::SendMessageA(children.Items[i], message, 0, 0); + + if (children.Items) + YRMemory::Deallocate(children.Items); +} + +static void RepaintChildWindows(HWND hWnd, HWND ownerHwnd, const RECT& ownerDrawRect) +{ + OwnerDrawHWNDVector children {}; + ::EnumChildWindows(hWnd, OwnerDraw::CollectChildHwndProc, reinterpret_cast(&children)); + + HWND comboDropHwnd = nullptr; + + for (int i = 0; i < children.Count; ++i) + { + const HWND childHwnd = children.Items[i]; + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, childHwnd); + + if (pOriginalWndProc == OwnerDraw::ComboDropWindowHandler) + { + comboDropHwnd = childHwnd; + continue; + } + + ::InvalidateRect(childHwnd, nullptr, FALSE); + ::UpdateWindow(childHwnd); + } + + if (comboDropHwnd) + { + ::InvalidateRect(comboDropHwnd, nullptr, FALSE); + ::UpdateWindow(comboDropHwnd); + } + else if (OwnerDraw::ComboDropActiveDropHwnd && OwnerDraw::ComboDropActiveParentHwnd == ownerHwnd) + { + RECT dropRect {}; + OwnerDraw::GetRectangle(OwnerDraw::ComboDropActiveDropHwnd, &dropRect); + + RECT intersect {}; + if (::IntersectRect(&intersect, &ownerDrawRect, &dropRect)) + { + ::InvalidateRect(OwnerDraw::ComboDropActiveDropHwnd, nullptr, FALSE); + ::UpdateWindow(OwnerDraw::ComboDropActiveDropHwnd); + } + } + + if (children.Items) + YRMemory::Deallocate(children.Items); +} + +static void RepaintOverlappingPreviousSibling(HWND hWnd, HWND ownerHwnd, const RECT& ownerDrawRect, int paintCopyMode) +{ + if (paintCopyMode <= 0 || (hWnd != ownerHwnd && OwnerDraw::PaintDepth != 1)) + return; + + for (HWND sibling = ownerHwnd; sibling; ) + { + sibling = ::GetWindow(sibling, GW_HWNDPREV); + if (!sibling) + break; + + if (!::GetWindowLongA(sibling, DialogProcWindowLongIndex)) + continue; + + RECT siblingRect {}; + OwnerDraw::GetRectangle(sibling, &siblingRect); + + RECT intersect {}; + if (::IntersectRect(&intersect, &ownerDrawRect, &siblingRect)) + { + ::InvalidateRect(sibling, nullptr, FALSE); + ::UpdateWindow(sibling); + break; + } + } +} + +static void RestoreTooltipBackgroundForPaint(const RECT& ownerDrawRect, bool& redrawTooltip) +{ + auto& tooltip = OwnerDraw::TooltipBlitState; + + const RECT tooltipRect + { + tooltip.Rect.X, + tooltip.Rect.Y, + tooltip.Rect.X + tooltip.Rect.Width + 1, + tooltip.Rect.Y + tooltip.Rect.Height + 1 + }; + + RECT intersect {}; + if (OwnerDraw::PaintDepth != 1 + || !::IntersectRect(&intersect, &ownerDrawRect, &tooltipRect) + || !tooltip.Active + || tooltip.BackgroundRestored + || !tooltip.BackingSurface + || !DSurface::Primary) + { + return; + } + + RectangleStruct targetRect { tooltip.Rect.X, tooltip.Rect.Y, tooltip.Rect.Width, tooltip.Rect.Height }; + RectangleStruct sourceRect { 0, 0, tooltip.Rect.Width, tooltip.Rect.Height }; + DSurface::Primary->CopyFromPart(&targetRect, tooltip.BackingSurface, &sourceRect, false, true); + RenderDX::UpdateScreen(DSurface::Primary); + tooltip.BackgroundRestored = 1; + redrawTooltip = true; +} + +static void AccumulatePaintBounds(const RECT& ownerDrawRect) +{ + if (OwnerDraw::PaintLeft >= ownerDrawRect.left) + OwnerDraw::PaintLeft = ownerDrawRect.left; + if (OwnerDraw::PaintTop >= ownerDrawRect.top) + OwnerDraw::PaintTop = ownerDrawRect.top; + if (OwnerDraw::PaintRight <= ownerDrawRect.right) + OwnerDraw::PaintRight = ownerDrawRect.right; + if (OwnerDraw::PaintBottom <= ownerDrawRect.bottom) + OwnerDraw::PaintBottom = ownerDrawRect.bottom; +} + +struct PaintRoot +{ + HWND ProbeHwnd {}; + HWND OwnerHwnd {}; + OwnerDrawDialogElement* Data {}; +}; + +static PaintRoot FindPaintRoot(HWND hWnd) +{ + HWND ownerHwnd = hWnd; + HWND probeHwnd = hWnd; + + while (probeHwnd) + { + ownerHwnd = probeHwnd; + if (::GetWindowLongA(probeHwnd, DialogProcWindowLongIndex)) + break; + + probeHwnd = ::GetParent(probeHwnd); + } + + return PaintRoot + { + probeHwnd, + ownerHwnd, + probeHwnd ? FindOwnerDrawData(ownerHwnd) : nullptr + }; +} + +LRESULT CallSelectedHandler(WNDPROC pSelectedWndProc, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (!pSelectedWndProc) + return 0; + + return ::CallWindowProcA(pSelectedWndProc, hWnd, message, wParam, lParam); +} + +static LRESULT DispatchPaintMessage( + HWND hWnd, + UINT message, + WPARAM wParam, + LPARAM lParam, + WNDPROC pSelectedWndProc, + const RECT& ownerDrawRect, + int& paintCopyMode, + bool& redrawTooltip) +{ + auto pData = FindOwnerDrawData(hWnd); + if (!pData) + return CallSelectedHandler(pSelectedWndProc, hWnd, message, wParam, lParam); + + if (pData->NeedsControlImage) + { + ::ValidateRect(hWnd, nullptr); + return CallSelectedHandler(pSelectedWndProc, hWnd, WW_SCROLLBAR_UPDATETHUMB, wParam, lParam); + } + + RestoreTooltipBackgroundForPaint(ownerDrawRect, redrawTooltip); + AccumulatePaintBounds(ownerDrawRect); + + const auto root = FindPaintRoot(hWnd); + int rootPaintState = 0; + + if (root.ProbeHwnd == hWnd) + { + if (!root.Data) + { + ::ValidateRect(hWnd, nullptr); + return 1; + } + + rootPaintState = root.Data->Extra[PaintStateExtraIndex] < 1 ? 1 : root.Data->Extra[PaintStateExtraIndex]; + } + else + { + if (!root.Data) + { + ::ValidateRect(hWnd, nullptr); + RepaintOverlappingPreviousSibling(hWnd, root.OwnerHwnd, ownerDrawRect, paintCopyMode); + return 1; + } + + rootPaintState = root.Data->Extra[PaintStateExtraIndex]; + } + + paintCopyMode = rootPaintState; + LRESULT result = 1; + + if (rootPaintState < 1) + { + ::ValidateRect(hWnd, nullptr); + } + else + { + result = CallSelectedHandler(pSelectedWndProc, hWnd, message, wParam, lParam); + if (root.Data) + root.Data->Extra[PaintStateExtraIndex] = rootPaintState; + + RepaintChildWindows(hWnd, root.OwnerHwnd, ownerDrawRect); + } + + RepaintOverlappingPreviousSibling(hWnd, root.OwnerHwnd, ownerDrawRect, rootPaintState); + return result; +} + +static void FinishPaint(HWND hWnd, OwnerDrawDialogElement* pData, int paintCopyMode, int windowOffsetX, int windowOffsetY) +{ + if (!pData) + return; + + if (::GetWindowLongA(hWnd, DialogProcWindowLongIndex) && OwnerDraw::PaintDepth > 1) + pData->Extra[PaintStateExtraIndex] = 2; + + if (--OwnerDraw::PaintDepth != 0) + return; + + if (!pData->NeedsControlImage && paintCopyMode >= 1 && !OwnerDraw::IsWebBrowserVisible()) + { + const int paintLeft = OwnerDraw::PaintLeft; + const int paintTop = OwnerDraw::PaintTop; + const int paintWidth = OwnerDraw::PaintRight - OwnerDraw::PaintLeft; + const int paintHeight = OwnerDraw::PaintBottom - OwnerDraw::PaintTop; + + if (paintWidth > 0 && paintHeight > 0) + { + RectangleStruct sourceRect { paintLeft, paintTop, paintWidth, paintHeight }; + RectangleStruct destRect { paintLeft + windowOffsetX, paintTop + windowOffsetY, paintWidth, paintHeight }; + + if (pData->Extra[PaintStateExtraIndex] == 1) + { + pData->Extra[PaintStateExtraIndex] = 2; + + if (::GetWindowLongA(hWnd, DialogProcWindowLongIndex) && OwnerDraw::RunOpenAnimationIfNeeded(hWnd)) + pData->Extra[PaintStateExtraIndex] = 3; + + CopyAlternateToPrimary(destRect, sourceRect); + NotifyChildren(hWnd, WW_EDIT_RESTOREFOCUS); + } + else + { + char className[0x80] {}; + ::GetClassNameA(hWnd, className, sizeof(className)); + + if (!std::strcmp(className, "ComboBox")) + { + InsetSurfaceRect(sourceRect, -1, -1); + InsetSurfaceRect(destRect, -1, -1); + } + + CopyAlternateToPrimary(destRect, sourceRect); + } + } + } + + OwnerDraw::PaintRight = 0; + OwnerDraw::PaintLeft = 0xFFFFFF; + OwnerDraw::PaintTop = 0xFFFFFF; + OwnerDraw::PaintBottom = 0; +} + +static void ReleaseElementText(OwnerDrawDialogElement& data) +{ + if (!data.TextBuffer) + return; + + YRMemory::Deallocate(data.TextBuffer); + data.TextBuffer = nullptr; +} + +static void SetElementTextA(OwnerDrawDialogElement& data, const char* pText) +{ + ReleaseElementText(data); + + if (pText && *pText) + { + const auto length = std::strlen(pText); + data.TextBuffer = static_cast(YRMemory::Allocate(sizeof(wchar_t) * (length + 1))); + std::swprintf(data.TextBuffer, length + 1, L"%hs", pText); + } + + data.HasText = 1; +} + +static bool SetElementTextW(OwnerDrawDialogElement& data, const wchar_t* pText) +{ + const bool changed = (!data.TextBuffer && pText) + || (data.TextBuffer && !pText) + || (data.TextBuffer && pText && std::wcscmp(data.TextBuffer, pText)); + + ReleaseElementText(data); + + if (pText && *pText) + { + const auto length = std::wcslen(pText); + data.TextBuffer = static_cast(YRMemory::Allocate(sizeof(wchar_t) * (length + 1))); + std::wcscpy(data.TextBuffer, pText); + } + + data.HasText = 0; + return changed; +} + +static void CopyTextA(const OwnerDrawDialogElement& data, WPARAM length, LPARAM lParam) +{ + if (!lParam) + return; + + auto pBuffer = reinterpret_cast(lParam); + *pBuffer = '\0'; + + if (data.TextBuffer) + { + OwnerDraw::WideToCharString(pBuffer, data.TextBuffer, length); + if (length) + pBuffer[length - 1] = '\0'; + } +} + +static void CopyTextW(const OwnerDrawDialogElement& data, WPARAM length, LPARAM lParam) +{ + if (!lParam) + return; + + auto pBuffer = reinterpret_cast(lParam); + *pBuffer = L'\0'; + + if (data.TextBuffer) + { + std::wcsncpy(pBuffer, data.TextBuffer, length); + if (length) + pBuffer[length - 1] = L'\0'; + } +} + +static bool IsNativeTextMessage(UINT message, LRESULT& result) +{ + switch (message) + { + case WM_SETTEXT: + result = 0; + return true; + + case WM_GETTEXT: + case LB_GETTEXT: + result = 0; + return true; + + case CB_ADDSTRING: + case CB_FINDSTRING: + case CB_FINDSTRINGEXACT: + case CB_SELECTSTRING: + case CB_INSERTSTRING: + case CB_GETLBTEXT: + case LB_ADDSTRING: + case LB_FINDSTRING: + case LB_FINDSTRINGEXACT: + case LB_SELECTSTRING: + case LB_INSERTSTRING: + result = -1; + return true; + + default: + return false; + } +} + +static bool HandleElementTextMessage( + OwnerDrawDialogElement& data, + HWND hWnd, + UINT message, + WPARAM wParam, + LPARAM lParam, + LRESULT& result, + bool& callSelectedHandler) +{ + switch (message) + { + case WW_SETHASTEXT: + result = data.HasText == 0; + callSelectedHandler = false; + return true; + + case WW_GETTEXTA: + CopyTextA(data, wParam, lParam); + return true; + + case WW_GETTEXTW: + CopyTextW(data, wParam, lParam); + return true; + + case WW_SETUNKNOWNPROP50: + data.AsNewEdit().RejectChars() = reinterpret_cast(lParam); + return true; + + case WW_SETTEXTA: + SetElementTextA(data, reinterpret_cast(lParam)); + return true; + + case WW_SETUNKNOWNPROP30: + result = data.AsNewEdit().AsciiOnly(); + data.AsNewEdit().AsciiOnly() = static_cast(wParam); + callSelectedHandler = false; + return true; + + case WW_SETTEXTW: + { + const bool changed = SetElementTextW(data, reinterpret_cast(lParam)); + if (changed && data.AsStatic().DrawMode() == WWUIStaticDrawMode::TypewriterText && data.AsStatic().AnimationRunning()) + { + ::KillTimer(hWnd, 0); + data.AsStatic().AnimationRunning() = false; + ::SendMessageA(hWnd, WW_STATIC_REVEALTEXTS, 0, 0); + } + return true; + } + + default: + return false; + } +} + +static void UpdateTooltipTextOnMouseMove(HWND hWnd, LPARAM lParam) +{ + const HWND parentHwnd = ::GetParent(hWnd); + const HWND tooltipHwnd = parentHwnd ? ::GetDlgItem(parentHwnd, OwnerDraw::TooltipText) : nullptr; + if (!tooltipHwnd) + return; + + WideWstring tooltipText; + + const LPARAM pointParam = MAKELPARAM(LOWORD(lParam), HIWORD(lParam)); + const auto hitCode = ::SendMessageA(hWnd, WW_QUERYTOOLTIPHIT, 0, pointParam); + tooltipText = QueryTooltipText(parentHwnd, hWnd, hitCode); + + if (IsEmpty(tooltipText)) + { + tooltipText = QueryTooltipText(parentHwnd, hWnd, -1); + + if (IsEmpty(tooltipText)) + { + if (const auto label = OwnerDraw::GetTooltipStringLabel(parentHwnd, hWnd)) + { + tooltipText = StringTable::LoadString( + label, + nullptr, + "D:\\ra2mdpost\\ownrdraw.cpp", + 1957); + } + else + { + tooltipText = L""; + } + } + } + + ::SendMessageA(tooltipHwnd, WW_SETTEXTW, 0, reinterpret_cast(GetWideTextBuffer(tooltipText))); +} + +void CleanupDestroyedWindow(HWND hWnd) +{ + if (auto pData = FindOwnerDrawData(hWnd)) + { + if (pData->CacheSurface) + { + DeleteSurfaceObject(pData->CacheSurface); + --OwnerDraw::CachedSurfaceCount; + } + } + + OwnerDraw::DialogProcs.erase(hWnd); + OwnerDraw::Dialogs.erase(hWnd); + OwnerDraw::SubclassProcs.erase(hWnd); + + const auto pUserData = reinterpret_cast(::GetWindowLongA(hWnd, GWL_USERDATA)); + if (pUserData) + YRMemory::Deallocate(pUserData); + + ::SetWindowLongA(hWnd, GWL_USERDATA, 0); + SessionIpb::UnregisterHwnd(hWnd); +} + +static void FinishDialogInitialization(HWND hWnd) +{ + if (!SessionClass::Instance.CurrentlyInGame) + OwnerDraw::LoadNotInGameResources(hWnd); + + OwnerDraw::UpdateTopPanelAnimationFlag(hWnd); + OwnerDraw::UpdateButtonAnimationFlag(hWnd); + OwnerDraw::UpdateMainScreenAnimationFlag(hWnd); + OwnerDraw::UpdateFlagD8FromDialogID(hWnd); + + ::EnumChildWindows(hWnd, OwnerDraw::InitCompactDialogControlsProc, 0); + + if (OwnerDraw::TrySetDialogLayoutBand1(hWnd)) + { + auto baseSize = OwnerDraw::BaseLayoutSize; + OwnerDraw::UpdateControlPosition(hWnd, &baseSize); + } + else + { + OwnerDraw::TrySetDialogLayoutBand2(hWnd); + } + + ::EnumChildWindows(hWnd, OwnerDraw::ClassifyLayoutBand, 0); + ::EnumChildWindows(hWnd, OwnerDraw::ResetControlDrawModeAndTimerProc, 0); + UI::CenterWindow(hWnd); + ::SetFocus(hWnd); +} + +static void RegisterDialogControls(HWND hWnd, int dialogID) +{ + UI::RegisterComboDropAndNewEditClasses(); + OwnerDraw::CurrentDialogHwnd = hWnd; + + ::EnumChildWindows(hWnd, OwnerDraw::SetNeedsControlImage, 1); + OwnerDraw::SetNeedsControlImage(hWnd, 1); + + ::EnumChildWindows(hWnd, OwnerDraw::ReplaceEditWithListboxProc, 1); + + ::EnumChildWindows(hWnd, OwnerDraw::RegisterChildControlProc, 0); + OwnerDraw::RegisterChildControlProc(hWnd, 0); + + ::EnumChildWindows(hWnd, OwnerDraw::SetNeedsControlImage, 0); + OwnerDraw::SetNeedsControlImage(hWnd, 0); + + OwnerDraw::ScaleControls(hWnd); + OwnerDraw::SetDialogID(hWnd, dialogID); +} + +static LRESULT HandleInitDialog(HWND hWnd, LPARAM lParam) +{ + ++Unsorted::WSDialogCount; + + if (lParam) + { + const int dialogID = *reinterpret_cast(lParam); + RegisterDialogControls(hWnd, dialogID); + } + else + { + OwnerDraw::PrepareDialogChildControls(hWnd, 0); + OwnerDraw::ScaleControls(hWnd); + + if (auto pData = FindOwnerDrawData(hWnd)) + pData->DialogID = 0; + } + + FinishDialogInitialization(hWnd); + return 0; +} + +static LRESULT HandlePaint(HWND hWnd) +{ + auto pData = FindOwnerDrawData(hWnd); + if (!pData) + return 0; + + if (pData->SkipDraw) + { + ::ValidateRect(hWnd, nullptr); + return 1; + } + + OwnerDraw::Paint(hWnd); + + pData = FindOwnerDrawData(hWnd); + if (pData && pData->HasFadeAnimation) + { + if (const auto movieHwnd = ::GetDlgItem(hWnd, OwnerDraw::TransitionMovie)) + ::SendMessageA(movieHwnd, WW_STATIC_DETACHMOVIE, 0, 0); + + OwnerDraw::DrawCampaignMenuTransition(hWnd, false); + pData->HasFadeAnimation = false; + } + + ::ValidateRect(hWnd, nullptr); + return 0; +} + +static LRESULT HandleTooltipRefresh(HWND hWnd, LPARAM lParam) +{ + const auto tooltipHwnd = ::GetDlgItem(hWnd, OwnerDraw::TooltipText); + if (!tooltipHwnd) + return 0; + + const int screenX = LOWORD(lParam); + const int screenY = HIWORD(lParam); + + RECT dialogRect; + ::GetWindowRect(hWnd, &dialogRect); + + const POINT clientPoint + { + screenX - dialogRect.left, + screenY - dialogRect.top + }; + + WideWstring tooltipText; + + if (const auto controlHwnd = ::ChildWindowFromPointEx(hWnd, clientPoint, CWP_SKIPINVISIBLE)) + { + const LPARAM pointParam = MAKELPARAM(LOWORD(lParam), HIWORD(lParam)); + const auto hitCode = ::SendMessageA(controlHwnd, WW_QUERYTOOLTIPHIT, 0, pointParam); + + tooltipText = QueryTooltipText(hWnd, controlHwnd, hitCode); + + if (IsEmpty(tooltipText)) + { + tooltipText = QueryTooltipText(hWnd, controlHwnd, -1); + + if (IsEmpty(tooltipText)) + { + if (const auto label = OwnerDraw::GetTooltipStringLabel(hWnd, controlHwnd)) + { + tooltipText = StringTable::LoadString( + label, + nullptr, + "D:\\ra2mdpost\\ownrdraw.cpp", + 1957); + } + else + { + tooltipText = L""; + } + } + } + } + + ::SendMessageA(tooltipHwnd, WW_SETTEXTW, 0, reinterpret_cast(GetWideTextBuffer(tooltipText))); + return 0; +} + +LRESULT __fastcall WWUI::OwnerDrawStandardWndProc(HWND hWnd, UINT message, WPARAM, LPARAM lParam) +{ + if (message <= WM_NCHITTEST) + { + switch (message) + { + case WM_DESTROY: + UI::RemoveModelessDialog(hWnd); + --Unsorted::WSDialogCount; + ::SetFocus(Game::hWnd); + return 0; + + case WM_PAINT: + return HandlePaint(hWnd); + + case WM_ERASEBKGND: + return 1; + + case WM_DRAWITEM: + OwnerDraw::DrawItem(reinterpret_cast(lParam)); + return 1; + + case WM_NCHITTEST: + return HandleTooltipRefresh(hWnd, lParam); + + default: + return 0; + } + } + + if (message == WM_INITDIALOG) + return HandleInitDialog(hWnd, lParam); + + if (message >= WM_CTLCOLORMSGBOX && message <= WM_CTLCOLORSTATIC) + return reinterpret_cast(::GetStockObject(BLACK_BRUSH)); + + if (message == WW_INITDIALOG) + { + ::SendMessageA(hWnd, WW_BRINGTOTOP, reinterpret_cast(hWnd), 1); + return 0; + } + + if (message == WW_TRANSITION_COMPLETE) + ::EnumChildWindows(hWnd, OwnerDraw::SendTransitionCompleteToCustomTextChildProc, 0); + + return 0; +} + +LRESULT CALLBACK WWUI::OwnerDrawWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_SETCURSOR) + return 1; + + const bool isPaintMessage = message == WM_PAINT; + const auto updatePrimaryAfterPaint = [isPaintMessage]() + { + if (isPaintMessage) + RenderDX::UpdateScreen(DSurface::Primary); + }; + + if (OwnerDraw::ServiceIMEMessage(hWnd, message, wParam, lParam)) + { + const auto imeResult = OwnerDraw::GetIMEResult(); + updatePrimaryAfterPaint(); + return imeResult; + } + + const auto pSelectedWndProc = FindWindowProc(OwnerDraw::SubclassProcs, hWnd); + + if (message == WM_SYSKEYUP && wParam == VK_TAB) + ::SendMessageA(Game::hWnd, WM_SYSKEYUP, VK_TAB, lParam); + + // RenderDX's original 0x610E77 hook made the final copyback use client-surface coordinates. + constexpr int windowOffsetX = 0; + constexpr int windowOffsetY = 0; + + if (IsActiveWindowMessageBlocked(hWnd, message)) + { + updatePrimaryAfterPaint(); + return 0; + } + + WindowMessageGuardScope guard(hWnd, message); + if (!guard.Enter()) + { + updatePrimaryAfterPaint(); + return 0; + } + + RECT clientRect {}; + ::GetClientRect(hWnd, &clientRect); + + RECT ownerDrawClientRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerDrawClientRect); + + if (isPaintMessage && !Unsorted::GameInFocus) + { + ::ValidateRect(hWnd, nullptr); + guard.Release(); + updatePrimaryAfterPaint(); + return 0; + } + + if (isPaintMessage) + { + ++OwnerDraw::PaintDepth; + + RECT updateRect {}; + ::GetUpdateRect(hWnd, &updateRect, FALSE); + updateRect.left += ownerDrawClientRect.left; + updateRect.right += ownerDrawClientRect.left; + updateRect.top += ownerDrawClientRect.top; + updateRect.bottom += ownerDrawClientRect.top; + } + + auto pData = FindOwnerDrawData(hWnd); + int paintCopyMode = 0; + bool redrawTooltip = false; + LRESULT result = 0; + + auto complete = [&](LRESULT result) -> LRESULT + { + guard.Release(); + + if (isPaintMessage) + FinishPaint(hWnd, pData, paintCopyMode, windowOffsetX, windowOffsetY); + + if (redrawTooltip) + OwnerDraw::DrawTooltip(true); + + updatePrimaryAfterPaint(); + + return message == WM_INITDIALOG ? 0 : result; + }; + + switch (message) + { + case WW_GETHWND: + return complete(pData && pData->LinkedHwnd() ? 1 : 0); + + case WW_GETGDIPROPS: + if (pData) + { + const auto hdc = reinterpret_cast(lParam); + const auto oldFont = ::SelectObject(hdc, ::GetStockObject(SYSTEM_FONT)); + pData->Extra[SavedFontExtraIndex] = reinterpret_cast(oldFont); + ::SelectObject(hdc, oldFont); + pData->Extra[SavedBkModeExtraIndex] = ::GetBkMode(hdc); + pData->Extra[SavedBkColorExtraIndex] = ::GetBkColor(hdc); + pData->Extra[SavedTextColorExtraIndex] = ::GetTextColor(hdc); + return complete(1); + } + break; + + case WW_SETGDIPROPS: + if (pData) + { + const auto hdc = reinterpret_cast(lParam); + ::SelectObject(hdc, reinterpret_cast(pData->Extra[SavedFontExtraIndex])); + ::SetBkMode(hdc, pData->Extra[SavedBkModeExtraIndex]); + ::SetBkColor(hdc, pData->Extra[SavedBkColorExtraIndex]); + ::SetTextColor(hdc, pData->Extra[SavedTextColorExtraIndex]); + return complete(1); + } + break; + + case WW_SETHASIMAGE: + if (pData) + { + const auto previous = pData->NeedsControlImage; + const HWND linkedHwnd = pData->LinkedHwnd(); + pData->NeedsControlImage = lParam; + + if (linkedHwnd) + { + if (auto pLinkedData = FindOwnerDrawData(linkedHwnd)) + pLinkedData->NeedsControlImage = lParam; + } + + result = previous; + } + break; + + case WW_BRINGTOTOP: + return complete(BringOwnerDrawWindowToTop(hWnd, wParam, lParam)); + + default: + break; + } + + if (OwnerDraw::ActiveWindowStackCount) + { + if (message == WM_WINDOWPOSCHANGING) + { + LRESULT windowPosResult = 0; + if (HandleWindowPosChanging(hWnd, lParam, windowPosResult)) + return complete(windowPosResult); + } + else if (message == WM_DESTROY) + { + RemoveActiveWindow(hWnd); + } + } + + RestoreAndClearTooltipIfNeeded(hWnd, message); + + bool callSelectedHandler = true; + + switch (message) + { + case WM_ERASEBKGND: + return complete(1); + + case WM_SETFOCUS: + if (pSelectedWndProc == OwnerDraw::OwnerDrawButtonHandler || pSelectedWndProc == OwnerDraw::ListBoxHandler) + ::SetFocus(reinterpret_cast(wParam)); + + if (pData && !pData->HasFocus) + { + OwnerDraw::CancelIMEComposition(); + pData->HasFocus = 1; + } + break; + + case WM_KILLFOCUS: + if (pData) + pData->HasFocus = 0; + break; + + case WM_SHOWWINDOW: + if (!wParam && pData) + pData->Extra[PaintStateExtraIndex] = 0; + break; + + default: + break; + } + + if (pData) + { + switch (message) + { + case WW_SETIMAGE: + result = reinterpret_cast(pData->ControlImage); + pData->ControlImage = reinterpret_cast(lParam); + return complete(result); + + case WW_SETACTIVEIMAGE: + result = reinterpret_cast(pData->StateImageSurface); + pData->StateImageSurface = reinterpret_cast(lParam); + return complete(result); + + case WW_SETUNKNOWNPROP24: + result = pData->UnknownProp24(); + pData->UnknownProp24() = lParam; + return complete(result); + + default: + break; + } + + if (pData->UnknownProp24() && (message == WM_TIMER || (message >= WM_MOUSEFIRST && message <= WM_MBUTTONDBLCLK))) + { + RECT rect {}; + ::GetWindowRect(hWnd, &rect); + ::WindowFromPoint(POINT { rect.left, rect.top }); + } + } + + if (IsNativeTextMessage(message, result)) + callSelectedHandler = false; + + if (pData) + HandleElementTextMessage(*pData, hWnd, message, wParam, lParam, result, callSelectedHandler); + + if (message == WM_MOUSEMOVE) + UpdateTooltipTextOnMouseMove(hWnd, lParam); + + if (isPaintMessage && pData) + { + result = DispatchPaintMessage( + hWnd, + message, + wParam, + lParam, + pSelectedWndProc, + ownerDrawClientRect, + paintCopyMode, + redrawTooltip); + } + else if (callSelectedHandler && pSelectedWndProc) + { + result = CallSelectedHandler(pSelectedWndProc, hWnd, message, wParam, lParam); + } + + if (message == WM_NCDESTROY) + CleanupDestroyedWindow(hWnd); + + return complete(result); +} diff --git a/src/OwnerDraw/OwnerDraw.h b/src/OwnerDraw/OwnerDraw.h new file mode 100644 index 0000000000..88cf97218f --- /dev/null +++ b/src/OwnerDraw/OwnerDraw.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace WWUI +{ + LRESULT __fastcall OwnerDrawStandardWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK OwnerDrawWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK ScrollBarCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK ListBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK ProgressCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK NewEditCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK EditCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK StaticCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK TabCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK GroupBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK OwnerDrawCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK CheckboxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK RadioCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK InputCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT CALLBACK SysListViewCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); +} diff --git a/src/OwnerDraw/Progress.cpp b/src/OwnerDraw/Progress.cpp new file mode 100644 index 0000000000..7f85c49285 --- /dev/null +++ b/src/OwnerDraw/Progress.cpp @@ -0,0 +1,87 @@ +#include "OwnerDraw.Internal.h" + +static void EnsureProgressCache(OwnerDrawDialogElement& data, const RectangleStruct& cacheRect, const RectangleStruct& screenRect) +{ + if (data.CacheSurface || !DSurface::Alternate || cacheRect.Width <= 0 || cacheRect.Height <= 0) + return; + + data.CacheSurface = GameCreate(cacheRect.Width, cacheRect.Height); + if (!data.CacheSurface) + return; + + ++OwnerDraw::CachedSurfaceCount; + CopySurfacePart(data.CacheSurface, cacheRect, DSurface::Alternate, screenRect); +} + +static void PaintProgress(OwnerDrawDialogElement& data, const RECT& ownerRect) +{ + if (!DSurface::Alternate) + return; + + const int width = ownerRect.right - ownerRect.left + 1; + const int height = ownerRect.bottom - ownerRect.top + 1; + if (width <= 0 || height <= 0) + return; + + RectangleStruct cacheRect { 0, 0, width, height }; + RectangleStruct screenRect { ownerRect.left, ownerRect.top, width, height }; + + EnsureProgressCache(data, cacheRect, screenRect); + + if (data.CacheSurface) + CopySurfacePart(DSurface::Alternate, screenRect, data.CacheSurface, cacheRect); + + const int rangeSpan = data.AsProgress().MaxValue() - data.AsProgress().MinValue(); + const int widthScale = rangeSpan + ? static_cast((static_cast(data.AsProgress().Position()) << 16) / rangeSpan) + : 0; + + const WORD color = static_cast(ConvertRGBToSurfaceColor(RGB(255, 0, 0))); + BlendGradientRect(screenRect, DSurface::Alternate, color, widthScale); +} + +LRESULT CALLBACK WWUI::ProgressCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + RECT ownerRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + auto pData = FindOwnerDrawData(hWnd); + if (!pData) + return 0; + + auto& data = *pData; + + switch (message) + { + case WW_PROGRESS_SETRANGE: + data.AsProgress().MinValue() = LOWORD(lParam); + data.AsProgress().MaxValue() = HIWORD(lParam); + return 0; + + case WW_PROGRESS_SETPOS: + { + int position = static_cast(wParam); + if (position < data.AsProgress().MinValue()) + position = data.AsProgress().MinValue(); + + if (position > data.AsProgress().MaxValue()) + position = data.AsProgress().MaxValue(); + + data.AsProgress().Position() = position; + ::InvalidateRect(hWnd, nullptr, FALSE); + return 0; + } + + case WM_PAINT: + PaintProgress(data, ownerRect); + ::ValidateRect(hWnd, nullptr); + return 0; + + case WW_INITDIALOG: + data.AsProgress().MaxValue() = 100; + return 0; + + default: + return 0; + } +} diff --git a/src/OwnerDraw/ScrollBar.cpp b/src/OwnerDraw/ScrollBar.cpp new file mode 100644 index 0000000000..e48a23cc37 --- /dev/null +++ b/src/OwnerDraw/ScrollBar.cpp @@ -0,0 +1,482 @@ +#include "OwnerDraw.Internal.h" + +constexpr int ScrollBarButtonHeight = 22; +constexpr int ScrollBarMinimumThumbHeight = 14; +constexpr int ScrollBarInitialRepeatMs = 500; +constexpr int ScrollBarRepeatMs = 25; + +void DrawScrollArrow(Surface* pSurface, const RectangleStruct& rect, bool isUp, bool pressed, bool useGreyArt) +{ + char filename[32] {}; + std::snprintf(filename, sizeof(filename), isUp ? "guparrow%c.pcx" : "gdnarrow%c.pcx", pressed ? 'p' : 'r'); + + const char* pFilename = useGreyArt ? filename : filename + 1; + DrawPCXCopy(pSurface, rect, GetPCXSurface(pFilename)); +} + +static void BlendScrollBarCache(OwnerDrawDialogElement& data, int width, int height) +{ + auto pPixels = static_cast(data.CacheSurface->Lock(0, 0)); + if (!pPixels) + return; + + const int pixelCount = width * height; + const int alpha = static_cast(data.Alpha); + + for (int i = 0; i < pixelCount; ++i) + { + pPixels[i] = BlendSurfacePixelTowardMasks(pPixels[i], alpha); + } + + data.CacheSurface->Unlock(); +} + +void EnsureScrollBarCache( + OwnerDrawDialogElement& data, + OwnerDrawDialogElement* pParentData, + const RectangleStruct& localRect, + const RectangleStruct& parentSourceRect) +{ + if (data.CacheSurface) + { + if (data.CacheSurface->GetWidth() != localRect.Width || data.CacheSurface->GetHeight() != localRect.Height) + DeleteSurfaceObject(data.CacheSurface); + } + + if (data.CacheSurface || localRect.Width <= 0 || localRect.Height <= 0) + return; + + data.CacheSurface = GameCreate(localRect.Width, localRect.Height); + ++OwnerDraw::CachedSurfaceCount; + + if (pParentData && pParentData->CacheSurface) + CopySurfacePart(data.CacheSurface, localRect, pParentData->CacheSurface, parentSourceRect); + + BlendScrollBarCache(data, localRect.Width, localRect.Height); +} + +static void PaintScrollBar( + HWND hWnd, + OwnerDrawDialogElement& data, + const RECT& clientRect, + const RECT& scrollBarRect, + int thumbTop, + int thumbBottom, + bool upPressed, + bool downPressed) +{ + if (!DSurface::Alternate) + return; + + RectangleStruct localRect { 0, 0, clientRect.right, clientRect.bottom }; + RectangleStruct destRect { scrollBarRect.left, scrollBarRect.top, clientRect.right, clientRect.bottom }; + + const HWND parentHwnd = ::GetParent(hWnd); + auto pParentData = parentHwnd ? FindOwnerDrawData(parentHwnd) : nullptr; + + RECT parentRect {}; + if (parentHwnd) + OwnerDraw::GetRectangle(parentHwnd, &parentRect); + + RectangleStruct parentSourceRect = localRect; + if (pParentData && pParentData->CacheSurface) + { + parentSourceRect.X = scrollBarRect.left - parentRect.left; + parentSourceRect.Y = scrollBarRect.top - parentRect.top; + } + + EnsureScrollBarCache(data, pParentData, localRect, parentSourceRect); + + if (pParentData && pParentData->CacheSurface) + CopySurfacePart(DSurface::Alternate, destRect, pParentData->CacheSurface, parentSourceRect); + + const bool disabled = data.AsScrollBar().Disabled(); + int borderColor = ConvertRGBToSurfaceColor(disabled ? OwnerDraw::AltBorderColor : OwnerDraw::DefaultBorderColor); + if (disabled && OwnerDraw::AltBorderColor == static_cast(-1)) + borderColor = -1; + + DrawBeveledBorder(DSurface::Alternate, destRect, 2, borderColor); + + RectangleStruct thumbRect + { + scrollBarRect.left, + scrollBarRect.top + thumbTop, + clientRect.right, + thumbBottom - thumbTop + }; + + if (auto pGripMiddle = GetPCXSurface(disabled ? "gsbgripm.pcx" : "sbgripm.pcx")) + { + auto middleRect = thumbRect; + middleRect.Width = pGripMiddle->GetWidth(); + BlitTiledPCX(middleRect, DSurface::Alternate, pGripMiddle, 0, 0); + } + + if (auto pGripTop = GetPCXSurface(disabled ? "gsbgript.pcx" : "sbgript.pcx")) + DrawPCXCopy(DSurface::Alternate, thumbRect, pGripTop); + + if (auto pGripBottom = GetPCXSurface(disabled ? "gsbgripb.pcx" : "sbgripb.pcx")) + { + RectangleStruct bottomRect + { + thumbRect.X, + scrollBarRect.top + thumbBottom - pGripBottom->GetHeight(), + thumbRect.Width, + thumbRect.Height + }; + + DrawPCXCopy(DSurface::Alternate, bottomRect, pGripBottom); + } + + RectangleStruct upButtonRect { scrollBarRect.left, scrollBarRect.top, clientRect.right, ScrollBarButtonHeight }; + RectangleStruct upButtonSource { 0, 0, clientRect.right, ScrollBarButtonHeight }; + CopySurfacePart(DSurface::Alternate, upButtonRect, data.CacheSurface, upButtonSource); + + const BYTE bevelAlpha = static_cast(OwnerDraw::ScrollButtonBevelAlpha); + DrawAlphaBeveledRect(DSurface::Alternate, upButtonRect, !upPressed, 2, bevelAlpha, bevelAlpha, bevelAlpha, bevelAlpha); + DrawScrollArrow(DSurface::Alternate, upButtonRect, true, upPressed, disabled); + + RectangleStruct downButtonRect + { + scrollBarRect.left, + scrollBarRect.bottom - ScrollBarButtonHeight, + clientRect.right, + ScrollBarButtonHeight + }; + RectangleStruct downButtonSource { 0, clientRect.bottom - ScrollBarButtonHeight, clientRect.right, ScrollBarButtonHeight }; + CopySurfacePart(DSurface::Alternate, downButtonRect, data.CacheSurface, downButtonSource); + DrawAlphaBeveledRect(DSurface::Alternate, downButtonRect, !downPressed, 2, bevelAlpha, bevelAlpha, bevelAlpha, bevelAlpha); + DrawScrollArrow(DSurface::Alternate, downButtonRect, false, downPressed, disabled); +} + +LRESULT CALLBACK WWUI::ScrollBarCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + RECT clientRect {}; + ::GetClientRect(hWnd, &clientRect); + + RECT scrollBarRect {}; + OwnerDraw::GetRectangle(hWnd, &scrollBarRect); + + const int inset = OwnerDraw::ControlInsetPx; + const int clientWidth = clientRect.right - 2 * inset; + clientRect.bottom -= 2 * inset; + clientRect.right = clientWidth; + scrollBarRect.left += inset; + scrollBarRect.top += inset; + scrollBarRect.right -= inset; + scrollBarRect.bottom -= inset; + + auto pData = FindOwnerDrawData(hWnd); + if (!pData) + return 0; + + auto& data = *pData; + + const bool restoreCaptureToNotify = data.AsScrollBar().RestoreCaptureToNotifyHwnd() != 0; + bool isMouseTracking = data.AsScrollBar().IsMouseTracking() != 0; + bool isThumbDragging = data.AsScrollBar().IsThumbDragging() != 0; + int rangeMax = data.AsScrollBar().RangeMax(); + int position = data.AsScrollBar().Position(); + bool upButtonPressed = data.AsScrollBar().UpButtonPressed() != 0; + bool downButtonPressed = data.AsScrollBar().DownButtonPressed() != 0; + + if (!rangeMax) + rangeMax = 100; + + const int scrollBarWidth = clientRect.right - clientRect.left; + const int scrollBarLeft = clientRect.right - scrollBarWidth; + const int trackHeight = clientRect.bottom - clientRect.top - 2 * ScrollBarButtonHeight; + + int thumbHeight = static_cast( + static_cast(trackHeight) + - std::log(static_cast(rangeMax + 1)) * static_cast(trackHeight) * 0.2); + + if (thumbHeight <= ScrollBarMinimumThumbHeight) + thumbHeight = ScrollBarMinimumThumbHeight; + + int thumbTravel = trackHeight - thumbHeight; + int thumbTravelDivisor = thumbTravel; + if (thumbTravelDivisor <= 1) + { + thumbTravel = 1; + thumbTravelDivisor = 1; + } + + int thumbTop = 0; + int thumbBottom = 0; + int notifyCode = SB_LINEUP; + + auto updateThumbFromCursor = [&]() + { + POINT point {}; + ::GetCursorPos(&point); + ::ScreenToClient(hWnd, &point); + + thumbTop = point.y - thumbHeight / 2; + if (thumbTop < ScrollBarButtonHeight) + thumbTop = ScrollBarButtonHeight; + + const int maxThumbTop = clientRect.bottom - thumbHeight - ScrollBarButtonHeight; + if (maxThumbTop < thumbTop) + thumbTop = maxThumbTop; + + thumbBottom = thumbTop + thumbHeight; + notifyCode = SB_THUMBTRACK; + position = rangeMax * (thumbTop - ScrollBarButtonHeight) / thumbTravelDivisor; + }; + + if (message < WM_USER || message == WW_SCROLLBAR_UPDATETHUMB) + { + if (isThumbDragging) + { + updateThumbFromCursor(); + } + else + { + thumbTop = position * thumbTravel / rangeMax + clientRect.top + ScrollBarButtonHeight; + thumbBottom = thumbTop + thumbHeight; + } + } + + auto restoreCapture = [&]() + { + ::KillTimer(hWnd, 0); + ::ReleaseCapture(); + + if (restoreCaptureToNotify && data.AsScrollBar().NotifyHwnd()) + ::SetCapture(data.AsScrollBar().NotifyHwnd()); + }; + + auto writeBack = [&]() -> LRESULT + { + const bool shouldNotify = + (position != data.AsScrollBar().Position() || rangeMax != data.AsScrollBar().RangeMax()) + && data.AsScrollBar().NotifyHwnd(); + + data.AsScrollBar().IsMouseTracking() = isMouseTracking; + data.AsScrollBar().IsThumbDragging() = isThumbDragging; + data.AsScrollBar().RangeMax() = rangeMax; + data.AsScrollBar().Position() = position; + data.AsScrollBar().UpButtonPressed() = upButtonPressed; + data.AsScrollBar().DownButtonPressed() = downButtonPressed; + + if (shouldNotify) + { + const WPARAM scrollParam = (static_cast(position & 0xFFFF) << 16) + | static_cast(notifyCode & 0xFFFF); + + ::SendMessageA(data.AsScrollBar().NotifyHwnd(), WM_VSCROLL, scrollParam, reinterpret_cast(hWnd)); + ::InvalidateRect(hWnd, nullptr, FALSE); + } + + return 0; + }; + + switch (message) + { + case SBM_GETPOS: + return position; + + case WM_ERASEBKGND: + return 0; + + case WM_NCHITTEST: + case WM_GETDLGCODE: + return CallSelectedHandler(FindWindowProc(OwnerDraw::DialogProcs, hWnd), hWnd, message, wParam, lParam); + + case WM_PAINT: + if (data.SkipDraw) + { + ::ValidateRect(hWnd, nullptr); + return writeBack(); + } + + if (data.NeedsControlImage) + return 0; + + PaintScrollBar( + hWnd, + data, + clientRect, + scrollBarRect, + thumbTop, + thumbBottom, + upButtonPressed, + downButtonPressed); + + ::ValidateRect(hWnd, nullptr); + return writeBack(); + + case SBM_SETPOS: + if (static_cast(wParam) <= rangeMax && static_cast(wParam) > 0) + position = static_cast(wParam); + + return writeBack(); + + case SBM_SETRANGE: + rangeMax = static_cast(lParam); + if (position > rangeMax) + position = rangeMax; + + return writeBack(); + + case SBM_SETSCROLLINFO: + { + const auto pScrollInfo = reinterpret_cast(lParam); + rangeMax = pScrollInfo->nMax; + position = pScrollInfo->nPos; + return writeBack(); + } + + case WM_TIMER: + { + POINT point {}; + ::GetCursorPos(&point); + ::ScreenToClient(hWnd, &point); + + upButtonPressed = false; + downButtonPressed = false; + + if (isMouseTracking && point.x > scrollBarLeft) + { + if (point.y < ScrollBarButtonHeight) + { + upButtonPressed = !isThumbDragging; + if (position) + { + notifyCode = SB_LINEUP; + --position; + ::SetTimer(hWnd, 0, ScrollBarRepeatMs, nullptr); + return writeBack(); + } + } + else if (point.y > clientRect.bottom - ScrollBarButtonHeight) + { + downButtonPressed = !isThumbDragging; + if (position + 1 <= rangeMax) + { + notifyCode = SB_LINEDOWN; + ++position; + } + } + } + + ::SetTimer(hWnd, 0, ScrollBarRepeatMs, nullptr); + return writeBack(); + } + + case WM_MOUSEMOVE: + if (isThumbDragging) + { + RECT invalidateRect + { + scrollBarLeft, + clientRect.top, + clientRect.right, + clientRect.bottom + }; + ::InvalidateRect(hWnd, &invalidateRect, FALSE); + } + + if (wParam & MK_LBUTTON) + return writeBack(); + + [[fallthrough]]; + + case WM_LBUTTONUP: + isMouseTracking = false; + isThumbDragging = false; + + if (upButtonPressed || downButtonPressed) + ::InvalidateRect(hWnd, nullptr, FALSE); + + upButtonPressed = false; + downButtonPressed = false; + restoreCapture(); + notifyCode = SB_ENDSCROLL; + return writeBack(); + + case WM_LBUTTONDOWN: + case WM_LBUTTONDBLCLK: + if (message == WM_LBUTTONDOWN) + { + isMouseTracking = true; + ::SetCapture(hWnd); + ::SetTimer(hWnd, 0, ScrollBarInitialRepeatMs, nullptr); + } + else + { + isMouseTracking = false; + isThumbDragging = false; + restoreCapture(); + } + + { + const int clickX = LOWORD(lParam); + const int clickY = HIWORD(lParam); + const int repeatCount = message == WM_LBUTTONDBLCLK ? 2 : 1; + + upButtonPressed = false; + downButtonPressed = false; + + for (int i = 0; i < repeatCount; ++i) + { + if (clickX <= scrollBarLeft) + continue; + + if (clickY < ScrollBarButtonHeight && position) + { + upButtonPressed = true; + notifyCode = SB_LINEUP; + --position; + } + else if (clickY <= clientRect.bottom - ScrollBarButtonHeight || position + 1 > rangeMax) + { + if (clickY < thumbTop || clickY >= thumbBottom) + { + thumbTop = clickY - thumbHeight / 2; + if (thumbTop < ScrollBarButtonHeight) + thumbTop = ScrollBarButtonHeight; + + const int maxThumbTop = clientRect.bottom - thumbHeight - ScrollBarButtonHeight; + if (maxThumbTop < thumbTop) + thumbTop = maxThumbTop; + + thumbBottom = thumbTop + thumbHeight; + notifyCode = SB_THUMBTRACK; + position = rangeMax * (thumbTop - ScrollBarButtonHeight) / thumbTravelDivisor; + } + else if (message == WM_LBUTTONDOWN) + { + isThumbDragging = true; + } + } + else + { + ++position; + downButtonPressed = true; + notifyCode = SB_LINEDOWN; + } + } + } + + return writeBack(); + + default: + break; + } + + if (message == WW_DROPDOWN_SETACTIVE) + { + data.AsScrollBar().RestoreCaptureToNotifyHwnd() = lParam != 0; + return writeBack(); + } + + if (message == WW_CB_SETALTERNATEPALETTE) + { + data.AsScrollBar().Disabled() = lParam == 1; + return writeBack(); + } + + return writeBack(); +} diff --git a/src/OwnerDraw/Slider.cpp b/src/OwnerDraw/Slider.cpp new file mode 100644 index 0000000000..b829e95b7f --- /dev/null +++ b/src/OwnerDraw/Slider.cpp @@ -0,0 +1,434 @@ +#include "OwnerDraw.Internal.h" + +constexpr int SliderValueLabelWidth = 50; +constexpr int SliderTrackRightPadding = 13; +constexpr int SliderGripCenterOffset = 6; +constexpr int SliderGripHitWidth = 12; +constexpr int SliderMouseHitBottomInset = 18; + +static int SliderTrackTravel(const RECT& clientRect, int valueLabelWidth) +{ + const int travel = clientRect.right - clientRect.left - valueLabelWidth - SliderTrackRightPadding; + return travel > 1 ? travel : 1; +} + +static int SliderThumbOffsetFromPosition(int positionOffset, int trackTravel, int rangeSpan) +{ + if (!rangeSpan) + return 0; + + return positionOffset * trackTravel / rangeSpan; +} + +static int SliderClampedGripX(int x, const RECT& clientRect, int valueLabelWidth) +{ + int gripX = x - SliderGripCenterOffset; + if (gripX < 1) + gripX = 1; + + const int maxGripX = clientRect.right - valueLabelWidth - SliderGripHitWidth; + if (maxGripX < gripX) + gripX = maxGripX; + + return gripX; +} + +static void SliderUpdateFromGripX( + int gripX, + int rangeSpan, + int rangeMin, + int stepValue, + int trackTravel, + int& positionOffset, + int& thumbOffsetPixels) +{ + if (!trackTravel) + trackTravel = 1; + + int offset = (rangeSpan + 1) * (gripX - 1) / trackTravel; + if (offset >= rangeSpan) + offset = rangeSpan; + + if (!stepValue) + stepValue = 1; + + positionOffset = stepValue * ((rangeMin + offset) / stepValue) - rangeMin; + thumbOffsetPixels = SliderThumbOffsetFromPosition(positionOffset, trackTravel, rangeSpan); +} + +static void EnsureSliderCache(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect) +{ + if (data.CacheSurface || !DSurface::Alternate) + return; + + const int width = clientRect.right + 1; + const int height = clientRect.bottom + 1; + if (width <= 0 || height <= 0) + return; + + data.CacheSurface = GameCreate(width, height); + ++OwnerDraw::CachedSurfaceCount; + + RectangleStruct destRect { 0, 0, width, height }; + RectangleStruct sourceRect { ownerRect.left, ownerRect.top, width, height }; + CopySurfacePart(data.CacheSurface, destRect, DSurface::Alternate, sourceRect); + BlendFillRect(destRect, data.CacheSurface, 0, static_cast(data.Alpha)); +} + +static int SliderBorderColor(bool disabled) +{ + const COLORREF color = disabled ? OwnerDraw::DisabledBorderColor : OwnerDraw::DefaultBorderColor; + if (color == static_cast(-1)) + return -1; + + return ConvertRGBToSurfaceColor(color); +} + +static void PaintSlider( + HWND hWnd, + OwnerDrawDialogElement& data, + const RECT& clientRect, + const RECT& ownerRect, + int thumbHitLeftX, + int thumbHitRightX, + int rangeMin, + int positionOffset, + int stepValue, + int valueLabelWidth, + bool showValueLabel) +{ + if (!DSurface::Alternate) + return; + + const int controlLeft = ownerRect.left; + const int controlTop = ownerRect.top; + const int controlWidth = ownerRect.right - ownerRect.left; + const int controlHeight = ownerRect.bottom - ownerRect.top; + + const LONG style = ::GetWindowLongA(hWnd, GWL_STYLE); + const bool disabled = (style & WS_DISABLED) != 0; + + EnsureSliderCache(hWnd, data, clientRect, ownerRect); + + if (data.CacheSurface) + { + RectangleStruct destRect { ownerRect.left, ownerRect.top, clientRect.right + 1, clientRect.bottom + 1 }; + RectangleStruct sourceRect { 0, 0, clientRect.right + 1, clientRect.bottom + 1 }; + CopySurfacePart(DSurface::Alternate, destRect, data.CacheSurface, sourceRect); + } + + if (showValueLabel) + { + const int labelPanelX = controlWidth - valueLabelWidth + controlLeft + 1; + const int labelPanelTop = controlTop - 1; + + if (auto pMiddle = GetPCXSurface("trofm.pcx")) + { + RectangleStruct middleRect { labelPanelX, labelPanelTop, valueLabelWidth, pMiddle->GetHeight() }; + BlitTiledPCX(middleRect, DSurface::Alternate, pMiddle, 0, 0); + } + + if (auto pLeft = GetPCXSurface("trofl.pcx")) + { + RectangleStruct leftRect { labelPanelX, labelPanelTop, pLeft->GetWidth(), pLeft->GetHeight() }; + DrawPCXCopy(DSurface::Alternate, leftRect, pLeft); + } + + if (auto pRight = GetPCXSurface("trofr.pcx")) + { + RectangleStruct rightRect + { + controlLeft + controlWidth - pRight->GetWidth() + 1, + labelPanelTop, + pRight->GetWidth(), + pRight->GetHeight() + }; + DrawPCXCopy(DSurface::Alternate, rightRect, pRight); + } + } + + RectangleStruct gripRect + { + controlLeft + thumbHitLeftX, + controlTop, + thumbHitRightX - thumbHitLeftX, + controlHeight + }; + + if (auto pGrip = GetPCXSurface("trakgrip.pcx")) + DrawPCXCopy(DSurface::Alternate, gripRect, pGrip); + + if (disabled) + BlendFillRect(gripRect, DSurface::Alternate, 0, OwnerDraw::DisabledOverlayAlpha); + + const int borderColor = SliderBorderColor(disabled); + RectangleStruct trackBorderRect + { + controlLeft, + controlTop, + controlWidth - valueLabelWidth, + controlHeight + }; + DrawBeveledBorder(DSurface::Alternate, trackBorderRect, 2, borderColor); + + const int labelInset = (showValueLabel ? 1 : 0) + 1; + RectangleStruct labelBorderRect + { + controlLeft + controlWidth + labelInset - valueLabelWidth, + controlTop, + valueLabelWidth - labelInset, + controlHeight + }; + DrawBeveledBorder(DSurface::Alternate, labelBorderRect, 2, borderColor); + + if (showValueLabel) + { + if (!stepValue) + stepValue = 1; + + wchar_t text[0x100] {}; + std::swprintf(text, std::size(text), L"%d", stepValue * ((rangeMin + positionOffset) / stepValue)); + + RECT textRect + { + ownerRect.right - 49, + ownerRect.top, + ownerRect.right, + ownerRect.bottom + }; + + const COLORREF textColor = disabled ? Phobos::UI::ColorDisabledSlider : Phobos::UI::ColorTextSlider; + OwnerDraw::DrawWideText(DSurface::Alternate, text, &textRect, data.AsSlider().Font(), textColor, 5, 12, 0, 0, 0); + } +} + +LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + RECT clientRect {}; + ::GetClientRect(hWnd, &clientRect); + + RECT ownerRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + auto pData = FindOwnerDrawData(hWnd); + if (!pData) + return 0; + + auto& data = *pData; + + int playClickSound = 1; + int isMouseTracking = data.AsSlider().IsMouseTracking(); + int isThumbDragging = data.AsSlider().IsThumbDragging(); + int rangeSpan = data.AsSlider().RangeSpan(); + int positionOffset = data.AsSlider().PositionOffset(); + int rangeMin = data.AsSlider().RangeMin(); + int thumbOffsetPixels = data.AsSlider().ThumbOffsetPixels(); + int stepValue = data.AsSlider().StepValue(); + int showValueLabel = data.AsSlider().ShowValueLabel(); + + int valueLabelWidth = showValueLabel ? SliderValueLabelWidth : 0; + const int trackTravel = SliderTrackTravel(clientRect, valueLabelWidth); + + if (!rangeSpan) + { + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + const int rangeMax = static_cast(CallSelectedHandler(pOriginalWndProc, hWnd, WW_SLIDER_GETRANGEMAX, 0, 0)); + rangeMin = static_cast(CallSelectedHandler(pOriginalWndProc, hWnd, WW_SLIDER_GETRANGEMIN, 0, 0)); + rangeSpan = rangeMax - rangeMin; + if (!rangeSpan) + rangeSpan = 100; + + positionOffset = static_cast(CallSelectedHandler(pOriginalWndProc, hWnd, WW_SLIDER_GETPOS, 0, 0)) - rangeMin; + thumbOffsetPixels = SliderThumbOffsetFromPosition(positionOffset, trackTravel, rangeSpan); + + data.AsSlider().ThumbOffsetPixels() = thumbOffsetPixels; + data.AsSlider().RangeSpan() = rangeSpan; + data.AsSlider().PositionOffset() = positionOffset; + data.AsSlider().RangeMin() = rangeMin; + data.AsSlider().StepValue() = stepValue; + data.AsSlider().ShowValueLabel() = showValueLabel; + } + + if (!stepValue) + { + valueLabelWidth = SliderValueLabelWidth; + stepValue = 1; + showValueLabel = 1; + } + + int thumbHitLeftX = clientRect.left + thumbOffsetPixels + 1; + int thumbHitRightX = thumbHitLeftX + SliderGripHitWidth; + + if (isThumbDragging) + { + POINT point {}; + ::GetCursorPos(&point); + ::ScreenToClient(hWnd, &point); + + SliderUpdateFromGripX( + SliderClampedGripX(point.x, clientRect, valueLabelWidth), + rangeSpan, + rangeMin, + stepValue, + trackTravel, + positionOffset, + thumbOffsetPixels); + + thumbHitLeftX = thumbOffsetPixels + 1; + thumbHitRightX = thumbHitLeftX + SliderGripHitWidth; + } + + auto forwardOriginal = [&]() -> LRESULT + { + return CallSelectedHandler(FindWindowProc(OwnerDraw::DialogProcs, hWnd), hWnd, message, wParam, lParam); + }; + + auto writeBack = [&]() -> LRESULT + { + const bool valueChanged = + positionOffset != data.AsSlider().PositionOffset() + || rangeSpan != data.AsSlider().RangeSpan() + || rangeMin != data.AsSlider().RangeMin(); + + data.AsSlider().IsMouseTracking() = isMouseTracking; + data.AsSlider().IsThumbDragging() = isThumbDragging; + data.AsSlider().ThumbOffsetPixels() = thumbOffsetPixels; + data.AsSlider().RangeSpan() = rangeSpan; + data.AsSlider().PositionOffset() = positionOffset; + data.AsSlider().RangeMin() = rangeMin; + data.AsSlider().StepValue() = stepValue; + data.AsSlider().ShowValueLabel() = showValueLabel; + + if (valueChanged) + { + ::InvalidateRect(hWnd, nullptr, FALSE); + ::SendMessageA(::GetParent(hWnd), WM_HSCROLL, MAKELONG(SB_THUMBTRACK, positionOffset + rangeMin), reinterpret_cast(hWnd)); + + if (playClickSound == 1 && !data.AsSlider().SuppressClickSound() && RulesClass::Instance) + VocClass::PlayGlobal(RulesClass::Instance->GenericClick, 0x2000, 1.0f); + } + + return 0; + }; + + switch (message) + { + case WM_ERASEBKGND: + return 0; + + case WM_NCHITTEST: + case WM_GETDLGCODE: + return forwardOriginal(); + + case WM_ENABLE: + ::InvalidateRect(hWnd, nullptr, FALSE); + return writeBack(); + + case WM_PAINT: + PaintSlider( + hWnd, + data, + clientRect, + ownerRect, + thumbHitLeftX, + thumbHitRightX, + rangeMin, + positionOffset, + stepValue, + valueLabelWidth, + showValueLabel != 0); + ::ValidateRect(hWnd, nullptr); + return writeBack(); + + case WM_MOUSEMOVE: + if (isThumbDragging) + ::InvalidateRect(hWnd, &clientRect, FALSE); + + if (wParam & MK_LBUTTON) + return writeBack(); + + [[fallthrough]]; + + case WM_LBUTTONUP: + isMouseTracking = 0; + isThumbDragging = 0; + ::ReleaseCapture(); + return writeBack(); + + case WM_LBUTTONDOWN: + case WM_LBUTTONDBLCLK: + if (message == WM_LBUTTONDOWN) + { + isMouseTracking = 1; + ::SetCapture(hWnd); + } + else + { + isMouseTracking = 0; + ::ReleaseCapture(); + } + + if (HIWORD(lParam) > clientRect.bottom - SliderMouseHitBottomInset) + { + const int clickX = LOWORD(lParam); + if (clickX < thumbHitLeftX || clickX >= thumbHitRightX) + { + SliderUpdateFromGripX( + SliderClampedGripX(clickX, clientRect, valueLabelWidth), + rangeSpan, + rangeMin, + stepValue, + trackTravel, + positionOffset, + thumbOffsetPixels); + } + else if (message == WM_LBUTTONDOWN) + { + isThumbDragging = 1; + } + } + return writeBack(); + + case WW_SLIDER_GETPOS: + return stepValue * ((rangeMin + positionOffset) / stepValue); + + case WW_SLIDER_SETPOS: + { + const int requestedOffset = static_cast(lParam) - rangeMin; + if (requestedOffset <= rangeSpan && requestedOffset >= 0) + positionOffset = requestedOffset; + + playClickSound = 0; + thumbOffsetPixels = SliderThumbOffsetFromPosition(positionOffset, trackTravel, rangeSpan); + return writeBack(); + } + + case WW_SLIDER_SETRANGE: + rangeMin = LOWORD(lParam); + rangeSpan = HIWORD(lParam) - LOWORD(lParam); + if (positionOffset > rangeSpan) + positionOffset = rangeSpan; + + if (positionOffset < rangeMin) + positionOffset = rangeMin; + + playClickSound = 0; + thumbOffsetPixels = SliderThumbOffsetFromPosition(positionOffset, trackTravel, rangeSpan); + return writeBack(); + + case WW_SLIDER_SETSTEP: + stepValue = static_cast(lParam); + return writeBack(); + + case WW_SLIDER_SHOWVALUE: + showValueLabel = static_cast(lParam); + return writeBack(); + + case WW_SLIDER_SUPPRESSCLICK: + data.AsSlider().SuppressClickSound() = wParam == 0; + return writeBack(); + + default: + return writeBack(); + } +} diff --git a/src/OwnerDraw/Static.cpp b/src/OwnerDraw/Static.cpp new file mode 100644 index 0000000000..01167ca857 --- /dev/null +++ b/src/OwnerDraw/Static.cpp @@ -0,0 +1,586 @@ +#include "OwnerDraw.Internal.h" + +static UINT GetStaticAnimationTimerInterval(HWND parentHwnd, HWND controlHwnd) +{ + int dialogId = 0; + if (auto pParentData = FindOwnerDrawData(parentHwnd)) + dialogId = pParentData->DialogID; + + const int controlId = ::GetDlgCtrlID(controlHwnd); + constexpr int AnimatedDialogIds[] = + { + 148, 216, 245, 226, 213, 257, 297, 215, 187, 256, + 214, 293, 290, 274, 231, 278, 285, 284, 254, 271, + 279, 276, 230, 243, 244, 700, 270, 264, 3014, 3015 + }; + + if (controlId == 1820 + && std::find(std::begin(AnimatedDialogIds), std::end(AnimatedDialogIds), dialogId) != std::end(AnimatedDialogIds)) + { + return 100; + } + + if (dialogId == 148 && (controlId == 1770 || controlId == 1771 || controlId == 1772)) + return 100; + + if ((dialogId == 259 || dialogId == 3015) && controlId == 1835) + return 100; + + if (dialogId == 196 && controlId == 1961) + return 50; + + return 0; +} + +static void DestroyStaticMovie(OwnerDrawDialogElement& data) +{ + auto pMovie = data.AsStatic().MovieHandle(); + if (!pMovie) + return; + + if (pMovie->VTable && pMovie->VTable->Destructor) + pMovie->VTable->Destructor(pMovie, 1); + + data.AsStatic().MovieHandle() = nullptr; +} + +static void DestroyStaticMovieAux(OwnerDrawDialogElement& data) +{ + DeleteUnknownGameObject(data.AsStatic().MovieAuxHandle()); +} + +static void DetachStaticMovie(HWND hWnd, OwnerDrawDialogElement& data) +{ + DestroyStaticMovie(data); + ::KillTimer(hWnd, 0x65); + DestroyStaticMovieAux(data); +} + +static void PrepareStaticMovieLoad(HWND hWnd, OwnerDrawDialogElement& data) +{ + DestroyStaticMovie(data); + ::KillTimer(hWnd, 0x65); + DestroyStaticMovieAux(data); +} + +static LRESULT LoadStaticMovie(HWND hWnd, OwnerDrawDialogElement& data, const char* pMovieName) +{ + PrepareStaticMovieLoad(hWnd, data); + + RECT ownerRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + auto pMovie = OwnerDraw::InitMovieHandle(pMovieName, DSurface::Alternate, nullptr); + data.AsStatic().MovieHandle() = pMovie; + if (pMovie) + { + if (pMovie->VTable && pMovie->VTable->SetPosition) + pMovie->VTable->SetPosition(pMovie, ownerRect.left, ownerRect.top); + + ::MoveWindow(hWnd, ownerRect.left, ownerRect.top, pMovie->Width, pMovie->Height, FALSE); + ::SetTimer(hWnd, 0x65, 0x22, nullptr); + return 0; + } + + ::KillTimer(hWnd, 0x65); + DestroyStaticMovieAux(data); + return 0; +} + +static const char* GetStaticMovieName(int index) +{ + if (index < 0 || index >= MovieInfo::Array.Count) + return nullptr; + + return MovieInfo::Array.Items[index].Name; +} + +static bool EnsureStaticBackground(HWND hWnd, OwnerDrawDialogElement& data) +{ + if (data.AsStatic().CachedBackground() || !DSurface::Alternate) + return data.AsStatic().CachedBackground() != nullptr; + + RECT ownerRect {}; + RECT clientRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + ::GetClientRect(hWnd, &clientRect); + + const int width = clientRect.right + 1; + const int height = clientRect.bottom + 1; + if (width <= 0 || height <= 0) + return false; + + data.AsStatic().CachedBackground() = GameCreate(width, height); + if (!data.AsStatic().CachedBackground()) + return false; + + ++OwnerDraw::CachedSurfaceCount; + + RectangleStruct destRect { 0, 0, width, height }; + RectangleStruct sourceRect { ownerRect.left, ownerRect.top, width, height }; + CopySurfacePart(data.AsStatic().CachedBackground(), destRect, DSurface::Alternate, sourceRect); + return true; +} + +static void RestoreStaticBackground(HWND hWnd, OwnerDrawDialogElement& data, bool inclusiveBounds) +{ + if (!data.AsStatic().CachedBackground() || !DSurface::Alternate) + return; + + RECT ownerRect {}; + RECT clientRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + ::GetClientRect(hWnd, &clientRect); + + const int width = clientRect.right + (inclusiveBounds ? 1 : 0); + const int height = clientRect.bottom + (inclusiveBounds ? 1 : 0); + if (width <= 0 || height <= 0) + return; + + RectangleStruct destRect { ownerRect.left, ownerRect.top, width, height }; + RectangleStruct sourceRect { 0, 0, width, height }; + CopySurfacePart(DSurface::Alternate, destRect, data.AsStatic().CachedBackground(), sourceRect); +} + +static void ResetStaticBackground(HWND hWnd, OwnerDrawDialogElement& data) +{ + if (data.AsStatic().CachedBackground()) + DeleteSurfaceObject(data.AsStatic().CachedBackground()); + + ::InvalidateRect(hWnd, nullptr, FALSE); +} + +static void DrawStaticText(HWND hWnd, OwnerDrawDialogElement& data, const RECT& ownerRect) +{ + const auto pText = data.AsStatic().Text(); + if (!pText) + return; + + const auto drawMode = data.AsStatic().DrawMode(); + if (drawMode != WWUIStaticDrawMode::Text && !data.AsStatic().AnimationRunning()) + return; + + const LONG windowStyle = ::GetWindowLongA(hWnd, GWL_STYLE); + int textDrawStyle = 16; + if (windowStyle & SS_CENTER) + textDrawStyle = 17; + else if (windowStyle & SS_RIGHT) + textDrawStyle = 18; + + const COLORREF textColor = (windowStyle & WS_DISABLED) + ? Phobos::UI::ColorDisabledLabel + : data.AsStatic().TextColor(); + + RECT textRect = ownerRect; + OwnerDraw::DrawWideText( + DSurface::Alternate, + pText, + &textRect, + data.AsStatic().Font(), + textColor, + textDrawStyle, + data.AsStatic().TextFlags(), + 0, + data.AsStatic().TextRevealCount(), + data.AsStatic().ColorAdjust()); + + if (data.AsStatic().SoundIndex() != -1) + VocClass::PlayGlobal(data.AsStatic().SoundIndex(), 0x2000, 1.0f); + + if (drawMode == WWUIStaticDrawMode::TypewriterText) + { + const int revealLimit = static_cast(std::wcslen(pText)) + data.AsStatic().ColorAdjust() + 1; + if (data.AsStatic().TextRevealCount() < revealLimit) + { + data.AsStatic().TextRevealCount() += data.AsStatic().TextRevealStep(); + if (data.AsStatic().TextRevealCount() >= revealLimit) + ::KillTimer(hWnd, 0); + } + } +} + +static bool DrawStaticPCX(HWND hWnd, OwnerDrawDialogElement& data, RectangleStruct imageRect) +{ + auto pImage = static_cast(data.AsStatic().ImageSurface()); + if (!pImage) + return false; + + const int imageWidth = pImage->GetWidth(); + const int imageHeight = pImage->GetHeight(); + if (imageRect.Width > imageWidth) + { + imageRect.X += (imageRect.Width - imageWidth) / 2; + imageRect.Width = imageWidth; + } + + if (imageRect.Height > imageHeight) + { + imageRect.Y += (imageRect.Height - imageHeight) / 2; + imageRect.Height = imageHeight; + } + + PCX::Instance.BlitToSurface( + &imageRect, + DSurface::Alternate, + pImage, + static_cast(ConvertRGBToSurfaceColor(RGB(255, 0, 255)))); + + ::ValidateRect(hWnd, nullptr); + return true; +} + +static void DrawStaticShape(HWND hWnd, OwnerDrawDialogElement& data, const RectangleStruct& imageRect, bool animate) +{ + auto pShape = data.AsStatic().Shape(); + if (!pShape) + return; + + int shapeX = imageRect.X; + int shapeY = imageRect.Y; + if (imageRect.Width > pShape->Width) + shapeX += (imageRect.Width - pShape->Width) / 2; + + if (imageRect.Height > pShape->Height) + shapeY += (imageRect.Height - pShape->Height) / 2; + + Point2D position { shapeX, shapeY }; + RectangleStruct bounds = DSurface::Alternate->GetRect(); + const int currentFrame = data.AsStatic().CurrentFrame(); + CC_Draw_Shape( + DSurface::Alternate, + data.AsStatic().ShapeDrawer(), + pShape, + currentFrame, + &position, + &bounds, + BlitterFlags::bf_400, + 0, + 0, + ZGradient::Ground, + 1000, + 0, + nullptr, + 0, + 0, + 0); + + if (animate && data.AsStatic().AnimationRunning()) + { + int nextFrame = currentFrame + 1; + if (nextFrame >= data.AsStatic().FrameCount()) + nextFrame = 0; + + data.AsStatic().CurrentFrame() = nextFrame; + if (data.AsStatic().FrameNotifyHwnd()) + { + ::SendMessageA(data.AsStatic().FrameNotifyHwnd(), WW_STATIC_ANIMFRAMENOTIFY, nextFrame, reinterpret_cast(hWnd)); + ::ValidateRect(hWnd, nullptr); + } + } +} + +static LRESULT PaintStatic(HWND hWnd, OwnerDrawDialogElement& data) +{ + if (data.AsStatic().SuppressPaint() || data.AsStatic().MovieHandle()) + { + ::ValidateRect(hWnd, nullptr); + return 0; + } + + EnsureStaticBackground(hWnd, data); + + RECT ownerRect {}; + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + if (data.AsStatic().FillBackground()) + { + RectangleStruct fillRect + { + ownerRect.left, + ownerRect.top, + ownerRect.right - ownerRect.left, + ownerRect.bottom - ownerRect.top + }; + DSurface::Alternate->FillRect(&fillRect, ConvertRGBToSurfaceColor(data.AsStatic().FillColor())); + } + + const auto drawMode = data.AsStatic().DrawMode(); + if (drawMode < WWUIStaticDrawMode::PCX) + { + DrawStaticText(hWnd, data, ownerRect); + } + else if ((drawMode == WWUIStaticDrawMode::PCX + || drawMode == WWUIStaticDrawMode::Shape + || drawMode == WWUIStaticDrawMode::AnimatedShape) + && static_cast(::GetTickCount() - data.AsStatic().LastFrameTick()) > data.AsStatic().FrameDelayMs()) + { + RestoreStaticBackground(hWnd, data, false); + + RectangleStruct imageRect + { + ownerRect.left, + ownerRect.top, + ownerRect.right - ownerRect.left, + ownerRect.bottom - ownerRect.top + }; + + if (drawMode == WWUIStaticDrawMode::PCX) + { + if (DrawStaticPCX(hWnd, data, imageRect)) + return 0; + } + else + { + DrawStaticShape(hWnd, data, imageRect, drawMode == WWUIStaticDrawMode::AnimatedShape); + } + } + + ::ValidateRect(hWnd, nullptr); + return 0; +} + +static void DestroyStaticResources(HWND hWnd, OwnerDrawDialogElement& data) +{ + if (data.AsStatic().CachedBackground()) + DeleteSurfaceObject(data.AsStatic().CachedBackground()); + + if (data.AsStatic().OwnsShape() && data.AsStatic().Shape()) + { + YRMemory::Deallocate(data.AsStatic().Shape()); + data.AsStatic().Shape() = nullptr; + } + + const auto drawMode = data.AsStatic().DrawMode(); + if ((drawMode == WWUIStaticDrawMode::TypewriterText && data.AsStatic().AnimationRunning()) + || drawMode == WWUIStaticDrawMode::AnimatedShape) + { + ::KillTimer(hWnd, 0); + } + + DestroyStaticMovie(data); + ::KillTimer(hWnd, 0x65); + DestroyStaticMovieAux(data); +} + +static LRESULT HandleStaticMovieTimer(HWND hWnd, OwnerDrawDialogElement& data) +{ + auto pMovie = data.AsStatic().MovieHandle(); + if (!pMovie) + return 0; + + if (pMovie->VTable && pMovie->VTable->AdvanceFrame && pMovie->VTable->AdvanceFrame(pMovie)) + ::InvalidateRect(hWnd, nullptr, FALSE); + + if (pMovie->VTable && pMovie->VTable->FramesLeft && pMovie->VTable->FramesLeft(pMovie)) + { + if (data.AsStatic().LoopMovie()) + { + if (pMovie->VTable->SeekToFrame) + pMovie->VTable->SeekToFrame(pMovie, 1); + + return 0; + } + + DetachStaticMovie(hWnd, data); + } + + return 0; +} + +static LRESULT HandleStaticVisualTimer(HWND hWnd, OwnerDrawDialogElement& data) +{ + const auto drawMode = data.AsStatic().DrawMode(); + if (drawMode == WWUIStaticDrawMode::TypewriterText) + { + ::InvalidateRect(hWnd, nullptr, TRUE); + return 0; + } + + const bool singleShotVisualTimer = drawMode == WWUIStaticDrawMode::PCX || drawMode == WWUIStaticDrawMode::Shape; + const bool animatedShapeTimer = drawMode == WWUIStaticDrawMode::AnimatedShape; + if (!singleShotVisualTimer && !animatedShapeTimer) + return 0; + + if (static_cast(::GetTickCount() - data.AsStatic().LastFrameTick()) > data.AsStatic().FrameDelayMs()) + { + if (singleShotVisualTimer || (animatedShapeTimer && data.AsStatic().AnimationRunning())) + { + ::InvalidateRect(hWnd, nullptr, TRUE); + if (singleShotVisualTimer) + { + ::KillTimer(hWnd, 0); + return 0; + } + } + + if (!data.AsStatic().FrameCount()) + { + ::KillTimer(hWnd, 0); + if (auto pShape = data.AsStatic().Shape()) + { + data.AsStatic().FrameCount() = pShape->Frames; + ::SetTimer(hWnd, 0, pShape->Frames, nullptr); + } + } + } + + return 0; +} + +LRESULT CALLBACK WWUI::StaticCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto pData = FindOwnerDrawData(hWnd); + if (!pData) + return 0; + + auto& data = *pData; + auto forwardOriginal = [&]() -> LRESULT + { + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + }; + + switch (message) + { + case WW_INITDIALOG: + data.AsStatic().DrawMode() = WWUIStaticDrawMode::Text; + data.AsStatic().TextFlags() = 12; + data.AsStatic().TextColor() = Phobos::UI::ColorTextLabel; + return 0; + + case WM_PAINT: + return PaintStatic(hWnd, data); + + case WM_MOVE: + case WM_SIZE: + case WM_WINDOWPOSCHANGED: + ResetStaticBackground(hWnd, data); + return 0; + + case WM_DESTROY: + DestroyStaticResources(hWnd, data); + return forwardOriginal(); + + case WM_TIMER: + if (wParam == 0x65) + return HandleStaticMovieTimer(hWnd, data); + + return HandleStaticVisualTimer(hWnd, data); + + case WW_SETCOLOR: + { + const COLORREF newTextColor = lParam == static_cast(-1) + ? Phobos::UI::ColorTextLabel + : static_cast(lParam); + + if (newTextColor != data.AsStatic().TextColor()) + ::InvalidateRect(hWnd, nullptr, FALSE); + + data.AsStatic().TextColor() = newTextColor; + return 0; + } + + case WW_SETFILLCOLOR: + data.AsStatic().FillBackground() = true; + data.AsStatic().FillColor() = static_cast(lParam); + return 0; + + case WW_SETTEXTW: + case WW_SETTEXTA: + RestoreStaticBackground(hWnd, data, true); + if (data.AsStatic().CachedBackground()) + ::InvalidateRect(hWnd, nullptr, FALSE); + + return 1; + + case WW_RESETANIMTIMER: + if (!data.AsStatic().Shape() + || data.AsStatic().DrawMode() != WWUIStaticDrawMode::AnimatedShape + || data.AsStatic().AnimationRunning()) + { + return 0; + } + + data.AsStatic().AnimationRunning() = true; + ::SetTimer(hWnd, 0, GetStaticAnimationTimerInterval(::GetParent(hWnd), hWnd), nullptr); + return 0; + + case WW_STATIC_STOPANIM: + if (data.AsStatic().DrawMode() == WWUIStaticDrawMode::AnimatedShape && data.AsStatic().AnimationRunning()) + { + ::KillTimer(hWnd, 0); + data.AsStatic().AnimationRunning() = false; + } + return 0; + + case WW_STATIC_SETANIMFRAME: + if (data.AsStatic().DrawMode() == WWUIStaticDrawMode::AnimatedShape) + { + data.AsStatic().CurrentFrame() = static_cast(lParam); + ::InvalidateRect(hWnd, nullptr, TRUE); + } + return 0; + + case WW_STATIC_GETANIMFRAME: + return data.AsStatic().DrawMode() == WWUIStaticDrawMode::AnimatedShape ? data.AsStatic().CurrentFrame() : -1; + + case WW_STATIC_SETANIMFRAMENOTIFYHWND: + if (data.AsStatic().DrawMode() == WWUIStaticDrawMode::AnimatedShape) + data.AsStatic().FrameNotifyHwnd() = reinterpret_cast(lParam); + return 0; + + case WW_STATIC_SETCURRENTMOVIEBYINDEX: + if (const char* pMovieName = GetStaticMovieName(static_cast(wParam))) + return LoadStaticMovie(hWnd, data, pMovieName); + + return 0; + + case WW_STATIC_SETCURRENTMOVIEBYNAME: + return LoadStaticMovie(hWnd, data, reinterpret_cast(lParam)); + + case WW_STATIC_PAUSEMOVIE: + if (auto pMovie = data.AsStatic().MovieHandle()) + { + if (pMovie->VTable && pMovie->VTable->Pause) + pMovie->VTable->Pause(pMovie, 1); + } + return 0; + + case WW_STATIC_CONTINUEMOVIE: + if (auto pMovie = data.AsStatic().MovieHandle()) + { + if (pMovie->VTable && pMovie->VTable->Pause) + pMovie->VTable->Pause(pMovie, 0); + } + return 0; + + case WW_STATIC_DETACHMOVIE: + DetachStaticMovie(hWnd, data); + return 0; + + case WW_STATIC_SETLOOPMOVIE: + data.AsStatic().LoopMovie() = static_cast(wParam); + return 0; + + case WW_STATIC_REVEALTEXTS: + if (data.AsStatic().DrawMode() != WWUIStaticDrawMode::TypewriterText || data.AsStatic().AnimationRunning()) + return 0; + + data.AsStatic().AnimationRunning() = true; + data.AsStatic().TextRevealCount() = 1; + ::SetTimer(hWnd, 0, data.AsStatic().TextRevealDelay(), nullptr); + ::InvalidateRect(hWnd, nullptr, FALSE); + return 0; + + case WW_STATIC_BLITMOVIE: + if (auto pMovie = data.AsStatic().MovieHandle()) + { + if (pMovie->VTable && pMovie->VTable->Blit) + pMovie->VTable->Blit(pMovie); + } + return 0; + + default: + return forwardOriginal(); + } +} diff --git a/src/OwnerDraw/Tab.cpp b/src/OwnerDraw/Tab.cpp new file mode 100644 index 0000000000..469c400754 --- /dev/null +++ b/src/OwnerDraw/Tab.cpp @@ -0,0 +1,291 @@ +#include "OwnerDraw.Internal.h" + +static void DrawTabFrameTiledPCX(const char* pFilename, const RectangleStruct& rect) +{ + if (auto pPCX = GetPCXSurface(pFilename)) + BlitTiledPCX(rect, DSurface::Alternate, pPCX, 0, 0); +} + +static void DrawTabFrameCornerPCX(const char* pFilename, int x, int y) +{ + auto pPCX = GetPCXSurface(pFilename); + if (!pPCX) + return; + + RectangleStruct destRect { x, y, pPCX->GetWidth(), pPCX->GetHeight() }; + RectangleStruct sourceRect { 0, 0, pPCX->GetWidth(), pPCX->GetHeight() }; + CopySurfacePart(DSurface::Alternate, destRect, pPCX, sourceRect); +} + +static void DrawTabFrame(const RECT& panelRect) +{ + const int width = panelRect.right - panelRect.left; + const int height = panelRect.bottom - panelRect.top; + if (width <= 0 || height <= 0) + return; + + if (auto pLeft = GetPCXSurface("tab_fml.pcx")) + { + RectangleStruct rect { panelRect.left, panelRect.top, pLeft->GetWidth(), height }; + DrawTabFrameTiledPCX("tab_fml.pcx", rect); + } + + if (auto pRight = GetPCXSurface("tab_fmr.pcx")) + { + const int rightWidth = pRight->GetWidth(); + RectangleStruct rect { panelRect.right - rightWidth, panelRect.top, rightWidth, height }; + DrawTabFrameTiledPCX("tab_fmr.pcx", rect); + } + + if (auto pTop = GetPCXSurface("tab_ftm.pcx")) + { + RectangleStruct rect { panelRect.left, panelRect.top, width, pTop->GetHeight() }; + DrawTabFrameTiledPCX("tab_ftm.pcx", rect); + } + + if (auto pBottom = GetPCXSurface("tab_fbm.pcx")) + { + const int bottomHeight = pBottom->GetHeight(); + RectangleStruct rect { panelRect.left, panelRect.bottom - bottomHeight, width, bottomHeight }; + DrawTabFrameTiledPCX("tab_fbm.pcx", rect); + } + + DrawTabFrameCornerPCX("tab_ftl.pcx", panelRect.left, panelRect.top); + + if (auto pTopRight = GetPCXSurface("tab_ftr.pcx")) + DrawTabFrameCornerPCX("tab_ftr.pcx", panelRect.right - pTopRight->GetWidth(), panelRect.top); + + if (auto pBottomLeft = GetPCXSurface("tab_fbl.pcx")) + DrawTabFrameCornerPCX("tab_fbl.pcx", panelRect.left, panelRect.bottom - pBottomLeft->GetHeight()); + + if (auto pBottomRight = GetPCXSurface("tab_fbr.pcx")) + { + DrawTabFrameCornerPCX( + "tab_fbr.pcx", + panelRect.right - pBottomRight->GetWidth(), + panelRect.bottom - pBottomRight->GetHeight()); + } +} + +static void DrawTabTransparentPCX(const char* pFilename, const RectangleStruct& rect) +{ + auto pPCX = GetPCXSurface(pFilename); + if (!pPCX) + return; + + RectangleStruct blitRect { rect.X, rect.Y, pPCX->GetWidth(), pPCX->GetHeight() }; + PCX::Instance.BlitToSurface( + &blitRect, + DSurface::Alternate, + pPCX, + static_cast(ConvertRGBToSurfaceColor(RGB(255, 0, 255)))); +} + +static void DrawTabItemMiddle(char variant, int drawLeft, int drawTop, int drawWidth) +{ + auto pLeftCapBase = GetPCXSurface("tab_tlu.pcx"); + if (!pLeftCapBase) + return; + + char filename[32] {}; + std::snprintf(filename, sizeof(filename), "tab_tm%c.pcx", variant); + + auto pMiddle = GetPCXSurface(filename); + if (!pMiddle) + return; + + const int capWidth = pLeftCapBase->GetWidth(); + RectangleStruct rect + { + drawLeft + capWidth, + drawTop, + drawWidth - 2 * capWidth, + pMiddle->GetHeight() + }; + + DrawTabFrameTiledPCX(filename, rect); +} + +static void DrawTabItemCaps(char variant, int drawLeft, int drawTop, int drawWidth) +{ + char filename[32] {}; + std::snprintf(filename, sizeof(filename), "tab_tl%c.pcx", variant); + DrawTabTransparentPCX(filename, { drawLeft, drawTop, 0, 0 }); + + std::snprintf(filename, sizeof(filename), "tab_tr%c.pcx", variant); + if (auto pRightCap = GetPCXSurface(filename)) + { + DrawTabTransparentPCX( + filename, + { drawLeft + drawWidth - pRightCap->GetWidth(), drawTop, 0, 0 }); + } +} + +static void DrawTabItemText( + HWND hWnd, + OwnerDrawDialogElement& data, + int tabIndex, + bool selected, + int drawLeft, + int drawTop, + int drawWidth, + int drawHeight) +{ + char title[64] {}; + std::snprintf(title, sizeof(title), "Title"); + + TCITEMA item {}; + item.mask = TCIF_TEXT; + item.pszText = title; + item.cchTextMax = static_cast(std::size(title)); + ::SendMessageA(hWnd, TCM_GETITEMA, tabIndex, reinterpret_cast(&item)); + title[std::size(title) - 1] = '\0'; + + wchar_t wideTitle[64] {}; + std::swprintf(wideTitle, std::size(wideTitle), L"%hs", item.pszText ? item.pszText : title); + + RECT textRect + { + drawLeft, + drawTop + 6, + drawLeft + drawWidth, + drawTop + drawHeight + }; + + OwnerDraw::DrawWideText( + DSurface::Alternate, + wideTitle, + &textRect, + data.AsTab().Font(), + selected ? Phobos::UI::ColorText : OwnerDraw::SelectedTabTextColor, + 5, + 12, + 0, + 0, + 0); +} + +static void DrawTabItem(HWND hWnd, OwnerDrawDialogElement& data, const RECT& controlRect, int tabIndex, int selectedIndex, RECT& itemRect) +{ + const char variant = tabIndex == selectedIndex ? 'u' : 'd'; + const int leftOverlap = itemRect.left < 6 ? itemRect.left : 6; + const int drawLeft = controlRect.left + itemRect.left - leftOverlap; + const int drawTop = controlRect.top + itemRect.top; + const int drawWidth = itemRect.right + leftOverlap - itemRect.left; + const int drawHeight = itemRect.bottom - itemRect.top; + + DrawTabItemMiddle(variant, drawLeft, drawTop, drawWidth); + DrawTabItemCaps(variant, drawLeft, drawTop, drawWidth); + DrawTabItemText(hWnd, data, tabIndex, tabIndex == selectedIndex, drawLeft, drawTop, drawWidth, drawHeight); + ::ValidateRect(hWnd, &itemRect); +} + +static void InitializeTabCtrl(HWND hWnd) +{ + RECT controlRect {}; + OwnerDraw::GetRectangle(hWnd, &controlRect); + + int tabHeight = 0; + if (auto pLeftUp = GetPCXSurface("tab_tlu.pcx")) + tabHeight = std::max(pLeftUp->GetHeight() - 1, 0); + + ::SendMessageA(hWnd, TCM_SETITEMSIZE, 0, MAKELPARAM(0x59, tabHeight)); +} + +static LRESULT PaintTabCtrl(HWND hWnd, OwnerDrawDialogElement& data) +{ + RECT controlRect {}; + OwnerDraw::GetRectangle(hWnd, &controlRect); + + const int itemCount = static_cast(::SendMessageA(hWnd, TCM_GETITEMCOUNT, 0, 0)); + + RECT firstItemRect {}; + ::SendMessageA(hWnd, TCM_GETITEMRECT, 0, reinterpret_cast(&firstItemRect)); + ::SendMessageA(hWnd, TCM_GETITEMRECT, 0, reinterpret_cast(&firstItemRect)); + + RECT panelRect = controlRect; + panelRect.top += firstItemRect.bottom - firstItemRect.top + 3; + + RectangleStruct dimRect + { + panelRect.left, + panelRect.top, + panelRect.right - panelRect.left, + panelRect.bottom - panelRect.top + }; + OwnerDraw::CopyDimmedBackground(&dimRect, hWnd, 0); + ::ValidateRect(hWnd, nullptr); + + DrawTabFrame(panelRect); + + if (itemCount <= 0) + { + ::ValidateRect(hWnd, nullptr); + return 0; + } + + int selectedIndex = static_cast(::SendMessageA(hWnd, TCM_GETCURSEL, 0, 0)); + if (selectedIndex < 0 || selectedIndex >= itemCount) + selectedIndex = 0; + + OwnerDraw::GetRectangle(hWnd, &controlRect); + + int tabIndex = selectedIndex + 1; + if (tabIndex >= itemCount) + tabIndex = 0; + + for (int drawnCount = 0; drawnCount < itemCount; ++drawnCount) + { + RECT itemRect {}; + if (!::SendMessageA(hWnd, TCM_GETITEMRECT, tabIndex, reinterpret_cast(&itemRect))) + { + tabIndex = 0; + if (!::SendMessageA(hWnd, TCM_GETITEMRECT, tabIndex, reinterpret_cast(&itemRect))) + break; + } + + DrawTabItem(hWnd, data, controlRect, tabIndex, selectedIndex, itemRect); + + if (tabIndex == selectedIndex) + break; + + ++tabIndex; + if (tabIndex >= itemCount) + tabIndex = 0; + } + + ::ValidateRect(hWnd, nullptr); + return 0; +} + +LRESULT CALLBACK WWUI::TabCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); + auto forwardOriginal = [&]() -> LRESULT + { + return CallSelectedHandler(pOriginalWndProc, hWnd, message, wParam, lParam); + }; + + switch (message) + { + case WM_ERASEBKGND: + return 1; + + case WM_NCPAINT: + return 0; + + case WW_INITDIALOG: + InitializeTabCtrl(hWnd); + return forwardOriginal(); + + case WM_PAINT: + if (auto pData = FindOwnerDrawData(hWnd)) + return PaintTabCtrl(hWnd, *pData); + + ::ValidateRect(hWnd, nullptr); + return 0; + + default: + return forwardOriginal(); + } +} diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp index 8c450eb4d9..93fdb50391 100644 --- a/src/Phobos.INI.cpp +++ b/src/Phobos.INI.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -42,6 +43,32 @@ int Phobos::UI::SuperWeaponSidebar_Max = 0; int Phobos::UI::SuperWeaponSidebar_MaxColumns = INT32_MAX; bool Phobos::UI::WeedsCounter_Show = false; bool Phobos::UI::AnchoredToolTips = false; +COLORREF Phobos::UI::ColorText = 0xFFFF; +COLORREF Phobos::UI::ColorTextButton = 0xFFFF; +COLORREF Phobos::UI::ColorTextCheckbox = 0xFFFF; +COLORREF Phobos::UI::ColorTextRadio = 0xFFFF; +COLORREF Phobos::UI::ColorTextLabel = 0xFFFF; +COLORREF Phobos::UI::ColorTextList = 0xFFFF; +COLORREF Phobos::UI::ColorTextCombobox = 0xFFFF; +COLORREF Phobos::UI::ColorTextGroupbox = 0xFFFF; +COLORREF Phobos::UI::ColorTextSlider = 0xFFFF; +COLORREF Phobos::UI::ColorTextEdit = 0xFFFF; +COLORREF Phobos::UI::ColorTextObserver = 0xEEEEEE; +COLORREF Phobos::UI::ColorCaret = 0xFFFF; +COLORREF Phobos::UI::ColorSelection = 0xFF; +COLORREF Phobos::UI::ColorSelectionCombobox = 0xFF; +COLORREF Phobos::UI::ColorSelectionList = 0xFF; +COLORREF Phobos::UI::ColorSelectionObserver = 0x626262; +COLORREF Phobos::UI::ColorBorder1 = 0xC5BEA7; +COLORREF Phobos::UI::ColorBorder2 = 0x807A68; +COLORREF Phobos::UI::ColorDisabled = 0x9F; +COLORREF Phobos::UI::ColorDisabledLabel = 0x9F; +COLORREF Phobos::UI::ColorDisabledButton = 0xA7; +COLORREF Phobos::UI::ColorDisabledCombobox = 0x9F; +COLORREF Phobos::UI::ColorDisabledCheckbox = 0x9F; +COLORREF Phobos::UI::ColorDisabledSlider = 0x9F; +COLORREF Phobos::UI::ColorDisabledList = 0x9F; +COLORREF Phobos::UI::ColorDisabledObserver = 0x8F8F8F; bool Phobos::Config::ToolTipDescriptions = true; bool Phobos::Config::ToolTipBlur = false; @@ -84,6 +111,19 @@ int Phobos::Misc::CustomGS_ChangeInterval[7] = { -1, -1, -1, -1, -1, -1, -1 }; int Phobos::Misc::CustomGS_ChangeDelay[7] = { 0, 1, 2, 3, 4, 5, 6 }; int Phobos::Misc::CustomGS_DefaultDelay[7] = { 0, 1, 2, 3, 4, 5, 6 }; +namespace +{ + COLORREF ReadUIColor(CCINIClass& ini, const char* key, COLORREF defaultColor) + { + const ColorStruct fallback( + static_cast(defaultColor & 0xFF), + static_cast((defaultColor >> 8) & 0xFF), + static_cast((defaultColor >> 16) & 0xFF)); + const ColorStruct color = ini.ReadColor(UISETTINGS_SECTION, key, fallback); + return color.R | (color.G << 8) | (color.B << 16); + } +} + DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) { const auto phobosSection = "Phobos"; @@ -235,6 +275,33 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) // UISettings { + Phobos::UI::ColorText = ReadUIColor(ini_uimd, "Color.Text", 0xFFFF); + Phobos::UI::ColorTextButton = ReadUIColor(ini_uimd, "Color.Button.Text", Phobos::UI::ColorText); + Phobos::UI::ColorTextRadio = ReadUIColor(ini_uimd, "Color.Radio.Text", Phobos::UI::ColorText); + Phobos::UI::ColorTextCheckbox = ReadUIColor(ini_uimd, "Color.Checkbox.Text", Phobos::UI::ColorText); + Phobos::UI::ColorTextLabel = ReadUIColor(ini_uimd, "Color.Label.Text", Phobos::UI::ColorText); + Phobos::UI::ColorTextList = ReadUIColor(ini_uimd, "Color.List.Text", Phobos::UI::ColorText); + Phobos::UI::ColorTextCombobox = ReadUIColor(ini_uimd, "Color.Combobox.Text", Phobos::UI::ColorText); + Phobos::UI::ColorTextGroupbox = ReadUIColor(ini_uimd, "Color.Groupbox.Text", Phobos::UI::ColorText); + Phobos::UI::ColorTextSlider = ReadUIColor(ini_uimd, "Color.Slider.Text", Phobos::UI::ColorText); + Phobos::UI::ColorTextEdit = ReadUIColor(ini_uimd, "Color.Edit.Text", Phobos::UI::ColorText); + Phobos::UI::ColorTextObserver = ReadUIColor(ini_uimd, "Color.Observer.Text", 0xEEEEEE); + Phobos::UI::ColorCaret = ReadUIColor(ini_uimd, "Color.Caret", 0xFFFF); + Phobos::UI::ColorSelection = ReadUIColor(ini_uimd, "Color.Selection", 0xFF); + Phobos::UI::ColorSelectionCombobox = ReadUIColor(ini_uimd, "Color.Combobox.Selection", Phobos::UI::ColorSelection); + Phobos::UI::ColorSelectionList = ReadUIColor(ini_uimd, "Color.List.Selection", Phobos::UI::ColorSelection); + Phobos::UI::ColorSelectionObserver = ReadUIColor(ini_uimd, "Color.Observer.Selection", 0x626262); + Phobos::UI::ColorBorder1 = ReadUIColor(ini_uimd, "Color.Border1", 0xC5BEA7); + Phobos::UI::ColorBorder2 = ReadUIColor(ini_uimd, "Color.Border2", 0x807A68); + Phobos::UI::ColorDisabled = ReadUIColor(ini_uimd, "Color.Disabled", 0x9F); + Phobos::UI::ColorDisabledLabel = ReadUIColor(ini_uimd, "Color.Label.Disabled", Phobos::UI::ColorDisabled); + Phobos::UI::ColorDisabledCombobox = ReadUIColor(ini_uimd, "Color.Combobox.Disabled", Phobos::UI::ColorDisabled); + Phobos::UI::ColorDisabledSlider = ReadUIColor(ini_uimd, "Color.Slider.Disabled", Phobos::UI::ColorDisabled); + Phobos::UI::ColorDisabledButton = ReadUIColor(ini_uimd, "Color.Button.Disabled", 0xA7); + Phobos::UI::ColorDisabledCheckbox = ReadUIColor(ini_uimd, "Color.Checkbox.Disabled", Phobos::UI::ColorDisabled); + Phobos::UI::ColorDisabledList = ReadUIColor(ini_uimd, "Color.List.Disabled", Phobos::UI::ColorDisabled); + Phobos::UI::ColorDisabledObserver = ReadUIColor(ini_uimd, "Color.Observer.Disabled", 0x8F8F8F); + ini_uimd.ReadString(UISETTINGS_SECTION, "ShowBriefingResumeButtonLabel", "GUI:Resume", Phobos::readBuffer); Phobos::UI::ShowBriefingResumeButtonLabel = GeneralUtils::LoadStringOrDefault(Phobos::readBuffer, L""); diff --git a/src/Phobos.h b/src/Phobos.h index 1ec2645d83..9dab9ef2d9 100644 --- a/src/Phobos.h +++ b/src/Phobos.h @@ -64,6 +64,32 @@ class Phobos static int SuperWeaponSidebar_MaxColumns; static bool WeedsCounter_Show; static bool AnchoredToolTips; + static COLORREF ColorText; + static COLORREF ColorTextButton; + static COLORREF ColorTextCheckbox; + static COLORREF ColorTextRadio; + static COLORREF ColorTextLabel; + static COLORREF ColorTextList; + static COLORREF ColorTextCombobox; + static COLORREF ColorTextGroupbox; + static COLORREF ColorTextSlider; + static COLORREF ColorTextEdit; + static COLORREF ColorTextObserver; + static COLORREF ColorCaret; + static COLORREF ColorSelection; + static COLORREF ColorSelectionCombobox; + static COLORREF ColorSelectionList; + static COLORREF ColorSelectionObserver; + static COLORREF ColorBorder1; + static COLORREF ColorBorder2; + static COLORREF ColorDisabled; + static COLORREF ColorDisabledLabel; + static COLORREF ColorDisabledButton; + static COLORREF ColorDisabledCombobox; + static COLORREF ColorDisabledCheckbox; + static COLORREF ColorDisabledSlider; + static COLORREF ColorDisabledList; + static COLORREF ColorDisabledObserver; static const wchar_t* CostLabel; static const wchar_t* PowerLabel; diff --git a/src/Render/Hooks.cpp b/src/Render/Hooks.cpp index 0526b8e7d0..3bb7624bc6 100644 --- a/src/Render/Hooks.cpp +++ b/src/Render/Hooks.cpp @@ -57,20 +57,6 @@ DEFINE_HOOK(0x432FF1, DXRenderUpdateScreenBinkMovieBlitToScreen, 0x7) return 0; } -static LRESULT CALLBACK OwnerDrawWindowProcedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - auto result = reinterpret_cast(0x610CA0)(hWnd, message, wParam, lParam); - if (message == WM_PAINT) - RenderDX::UpdateScreen(DSurface::Primary); - return result; -} -DEFINE_PATCH_TYPED(void*, 0x60FF06, OwnerDrawWindowProcedure); - -DEFINE_HOOK_AGAIN(0x611FB0, DXRenderUpdateScreenOwnerDrawWindow, 0x6); -DEFINE_HOOK(0x61187D, DXRenderUpdateScreenOwnerDrawWindow, 0xA) { - RenderDX::UpdateScreen(DSurface::Primary); - return 0; -} - DEFINE_HOOK(0x7776B5, MainWindowProcWMPaint, 0x6) { RenderDX::MainProcHandlePaint(); return 0x7779B5; diff --git a/src/Render/WWUI.h b/src/Render/WWUI.h new file mode 100644 index 0000000000..74dcbb7eb3 --- /dev/null +++ b/src/Render/WWUI.h @@ -0,0 +1,3 @@ +#pragma once + +#include "../OwnerDraw/OwnerDraw.h" From c15668ec39829c74f5fdadefc88d8c94bc7f5b42 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Thu, 28 May 2026 03:32:06 +0800 Subject: [PATCH 13/21] Fix ProgressScreenClass crash --- src/Render/Surface.Hooks.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Render/Surface.Hooks.cpp b/src/Render/Surface.Hooks.cpp index aadf56bf59..23ff16e970 100644 --- a/src/Render/Surface.Hooks.cpp +++ b/src/Render/Surface.Hooks.cpp @@ -6,6 +6,12 @@ DEFINE_FUNCTION_JUMP(LJMP, 0x4BA770, DXSurface::CreatePrimary); DEFINE_JUMP(LJMP, 0x77747A, 0x777575); // Skip Restore_Check. +static bool __fastcall DSurface_IsSurfaceLost(DSurface*, void*) // Always return false to prevent surface lost state +{ + return false; +} +DEFINE_FUNCTION_JUMP(LJMP, 0x4BAFE0, DSurface_IsSurfaceLost); + static DXSurface* __fastcall DXSurfaceCtor(DXSurface* pSurface, void*, int width, int height, bool systemMem, bool enable3D) { return new(pSurface) DXSurface(width, height); } From a717ee0302493e2dcbb624b909ed3546860acdff Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Thu, 28 May 2026 04:32:07 +0800 Subject: [PATCH 14/21] Implement DXSurface::CopyFrom for scaled copy --- src/Render/Surface.cpp | 71 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/src/Render/Surface.cpp b/src/Render/Surface.cpp index 7283f8b522..17790ea415 100644 --- a/src/Render/Surface.cpp +++ b/src/Render/Surface.cpp @@ -4,6 +4,7 @@ #include #include +#include #include @@ -43,8 +44,74 @@ bool DXSurface::CopyFromPart(RectangleStruct* pClipRect, Surface* pSrc, Rectangl return DXSurface::CopyFrom(&destWindow, pClipRect, pSrc, &sourceWindow, pSrcRect, transparent, false); } -bool DXSurface::CopyFrom(RectangleStruct* pClipRect, RectangleStruct* pClipRect2, Surface* pSrc, RectangleStruct* pDestRect, RectangleStruct* pSrcRect, bool transparent, bool) { - JMP_THIS(0x7BBCF0); +bool DXSurface::CopyFrom(RectangleStruct* dcliprect, RectangleStruct* destrect, Surface* source, RectangleStruct* scliprect, RectangleStruct* sourcerect, bool trans, bool) { + if (!source->IsDSurface()) + { + // If source is not a DXSurface, then use vanilla XSurface handler + return reinterpret_cast(0x7BBB90)(this, destrect, source, sourcerect, trans, true); + } + + if (trans) + { + // XSurface original routine + RectangleStruct drect = *destrect; + RectangleStruct srect = *sourcerect; + if (!Drawing::BlitClip(drect, *dcliprect, srect, *scliprect)) + return false; + + BlitTrans blitter; + return Drawing::BitBlit(this, &drect, source, &srect, &blitter, 0, ZGradient::Deg135, 1000, 0); + } + + // Handle the untransparent case ourselves, supporting scaling + RectangleStruct srect = *sourcerect; + RectangleStruct drect = *destrect; + + RectangleStruct swindow = Drawing::Intersect(*scliprect, source->GetRect()); + RectangleStruct dwindow = Drawing::Intersect(*dcliprect, Surface::GetRect()); + + if (!Drawing::BlitClip(drect, dwindow, srect, swindow)) + return false; + + RectangleStruct src { srect.X + swindow.X, srect.Y + swindow.Y, srect.Width, srect.Height }; + RectangleStruct dst { drect.X + dwindow.X, drect.Y + dwindow.Y, drect.Width, drect.Height }; + if (src.Width <= 0 || src.Height <= 0 || dst.Width <= 0 || dst.Height <= 0) + return false; + + auto src_ptr = reinterpret_cast(source)->RawLock(src.X, src.Y); + auto dst_ptr = this->RawLock(dst.X, dst.Y); + const auto src_pitch = reinterpret_cast(source)->GetPitch(); + const auto dst_pitch = GetPitch(); + + // If the source and destination rectangles are the same size, we can do a simple copy. + if (src.Width == dst.Width && src.Height == dst.Height) + { + for (int y = 0; y < dst.Height; ++y) + { + std::copy_n(reinterpret_cast(src_ptr), dst.Width, reinterpret_cast(dst_ptr)); + src_ptr = reinterpret_cast(src_ptr) + src_pitch; + dst_ptr = reinterpret_cast(dst_ptr) + dst_pitch; + } + return true; + } + + // Otherwise we need to scale the source to fit the destination. + for (int y = 0; y < dst.Height; ++y) + { + const auto srcY = static_cast(static_cast(y) * src.Height / dst.Height); + auto srcRow = reinterpret_cast(src_ptr + srcY * src_pitch); + auto dstRow = reinterpret_cast(dst_ptr); + + for (int x = 0; x < dst.Width; ++x) + { + const auto srcX = static_cast(static_cast(x) * src.Width / dst.Width); + dstRow[x] = srcRow[srcX]; + } + + dst_ptr = reinterpret_cast(dst_ptr) + dst_pitch; + } + + return true; } bool DXSurface::FillRectEx(RectangleStruct* pClipRect, RectangleStruct* pFillRect, COLORREF nColor) { From 4a0ccdb90bce29767b1ea5768da18e328beffde4 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Thu, 28 May 2026 04:45:01 +0800 Subject: [PATCH 15/21] Improve DXSurface scaled copy initialization Zero-initialize newly allocated DXSurface buffers and switch scaled CopyFrom blits to fixed-point nearest-neighbor sampling. --- src/Render/Surface.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Render/Surface.cpp b/src/Render/Surface.cpp index 17790ea415..f8d10f10b5 100644 --- a/src/Render/Surface.cpp +++ b/src/Render/Surface.cpp @@ -15,6 +15,7 @@ void DXSurface::CTOR(int width, int height) { BytesPerPixel = 2; // 16-bit RGB565 color *InternalGetPitch() = (width * 2 + 256 - 1) & ~(256 - 1); // Keep rows aligned for texture uploads. *InternalGetBuffer() = YRMemory::Allocate(*InternalGetPitch() * height); + std::memset(*InternalGetBuffer(), 0, *InternalGetPitch() * height); } void DXSurface::DTOR() { @@ -96,19 +97,24 @@ bool DXSurface::CopyFrom(RectangleStruct* dcliprect, RectangleStruct* destrect, } // Otherwise we need to scale the source to fit the destination. + const auto incY = (static_cast(src.Height) << 16) / dst.Height; + const auto incX = (static_cast(src.Width) << 16) / dst.Width; + const auto dstGap = dst_pitch - 2 * dst.Width; + auto posY = incY / 2; for (int y = 0; y < dst.Height; ++y) { - const auto srcY = static_cast(static_cast(y) * src.Height / dst.Height); - auto srcRow = reinterpret_cast(src_ptr + srcY * src_pitch); - auto dstRow = reinterpret_cast(dst_ptr); - + const auto srcY = static_cast(posY >> 16); + const auto pSrcRow = src_ptr + srcY * src_pitch; + posY += incY; + auto posX = incX / 2; for (int x = 0; x < dst.Width; ++x) { - const auto srcX = static_cast(static_cast(x) * src.Width / dst.Width); - dstRow[x] = srcRow[srcX]; + const auto srcX = 2 * static_cast(posX >> 16); + posX += incX; + *reinterpret_cast(dst_ptr) = *reinterpret_cast(pSrcRow + srcX); + dst_ptr += 2; } - - dst_ptr = reinterpret_cast(dst_ptr) + dst_pitch; + dst_ptr += dstGap; } return true; From 3d3301d46d1b34d6b92467c18b3bcd2da77f2443 Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Fri, 29 May 2026 02:29:12 +0800 Subject: [PATCH 16/21] Implement always scaling support Vanilla YR should be supported now --- src/Misc/RetryDialog.cpp | 30 -- src/OwnerDraw/Button.cpp | 16 +- src/OwnerDraw/ComboBox.cpp | 182 +++++++--- src/OwnerDraw/ListBox.cpp | 205 +++++++++--- src/OwnerDraw/OwnerDraw.Hooks.cpp | 1 + src/OwnerDraw/OwnerDraw.Internal.h | 5 + src/OwnerDraw/OwnerDraw.cpp | 473 ++++++++++++++++++++++++-- src/OwnerDraw/OwnerDraw.h | 7 + src/OwnerDraw/ScrollBar.cpp | 75 ++++- src/OwnerDraw/Slider.cpp | 101 ++++-- src/OwnerDraw/Static.cpp | 124 ++++++- src/OwnerDraw/Tab.cpp | 15 +- src/Render/Functions.cpp | 518 +++++++++++++++++++++++++++-- src/Render/Functions.h | 23 ++ src/Render/Mouse.cpp | 16 +- src/Render/Surface.cpp | 2 +- src/Render/WWUI.Hooks.cpp | 282 +++++++++++++--- 17 files changed, 1759 insertions(+), 316 deletions(-) diff --git a/src/Misc/RetryDialog.cpp b/src/Misc/RetryDialog.cpp index e8f6945f10..8e4383c274 100644 --- a/src/Misc/RetryDialog.cpp +++ b/src/Misc/RetryDialog.cpp @@ -7,11 +7,6 @@ #include #include -namespace RetryDialogFlag -{ - bool IsCalledFromRetryDialog = false; -} - DEFINE_HOOK(0x686092, DoLose_RetryDialogForCampaigns, 0x7) { enum { OK = 0x6860F6, Cancel = 0x6860EE, LoadGame = 0x686231 }; @@ -39,9 +34,7 @@ DEFINE_HOOK(0x686092, DoLose_RetryDialogForCampaigns, 0x7) case WWMessageBox::Result::Button1: auto pDialog = GameCreate(); - RetryDialogFlag::IsCalledFromRetryDialog = true; const bool bIsAboutToLoad = pDialog->LoadDialog(); - RetryDialogFlag::IsCalledFromRetryDialog = false; GameDelete(pDialog); if (!bIsAboutToLoad) @@ -63,26 +56,3 @@ DEFINE_HOOK(0x686092, DoLose_RetryDialogForCampaigns, 0x7) return LoadGame; } - -DEFINE_HOOK(0x558F4E, LoadOptionClass_Dialog_CenterListBox, 0x5) -{ - if (RetryDialogFlag::IsCalledFromRetryDialog) - { - GET(HWND, hListBox, EAX); - GET(HWND, hDialog, EDI); - - HWND hLoadButton = GetDlgItem(hDialog, 1039); - - RECT buttonRect; - GetWindowRect(hLoadButton, &buttonRect); - - float scaleX = static_cast(buttonRect.right - buttonRect.left) / 108; - float scaleY = static_cast(buttonRect.bottom - buttonRect.top) / 22; - int X = buttonRect.left - static_cast(346 * scaleX); - int Y = buttonRect.top - static_cast(44 * scaleY); - - SetWindowPos(hListBox, NULL, X, Y, NULL, NULL, SWP_NOSIZE | SWP_NOZORDER); - } - - return 0; -} diff --git a/src/OwnerDraw/Button.cpp b/src/OwnerDraw/Button.cpp index e77a564835..af33095f66 100644 --- a/src/OwnerDraw/Button.cpp +++ b/src/OwnerDraw/Button.cpp @@ -309,7 +309,8 @@ static LRESULT PaintOwnerDrawButton(HWND hWnd, OwnerDrawDialogElement& data, LON RECT ownerRect {}; RECT clientRect {}; OwnerDraw::GetRectangle(hWnd, &ownerRect); - ::GetClientRect(hWnd, &clientRect); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect)) + ::GetClientRect(hWnd, &clientRect); const int width = ownerRect.right - ownerRect.left; const int height = ownerRect.bottom - ownerRect.top; @@ -376,7 +377,8 @@ static LRESULT PaintCheckboxCtrl(HWND hWnd, OwnerDrawDialogElement& data, LONG w RECT clientRect {}; RECT textRect {}; RECT ownerRect {}; - ::GetClientRect(hWnd, &clientRect); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect)) + ::GetClientRect(hWnd, &clientRect); OwnerDraw::GetRectangle(hWnd, &textRect); OwnerDraw::GetRectangle(hWnd, &ownerRect); @@ -421,9 +423,10 @@ static LRESULT PaintCheckboxCtrl(HWND hWnd, OwnerDrawDialogElement& data, LONG w return 0; } -static bool IsInsideCheckboxArt(LPARAM lParam) +static bool IsInsideCheckboxArt(HWND hWnd, LPARAM lParam) { - return LOWORD(lParam) < CheckboxArtSize && HIWORD(lParam) < CheckboxArtSize; + const POINT point = RenderDX::MouseLParamToRenderLocalPoint(hWnd, lParam); + return point.x >= 0 && point.y >= 0 && point.x < CheckboxArtSize && point.y < CheckboxArtSize; } static void NotifyCheckboxClicked(HWND hWnd, int checkState) @@ -600,7 +603,8 @@ static LRESULT PaintRadioCtrl(HWND hWnd, OwnerDrawDialogElement& data, LONG wind RECT ownerRect {}; RECT clientRect {}; OwnerDraw::GetRectangle(hWnd, &ownerRect); - ::GetClientRect(hWnd, &clientRect); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect)) + ::GetClientRect(hWnd, &clientRect); const int width = ownerRect.right - ownerRect.left; const int height = ownerRect.bottom - ownerRect.top; @@ -722,7 +726,7 @@ LRESULT CALLBACK WWUI::CheckboxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPAR case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK: - if (!IsInsideCheckboxArt(lParam)) + if (!IsInsideCheckboxArt(hWnd, lParam)) return 0; data.AsCheckbox().CheckState() = data.AsCheckbox().CheckState() == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED; diff --git a/src/OwnerDraw/ComboBox.cpp b/src/OwnerDraw/ComboBox.cpp index d6499da79a..4dba7c15e2 100644 --- a/src/OwnerDraw/ComboBox.cpp +++ b/src/OwnerDraw/ComboBox.cpp @@ -15,6 +15,8 @@ static void SyncComboDropSelectionColor() constexpr int ComboBoxArrowWidth = 20; constexpr int ComboBoxArrowLeftOffset = 19; +constexpr int ComboBoxVisibleHeight = 24; +constexpr int ComboBoxDefaultMaxVisibleDropItems = 9; constexpr int ComboBoxMaxColorItems = 50; constexpr int ComboBoxTextEntryInlineBytes = 0; constexpr int ComboBoxEditListNotificationCode = 0x300; @@ -163,7 +165,50 @@ static void DrawComboDropButton(const RectangleStruct& rect, bool dropped, bool DrawScrollArrow(DSurface::Alternate, rect, dropped, dropped, alternatePalette); } -static void PaintComboBox(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, const RECT& ownerRect, WNDPROC pOriginalWndProc) +static bool GetComboBoxRenderLocalRect(HWND hWnd, const RECT& ownerRect, RECT& localRect) +{ + localRect = ownerRect; + + const HWND parentHwnd = ::GetParent(hWnd); + if (!parentHwnd || parentHwnd == Game::hWnd) + return true; + + RECT parentRect {}; + if (!OwnerDraw::GetRectangle(parentHwnd, &parentRect)) + return false; + + localRect.left -= parentRect.left; + localRect.right -= parentRect.left; + localRect.top -= parentRect.top; + localRect.bottom -= parentRect.top; + return true; +} + +static void EnsureComboBoxWindowHeight(HWND hWnd, OwnerDrawDialogElement& data, RECT& clientRect, RECT& ownerRect) +{ + const int width = ownerRect.right - ownerRect.left; + if (width <= 0 || ownerRect.bottom - ownerRect.top == ComboBoxVisibleHeight) + return; + + RECT localRect {}; + if (!GetComboBoxRenderLocalRect(hWnd, ownerRect, localRect)) + return; + + const BOOL moved = RenderDX::IsOwnerDrawUsingRawWindowCoordinates() + ? ::MoveWindow(hWnd, localRect.left, localRect.top, width, ComboBoxVisibleHeight, FALSE) + : RenderDX::MoveWindowInRender(hWnd, localRect.left, localRect.top, width, ComboBoxVisibleHeight, FALSE); + + if (!moved) + return; + + ResetOwnerDrawCachedSurface(data); + OwnerDraw::GetRectangle(hWnd, &ownerRect); + + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect)) + ::GetClientRect(hWnd, &clientRect); +} + +static void PaintComboBox(HWND hWnd, OwnerDrawDialogElement& data, const RECT& ownerRect, WNDPROC pOriginalWndProc) { if (!DSurface::Alternate) return; @@ -178,7 +223,7 @@ static void PaintComboBox(HWND hWnd, OwnerDrawDialogElement& data, const RECT& c ownerRect.left, ownerRect.top, width, - 24 + ComboBoxVisibleHeight }; RectangleStruct localRect { 0, 0, width, height }; @@ -245,14 +290,15 @@ static void PaintComboBox(HWND hWnd, OwnerDrawDialogElement& data, const RECT& c DSurface::Alternate->FillRect(&fillRect, ConvertRGBToSurfaceColor(textColor)); } - TrimComboTextToWidth(textBuffer, std::size(textBuffer), pFont, clientRect.right - ComboBoxArrowWidth); + const int textMaxWidth = std::max(textAreaRect.Width - 4, 0); + TrimComboTextToWidth(textBuffer, std::size(textBuffer), pFont, textMaxWidth); RECT textRect { - ownerRect.left + 2, - ownerRect.top, - ownerRect.right, - ownerRect.bottom + textAreaRect.X + 2, + textAreaRect.Y, + textAreaRect.X + textAreaRect.Width, + textAreaRect.Y + textAreaRect.Height }; OwnerDraw::DrawWideText(DSurface::Alternate, textBuffer, &textRect, pFont, textColor, 4, 12, 0, 0, 0); @@ -449,6 +495,25 @@ static void CloseComboDropDown(OwnerDrawDialogElement& data, HWND hWnd) data.AsComboBox().DropDownHwnd() = nullptr; } +static bool GetDroppedControlRectInRender(HWND hWnd, RECT& rect) +{ + ::SendMessageA(hWnd, CB_GETDROPPEDCONTROLRECT, 0, reinterpret_cast(&rect)); + + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + return true; + + POINT topLeft { rect.left, rect.top }; + POINT bottomRight { rect.right, rect.bottom }; + if (!RenderDX::ScreenToRenderPoint(&topLeft, false) || !RenderDX::ScreenToRenderPoint(&bottomRight, false)) + return false; + + rect.left = topLeft.x; + rect.top = topLeft.y; + rect.right = bottomRight.x; + rect.bottom = bottomRight.y; + return true; +} + static LRESULT OpenComboDropDown(OwnerDrawDialogElement& data, HWND hWnd, const RECT& clientRect, const RECT& ownerRect) { if (data.AsComboBox().DropDownHwnd()) @@ -463,51 +528,76 @@ static LRESULT OpenComboDropDown(OwnerDrawDialogElement& data, HWND hWnd, const RECT parentRect {}; OwnerDraw::GetRectangle(parentHwnd, &parentRect); - RECT dropRect {}; - ::SendMessageA(hWnd, CB_GETDROPPEDCONTROLRECT, 0, reinterpret_cast(&dropRect)); - if (dropRect.bottom > parentRect.bottom) - dropRect.bottom = parentRect.bottom; - int itemHeight = static_cast(::SendMessageA(hWnd, CB_GETITEMHEIGHT, 0, 0)); if (itemHeight <= 0) itemHeight = 1; - int dropHeight = dropRect.bottom - dropRect.top; - int visibleByHeight = (dropHeight - 1) / itemHeight; int itemCount = static_cast(::SendMessageA(hWnd, CB_GETCOUNT, 0, 0)); if (itemCount < 1) itemCount = 1; + const int visibleComboHeight = ComboBoxVisibleHeight; const int maxVisibleItems = data.AsComboBox().MaxVisibleDropItems(); - if (maxVisibleItems > 0 && itemCount >= maxVisibleItems) + int visibleItems = 0; + if (maxVisibleItems > 0) { - dropHeight = maxVisibleItems * itemHeight + 1; + visibleItems = maxVisibleItems; } - else if (visibleByHeight > itemCount) + else { - dropHeight = itemCount * itemHeight + 1; + RECT dropRect {}; + if (GetDroppedControlRectInRender(hWnd, dropRect)) + { + int nativeListHeight = dropRect.bottom - dropRect.top; + if (dropRect.top <= ownerRect.top + visibleComboHeight / 2) + nativeListHeight -= visibleComboHeight; + + if (nativeListHeight > 0) + visibleItems = nativeListHeight / itemHeight; + } + + if (visibleItems <= 1 && itemCount > 1) + visibleItems = ComboBoxDefaultMaxVisibleDropItems; } - if (ownerRect.bottom + dropHeight > parentRect.bottom) - dropHeight = parentRect.bottom - ownerRect.bottom; + if (visibleItems < 1) + visibleItems = 1; + + visibleItems = std::min(visibleItems, itemCount); + + const int dropTop = ownerRect.top + visibleComboHeight + 1; + int maxVisibleByParent = (parentRect.bottom - dropTop) / itemHeight; + if (maxVisibleByParent < 1) + maxVisibleByParent = 1; + + visibleItems = std::min(visibleItems, maxVisibleByParent); - dropHeight -= dropHeight % itemHeight; + int dropHeight = visibleItems * itemHeight; if (dropHeight <= 0) dropHeight = itemHeight; - const int x = ownerRect.left - parentRect.left; - const int y = ownerRect.top - parentRect.top + clientRect.bottom + 1; const int width = clientRect.right - clientRect.left; + const RECT dropRenderRect + { + ownerRect.left, + dropTop, + ownerRect.left + width, + dropTop + dropHeight + }; + + RECT dropClientRect {}; + if (!RenderDX::RenderRectToClient(parentHwnd, dropRenderRect, &dropClientRect)) + return 1; const HWND dropHwnd = ::CreateWindowExA( 0, "ComboDropWin", nullptr, WS_CHILD, - x, - y, - width, - dropHeight, + dropClientRect.left, + dropClientRect.top, + dropClientRect.right - dropClientRect.left, + dropClientRect.bottom - dropClientRect.top, parentHwnd, nullptr, Game::hInstance, @@ -546,7 +636,8 @@ LRESULT CALLBACK WWUI::ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPAR auto& data = *pData; RECT clientRect {}; - ::GetClientRect(hWnd, &clientRect); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect)) + ::GetClientRect(hWnd, &clientRect); RECT ownerRect {}; OwnerDraw::GetRectangle(hWnd, &ownerRect); @@ -576,7 +667,8 @@ LRESULT CALLBACK WWUI::ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPAR return forwardOriginal(); case WM_PAINT: - PaintComboBox(hWnd, data, clientRect, ownerRect, pOriginalWndProc); + EnsureComboBoxWindowHeight(hWnd, data, clientRect, ownerRect); + PaintComboBox(hWnd, data, ownerRect, pOriginalWndProc); return 0; case WM_ERASEBKGND: @@ -587,7 +679,7 @@ LRESULT CALLBACK WWUI::ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPAR if (RulesClass::Instance) VocClass::PlayGlobal(RulesClass::Instance->GUIComboOpenSound, 0x2000, 1.0f); - if (LOWORD(lParam) > static_cast(clientRect.right - ComboBoxArrowWidth)) + if (RenderDX::MouseLParamToRenderLocalPoint(hWnd, lParam).x > clientRect.right - ComboBoxArrowWidth) { const bool dropped = ::SendMessageA(hWnd, CB_GETDROPPEDSTATE, 0, 0) == 1; ::PostMessageA(hWnd, CB_SHOWDROPDOWN, dropped ? 0 : 1, 0); @@ -634,6 +726,7 @@ LRESULT CALLBACK WWUI::ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPAR return SetComboSelection(data, pOriginalWndProc, hWnd, wParam); case CB_SHOWDROPDOWN: + EnsureComboBoxWindowHeight(hWnd, data, clientRect, ownerRect); if (wParam) return OpenComboDropDown(data, hWnd, clientRect, ownerRect); @@ -649,14 +742,17 @@ LRESULT CALLBACK WWUI::ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPAR case WW_INITDIALOG: { const int fontHeight = BitFontHeight(data.AsComboBox().Font()); + const int selectionHeight = static_cast(::SendMessageA(hWnd, CB_GETITEMHEIGHT, static_cast(-1), 0)); if (!data.AsComboBox().HeightInitialized() + || selectionHeight != ComboBoxVisibleHeight || ::SendMessageA(hWnd, CB_GETITEMHEIGHT, 0, 0) != fontHeight + 6) { - ::SendMessageA(hWnd, CB_SETITEMHEIGHT, static_cast(-1), fontHeight + 2); + ::SendMessageA(hWnd, CB_SETITEMHEIGHT, static_cast(-1), ComboBoxVisibleHeight); ::SendMessageA(hWnd, CB_SETITEMHEIGHT, 0, fontHeight + 6); data.AsComboBox().HeightInitialized() = 1; } + EnsureComboBoxWindowHeight(hWnd, data, clientRect, ownerRect); data.AsComboBox().CurrentSelection() = -1; std::memset(data.AsComboBox().ItemColorOverrides(), 0xFF, sizeof(int) * ComboBoxMaxColorItems); return 0; @@ -725,14 +821,22 @@ LRESULT CALLBACK WWUI::ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPAR case WW_QUERYTOOLTIPHIT: if (const HWND dropHwnd = data.AsComboBox().DropDownHwnd()) { - RECT comboWindowRect {}; - RECT dropWindowRect {}; - ::GetWindowRect(hWnd, &comboWindowRect); - ::GetWindowRect(dropHwnd, &dropWindowRect); - - const int x = LOWORD(lParam) + comboWindowRect.left - dropWindowRect.left; - const int y = HIWORD(lParam) + comboWindowRect.top - dropWindowRect.top; - return ::SendMessageA(dropHwnd, WW_QUERYTOOLTIPHIT, 0, MAKELPARAM(x, y)); + POINT point { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; + ::ClientToScreen(hWnd, &point); + if (!RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + { + point = RenderDX::ScreenToRenderLocalPoint(dropHwnd, point); + } + else + { + ::ScreenToClient(dropHwnd, &point); + } + + return ::SendMessageA( + dropHwnd, + WW_QUERYTOOLTIPHIT, + 0, + MAKELPARAM(static_cast(point.x), static_cast(point.y))); } return -1; diff --git a/src/OwnerDraw/ListBox.cpp b/src/OwnerDraw/ListBox.cpp index e4a37287a9..650f4571da 100644 --- a/src/OwnerDraw/ListBox.cpp +++ b/src/OwnerDraw/ListBox.cpp @@ -737,6 +737,76 @@ static void SyncListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, const } } +static bool HasListBoxScrollBar(OwnerDrawDialogElement& data) +{ + const HWND scrollBarHwnd = data.AsListBox().ScrollBarHwnd(); + return scrollBarHwnd && reinterpret_cast(scrollBarHwnd) > 1 && ::IsWindow(scrollBarHwnd); +} + +static void PositionListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, BOOL repaint) +{ + if (!HasListBoxScrollBar(data)) + return; + + const HWND parentHwnd = ::GetParent(hWnd); + if (!parentHwnd) + return; + + RECT parentRect {}; + RECT listRect {}; + if (!OwnerDraw::GetRectangle(parentHwnd, &parentRect) || !OwnerDraw::GetRectangle(hWnd, &listRect)) + return; + + const int inset = OwnerDraw::ControlInsetPx; + const int scrollBarWidth = 2 * inset + ListBoxScrollBarExtraWidth; + const int x = listRect.right - parentRect.left - 2 * inset + 1; + const int y = listRect.top - parentRect.top; + const int height = listRect.bottom - listRect.top; + + if (height <= 0) + return; + + const HWND scrollBarHwnd = data.AsListBox().ScrollBarHwnd(); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + { + ::MoveWindow(scrollBarHwnd, x, y, scrollBarWidth, height, repaint); + } + else + { + RenderDX::MoveWindowInRender(scrollBarHwnd, x, y, scrollBarWidth, height, repaint); + } + + if (auto pScrollData = FindOwnerDrawData(scrollBarHwnd)) + ResetOwnerDrawCachedSurface(*pScrollData); + + ::InvalidateRect(scrollBarHwnd, nullptr, FALSE); +} + +void WWUI::SyncListBoxScrollBarPositions(HWND rootHwnd) +{ + if (!rootHwnd) + { + for (auto it = OwnerDraw::Dialogs.begin(); it != OwnerDraw::Dialogs.end(); ++it) + { + const HWND hWnd = it->Key; + auto& data = it->Value; + if (::IsWindow(hWnd) && data.ControlType == WWControlType::ListBox) + PositionListBoxScrollBar(hWnd, data, FALSE); + } + + return; + } + + if (auto pData = FindOwnerDrawData(rootHwnd)) + { + if (pData->ControlType == WWControlType::ListBox) + PositionListBoxScrollBar(rootHwnd, *pData, FALSE); + } + + for (HWND child = ::GetWindow(rootHwnd, GW_CHILD); child; child = ::GetWindow(child, GW_HWNDNEXT)) + SyncListBoxScrollBarPositions(child); +} + static void UpdateListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect) { const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); @@ -758,19 +828,41 @@ static void UpdateListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, cons OwnerDraw::GetRectangle(parentHwnd, &parentRect); OwnerDraw::GetRectangle(hWnd, &listRect); - const int x = listRect.left - parentRect.left - scrollBarWidth + clientRect.right + 1; - const int y = listRect.top - parentRect.top + clientRect.top; const int height = listRect.bottom - listRect.top; + RECT scrollClientRect {}; + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + { + scrollClientRect.left = listRect.left - parentRect.left - scrollBarWidth + clientRect.right + 1; + scrollClientRect.top = listRect.top - parentRect.top + clientRect.top; + scrollClientRect.right = scrollClientRect.left + scrollBarWidth; + scrollClientRect.bottom = scrollClientRect.top + height; + } + else + { + const RECT scrollRenderRect + { + listRect.left - scrollBarWidth + clientRect.right + 1, + listRect.top + clientRect.top, + listRect.left + clientRect.right + 1, + listRect.top + clientRect.top + height + }; + + if (!RenderDX::RenderRectToClient(parentHwnd, scrollRenderRect, &scrollClientRect)) + { + data.AsListBox().ScrollBarHwnd() = nullptr; + return; + } + } data.AsListBox().ScrollBarHwnd() = ::CreateWindowExA( 0, "Scrollbar", nullptr, WS_CHILD | WS_VISIBLE | SBS_VERT | WS_TABSTOP, - x, - y, - scrollBarWidth, - height, + scrollClientRect.left, + scrollClientRect.top, + scrollClientRect.right - scrollClientRect.left, + scrollClientRect.bottom - scrollClientRect.top, parentHwnd, nullptr, reinterpret_cast(Phobos::hInstance), @@ -787,17 +879,32 @@ static void UpdateListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, cons SyncListBoxScrollBar(hWnd, data, clientRect, itemCount, itemHeight); - ::SetWindowPos( - hWnd, - nullptr, - 0, - 0, - listRect.right - listRect.left - scrollBarWidth, - listRect.bottom - listRect.top, - SWP_NOMOVE | SWP_NOZORDER); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + { + ::SetWindowPos( + hWnd, + nullptr, + 0, + 0, + listRect.right - listRect.left - scrollBarWidth, + listRect.bottom - listRect.top, + SWP_NOMOVE | SWP_NOZORDER); + } + else + { + RenderDX::SetWindowPosInRender( + hWnd, + nullptr, + 0, + 0, + listRect.right - listRect.left - scrollBarWidth, + listRect.bottom - listRect.top, + SWP_NOMOVE | SWP_NOZORDER); + } ::ShowWindow(data.AsListBox().ScrollBarHwnd(), SW_SHOW); ::BringWindowToTop(data.AsListBox().ScrollBarHwnd()); + PositionListBoxScrollBar(hWnd, data, FALSE); ::InvalidateRect(data.AsListBox().ScrollBarHwnd(), nullptr, FALSE); ::UpdateWindow(data.AsListBox().ScrollBarHwnd()); } @@ -814,15 +921,29 @@ static void UpdateListBoxScrollBar(HWND hWnd, OwnerDrawDialogElement& data, cons data.AsListBox().ScrollBarHwnd() = nullptr; RECT listRect {}; - ::GetWindowRect(hWnd, &listRect); - ::SetWindowPos( - hWnd, - nullptr, - 0, - 0, - listRect.right - listRect.left + scrollBarWidth, - listRect.bottom - listRect.top, - SWP_NOMOVE | SWP_NOZORDER); + OwnerDraw::GetRectangle(hWnd, &listRect); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + { + ::SetWindowPos( + hWnd, + nullptr, + 0, + 0, + listRect.right - listRect.left + scrollBarWidth, + listRect.bottom - listRect.top, + SWP_NOMOVE | SWP_NOZORDER); + } + else + { + RenderDX::SetWindowPosInRender( + hWnd, + nullptr, + 0, + 0, + listRect.right - listRect.left + scrollBarWidth, + listRect.bottom - listRect.top, + SWP_NOMOVE | SWP_NOZORDER); + } data.AsListBox().ScrollBarWidth() = 0; } @@ -837,7 +958,9 @@ LRESULT CALLBACK WWUI::ListBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARA const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, hWnd); RECT clientRect {}; - ::GetClientRect(hWnd, &clientRect); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect)) + ::GetClientRect(hWnd, &clientRect); + const RECT fullClientRect = clientRect; RECT ownerRect {}; OwnerDraw::GetRectangle(hWnd, &ownerRect); @@ -983,27 +1106,12 @@ LRESULT CALLBACK WWUI::ListBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARA switch (message) { case WM_SIZE: - if (data.AsListBox().ScrollBarHwnd() && reinterpret_cast(data.AsListBox().ScrollBarHwnd()) > 1) - { - const HWND parentHwnd = ::GetParent(hWnd); - RECT parentRect {}; - RECT listRect {}; - OwnerDraw::GetRectangle(parentHwnd, &parentRect); - OwnerDraw::GetRectangle(hWnd, &listRect); - ::MoveWindow( - data.AsListBox().ScrollBarHwnd(), - listRect.right - parentRect.left, - listRect.top - parentRect.top, - 2 * inset + ListBoxScrollBarExtraWidth, - listRect.bottom - listRect.top, - TRUE); - } + PositionListBoxScrollBar(hWnd, data, TRUE); if (data.CacheSurface - && (LOWORD(lParam) != data.CacheSurface->GetWidth() || HIWORD(lParam) != data.CacheSurface->GetHeight())) + && (fullClientRect.right != data.CacheSurface->GetWidth() || fullClientRect.bottom != data.CacheSurface->GetHeight())) { - DeleteSurfaceObject(data.CacheSurface); - --OwnerDraw::CachedSurfaceCount; + ResetOwnerDrawCachedSurface(data); } return finish(forwardOriginal()); @@ -1069,7 +1177,11 @@ LRESULT CALLBACK WWUI::ListBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARA case WM_LBUTTONDOWN: { const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1); - const int itemIndex = data.AsListBox().TopIndex() + SignedHighWord(lParam) / itemHeight; + const POINT point = RenderDX::MouseLParamToRenderLocalPoint(hWnd, lParam); + if (point.y < 0) + return 0; + + const int itemIndex = data.AsListBox().TopIndex() + point.y / itemHeight; const int itemCount = static_cast(::SendMessageA(hWnd, LB_GETCOUNT, 0, 0)); if (itemIndex < 0 || itemIndex >= itemCount) return 0; @@ -1354,9 +1466,10 @@ LRESULT CALLBACK WWUI::ListBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARA case WW_QUERYTOOLTIPHIT: { - const int x = SignedLowWord(lParam); - const int y = SignedHighWord(lParam); - if (x < clientRect.right && y < clientRect.bottom) + const POINT point = RenderDX::MouseLParamToRenderLocalPoint(hWnd, lParam); + const int x = point.x; + const int y = point.y; + if (x >= 0 && y >= 0 && x < clientRect.right && y < clientRect.bottom) { const int itemHeight = std::max(static_cast(::SendMessageA(hWnd, LB_GETITEMHEIGHT, 0, 0)), 1); const int itemIndex = data.AsListBox().TopIndex() + y / itemHeight; diff --git a/src/OwnerDraw/OwnerDraw.Hooks.cpp b/src/OwnerDraw/OwnerDraw.Hooks.cpp index d1f1ee1b80..b0239cbf3f 100644 --- a/src/OwnerDraw/OwnerDraw.Hooks.cpp +++ b/src/OwnerDraw/OwnerDraw.Hooks.cpp @@ -19,4 +19,5 @@ DEFINE_FUNCTION_JUMP(LJMP, 0x61E700, WWUI::GroupBoxCtrl); DEFINE_FUNCTION_JUMP(LJMP, 0x61ECA0, WWUI::InputCtrl); DEFINE_FUNCTION_JUMP(LJMP, 0x61D6D0, WWUI::ProgressCtrl); DEFINE_FUNCTION_JUMP(LJMP, 0x61C690, WWUI::ScrollBarCtrl); +DEFINE_FUNCTION_JUMP(LJMP, 0x622820, WWUI::RegisterOwnerDrawWindow); DEFINE_FUNCTION_JUMP(LJMP, 0x622B50, WWUI::OwnerDrawStandardWndProc); diff --git a/src/OwnerDraw/OwnerDraw.Internal.h b/src/OwnerDraw/OwnerDraw.Internal.h index 63181ec0a9..1f5326f5e0 100644 --- a/src/OwnerDraw/OwnerDraw.Internal.h +++ b/src/OwnerDraw/OwnerDraw.Internal.h @@ -5,6 +5,7 @@ #include "../Render/Functions.h" #include +#include #include #include @@ -53,6 +54,10 @@ const wchar_t* GetWideTextBuffer(const WideWstring& text); void DeleteSurfaceObject(Surface*& pSurface); +void ResetOwnerDrawCachedSurface(OwnerDrawDialogElement& data); + +void ResetOwnerDrawCachedSurfaceTree(HWND rootHwnd); + void DeleteUnknownGameObject(void*& pObject); void InsetSurfaceRect(RectangleStruct& rect, int x, int y); diff --git a/src/OwnerDraw/OwnerDraw.cpp b/src/OwnerDraw/OwnerDraw.cpp index 9073b15b73..4dfab60f90 100644 --- a/src/OwnerDraw/OwnerDraw.cpp +++ b/src/OwnerDraw/OwnerDraw.cpp @@ -19,6 +19,11 @@ WNDPROC FindWindowProc(OwnerDraw::HwndProcDict& procs, HWND hWnd) return nullptr; } +static WNDPROC GetComboDropWindowProc() +{ + return reinterpret_cast(0x60D540); +} + bool IsEmpty(const WideWstring& text) { return text.GetLength() == 0; @@ -40,6 +45,24 @@ static WideWstring QueryTooltipText(HWND parentHwnd, HWND controlHwnd, LPARAM hi return request.Text; } +static bool IsTooltipTextCurrent(HWND tooltipHwnd, const WideWstring& text) +{ + const auto pTooltipData = FindOwnerDrawData(tooltipHwnd); + if (!pTooltipData) + return false; + + const wchar_t* currentText = pTooltipData->TextBuffer ? pTooltipData->TextBuffer : L""; + return std::wcscmp(currentText, GetWideTextBuffer(text)) == 0; +} + +static void SetTooltipTextIfChanged(HWND tooltipHwnd, const WideWstring& text) +{ + if (IsTooltipTextCurrent(tooltipHwnd, text)) + return; + + ::SendMessageA(tooltipHwnd, WW_SETTEXTW, 0, reinterpret_cast(GetWideTextBuffer(text))); +} + static std::vector& ActiveWindowMessages() { static std::vector messages; @@ -113,6 +136,36 @@ static HWND GetActiveWindowStackTop() return OwnerDraw::ActiveWindowStack[count - 1]; } +static bool IsOwnerDrawDialogRoot(HWND hWnd) +{ + return hWnd + && ::IsWindow(hWnd) + && ::GetWindowLongA(hWnd, DialogProcWindowLongIndex); +} + +bool WWUI::HasActiveOwnerDrawDialog() +{ + if (OwnerDraw::ActiveWindowStackCount > 0 && OwnerDraw::ActiveWindowStack) + { + for (int i = 0; i < OwnerDraw::ActiveWindowStackCount; ++i) + { + if (IsOwnerDrawDialogRoot(OwnerDraw::ActiveWindowStack[i])) + return true; + } + } + + if (!OwnerDraw::Dialogs.size()) + return false; + + for (auto it = OwnerDraw::Dialogs.begin(); it != OwnerDraw::Dialogs.end(); ++it) + { + if (IsOwnerDrawDialogRoot(it->Key)) + return true; + } + + return false; +} + static void ResizeActiveWindowStack(int capacity) { if (capacity < 10) @@ -291,6 +344,39 @@ void DeleteSurfaceObject(Surface*& pSurface) pSurface = nullptr; } +void ResetOwnerDrawCachedSurface(OwnerDrawDialogElement& data) +{ + if (!data.CacheSurface) + return; + + DeleteSurfaceObject(data.CacheSurface); + if (OwnerDraw::CachedSurfaceCount > 0) + --OwnerDraw::CachedSurfaceCount; +} + +void ResetOwnerDrawCachedSurfaceTree(HWND rootHwnd) +{ + if (!rootHwnd) + { + for (auto it = OwnerDraw::Dialogs.begin(); it != OwnerDraw::Dialogs.end(); ++it) + { + ResetOwnerDrawCachedSurface(it->Value); + if (::IsWindow(it->Key)) + ::InvalidateRect(it->Key, nullptr, FALSE); + } + + return; + } + + if (auto pData = FindOwnerDrawData(rootHwnd)) + ResetOwnerDrawCachedSurface(*pData); + + ::InvalidateRect(rootHwnd, nullptr, FALSE); + + for (HWND child = ::GetWindow(rootHwnd, GW_CHILD); child; child = ::GetWindow(child, GW_HWNDNEXT)) + ResetOwnerDrawCachedSurfaceTree(child); +} + void DeleteUnknownGameObject(void*& pObject) { if (!pObject) @@ -707,7 +793,7 @@ static void RepaintChildWindows(HWND hWnd, HWND ownerHwnd, const RECT& ownerDraw const HWND childHwnd = children.Items[i]; const auto pOriginalWndProc = FindWindowProc(OwnerDraw::DialogProcs, childHwnd); - if (pOriginalWndProc == OwnerDraw::ComboDropWindowHandler) + if (pOriginalWndProc == GetComboDropWindowProc()) { comboDropHwnd = childHwnd; continue; @@ -1139,7 +1225,13 @@ static void UpdateTooltipTextOnMouseMove(HWND hWnd, LPARAM lParam) WideWstring tooltipText; - const LPARAM pointParam = MAKELPARAM(LOWORD(lParam), HIWORD(lParam)); + LPARAM pointParam = MAKELPARAM(LOWORD(lParam), HIWORD(lParam)); + if (!RenderDX::IsOwnerDrawUsingRawWindowCoordinates() + && FindWindowProc(OwnerDraw::DialogProcs, hWnd) == GetComboDropWindowProc()) + { + pointParam = RenderDX::MouseLParamToRenderLocal(hWnd, lParam); + } + const auto hitCode = ::SendMessageA(hWnd, WW_QUERYTOOLTIPHIT, 0, pointParam); tooltipText = QueryTooltipText(parentHwnd, hWnd, hitCode); @@ -1164,18 +1256,41 @@ static void UpdateTooltipTextOnMouseMove(HWND hWnd, LPARAM lParam) } } - ::SendMessageA(tooltipHwnd, WW_SETTEXTW, 0, reinterpret_cast(GetWideTextBuffer(tooltipText))); + SetTooltipTextIfChanged(tooltipHwnd, tooltipText); +} + +static bool IsComboDropMousePointMessage(UINT message) +{ + return message >= WM_MOUSEFIRST && message <= WM_MBUTTONDBLCLK; +} + +static LPARAM GetSelectedHandlerLParam(HWND hWnd, UINT message, LPARAM lParam, const OwnerDrawDialogElement* pData, WNDPROC pSelectedWndProc) +{ + if (pSelectedWndProc == GetComboDropWindowProc() + && IsComboDropMousePointMessage(message) + && !RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + { + return RenderDX::MouseLParamToRenderLocal(hWnd, lParam); + } + + if (!pData || pData->DialogID != 148 || !::GetWindowLongA(hWnd, DialogProcWindowLongIndex)) + return lParam; + + if (message != WM_LBUTTONDOWN && message != WM_LBUTTONUP) + return lParam; + + POINT point { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; + if (!::ClientToScreen(hWnd, &point)) + return lParam; + + return MAKELPARAM(static_cast(point.x), static_cast(point.y)); } void CleanupDestroyedWindow(HWND hWnd) { if (auto pData = FindOwnerDrawData(hWnd)) { - if (pData->CacheSurface) - { - DeleteSurfaceObject(pData->CacheSurface); - --OwnerDraw::CachedSurfaceCount; - } + ResetOwnerDrawCachedSurface(*pData); } OwnerDraw::DialogProcs.erase(hWnd); @@ -1190,6 +1305,291 @@ void CleanupDestroyedWindow(HWND hWnd) SessionIpb::UnregisterHwnd(hWnd); } +static bool GetRawWindowRectInRenderLayout(HWND hWnd, RECT& rect) +{ + RECT windowRect {}; + if (!::GetWindowRect(hWnd, &windowRect)) + return false; + + const HWND parentHwnd = ::GetParent(hWnd); + const HWND coordinateHwnd = parentHwnd ? parentHwnd : Game::hWnd; + + POINT origin { 0, 0 }; + if (coordinateHwnd && !::ClientToScreen(coordinateHwnd, &origin)) + return false; + + rect.left = windowRect.left - origin.x; + rect.top = windowRect.top - origin.y; + rect.right = windowRect.right - origin.x; + rect.bottom = windowRect.bottom - origin.y; + return true; +} + +static void ApplyRenderLayoutToWindowTree(HWND hWnd) +{ + RECT rect {}; + if (GetRawWindowRectInRenderLayout(hWnd, rect)) + { + RenderDX::MoveWindowInRender( + hWnd, + rect.left, + rect.top, + rect.right - rect.left, + rect.bottom - rect.top, + FALSE); + } + + for (HWND child = ::GetWindow(hWnd, GW_CHILD); child; child = ::GetWindow(child, GW_HWNDNEXT)) + ApplyRenderLayoutToWindowTree(child); +} + +struct CapturedOwnerDrawWindowRect +{ + HWND Hwnd {}; + RECT Rect {}; + int Depth {}; +}; + +static std::vector& CapturedOwnerDrawWindowRects() +{ + static std::vector rects; + return rects; +} + +static int GetWindowHierarchyDepth(HWND hWnd) +{ + int depth = 0; + for (HWND walker = ::GetParent(hWnd); walker; walker = ::GetParent(walker)) + ++depth; + + return depth; +} + +using WinDialogGetCurrentHandle = HWND(__fastcall*)(); +using WinDialogFindHandle = HWND(__fastcall*)(HWND); + +static HWND GetCurrentWinDialogHandle() +{ + return reinterpret_cast(0x775B10)(); +} + +static HWND FindPreviousWinDialogHandle(HWND hWnd) +{ + return reinterpret_cast(0x7759B0)(hWnd); +} + +static bool ContainsHwnd(const std::vector& windows, HWND hWnd) +{ + return std::find(windows.begin(), windows.end(), hWnd) != windows.end(); +} + +static void AddOwnerDrawDialogRoots(std::vector& windows) +{ + for (auto it = OwnerDraw::Dialogs.begin(); it != OwnerDraw::Dialogs.end(); ++it) + { + const HWND hWnd = it->Key; + if (!::IsWindow(hWnd) || ContainsHwnd(windows, hWnd)) + continue; + + if (::GetWindowLongA(hWnd, DialogProcWindowLongIndex)) + windows.push_back(hWnd); + } +} + +static std::vector GetOwnerDrawDialogRoots() +{ + std::vector windows; + + for (HWND hWnd = GetCurrentWinDialogHandle(); hWnd; hWnd = FindPreviousWinDialogHandle(hWnd)) + { + if (!::IsWindow(hWnd) || ContainsHwnd(windows, hWnd)) + continue; + + if (FindOwnerDrawData(hWnd)) + windows.push_back(hWnd); + } + + AddOwnerDrawDialogRoots(windows); + return windows; +} + +static bool FindCapturedOwnerDrawRect( + const std::vector& rects, + HWND hWnd, + RECT& rect) +{ + for (const auto& captured : rects) + { + if (captured.Hwnd == hWnd) + { + rect = captured.Rect; + return true; + } + } + + return false; +} + +static std::vector CaptureOwnerDrawRenderRects() +{ + std::vector rects; + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !OwnerDraw::Dialogs.size()) + return rects; + + rects.reserve(OwnerDraw::Dialogs.size()); + for (auto it = OwnerDraw::Dialogs.begin(); it != OwnerDraw::Dialogs.end(); ++it) + { + const HWND hWnd = it->Key; + if (!::IsWindow(hWnd)) + continue; + + RECT rect {}; + if (OwnerDraw::GetRectangle(hWnd, &rect)) + rects.push_back(CapturedOwnerDrawWindowRect { hWnd, rect, GetWindowHierarchyDepth(hWnd) }); + } + + return rects; +} + +static void NormalizeOwnerDrawWindowsToRawRenderCoordinates() +{ + auto rects = CaptureOwnerDrawRenderRects(); + if (rects.empty()) + return; + + std::stable_sort( + rects.begin(), + rects.end(), + [](const CapturedOwnerDrawWindowRect& lhs, const CapturedOwnerDrawWindowRect& rhs) + { + return lhs.Depth < rhs.Depth; + }); + + const bool restoreRawWindowCoordinates = RenderDX::IsOwnerDrawUsingRawWindowCoordinates(); + RenderDX::SetOwnerDrawRawWindowCoordinates(true); + + for (const auto& captured : rects) + { + if (!::IsWindow(captured.Hwnd)) + continue; + + POINT parentOrigin {}; + const HWND parentHwnd = ::GetParent(captured.Hwnd); + if (parentHwnd && parentHwnd != Game::hWnd) + { + RECT parentRect {}; + if (FindCapturedOwnerDrawRect(rects, parentHwnd, parentRect)) + { + parentOrigin.x = parentRect.left; + parentOrigin.y = parentRect.top; + } + } + + ::MoveWindow( + captured.Hwnd, + captured.Rect.left - parentOrigin.x, + captured.Rect.top - parentOrigin.y, + captured.Rect.right - captured.Rect.left, + captured.Rect.bottom - captured.Rect.top, + FALSE); + } + + RenderDX::SetOwnerDrawRawWindowCoordinates(restoreRawWindowCoordinates); +} + +void WWUI::CaptureOwnerDrawWindowRects() +{ + auto& capturedRects = CapturedOwnerDrawWindowRects(); + capturedRects.clear(); + + capturedRects = CaptureOwnerDrawRenderRects(); +} + +void WWUI::ApplyOwnerDrawWindowRects() +{ + auto& capturedRects = CapturedOwnerDrawWindowRects(); + if (capturedRects.empty()) + return; + + std::stable_sort( + capturedRects.begin(), + capturedRects.end(), + [](const CapturedOwnerDrawWindowRect& lhs, const CapturedOwnerDrawWindowRect& rhs) + { + return lhs.Depth < rhs.Depth; + }); + + for (const auto& captured : capturedRects) + { + if (!::IsWindow(captured.Hwnd)) + continue; + + RECT clientRect {}; + if (!RenderDX::RenderRectToClient(::GetParent(captured.Hwnd), captured.Rect, &clientRect)) + continue; + + ::MoveWindow( + captured.Hwnd, + clientRect.left, + clientRect.top, + clientRect.right - clientRect.left, + clientRect.bottom - clientRect.top, + FALSE); + } + + WWUI::SyncListBoxScrollBarPositions(nullptr); + WWUI::SyncStaticMoviePositions(nullptr); + ResetOwnerDrawCachedSurfaceTree(nullptr); + capturedRects.clear(); +} + +void WWUI::RelayoutWindowsAfterDisplayModeChange() +{ + if (!OwnerDraw::Dialogs.size()) + return; + + const auto windows = GetOwnerDrawDialogRoots(); + if (windows.empty()) + { + ResetOwnerDrawCachedSurfaceTree(nullptr); + WWUI::SyncListBoxScrollBarPositions(nullptr); + WWUI::SyncStaticMoviePositions(nullptr); + return; + } + + const bool restoreRawWindowCoordinates = RenderDX::IsOwnerDrawUsingRawWindowCoordinates(); + if (!restoreRawWindowCoordinates) + NormalizeOwnerDrawWindowsToRawRenderCoordinates(); + + RenderDX::SetOwnerDrawRawWindowCoordinates(true); + + auto baseSize = OwnerDraw::BaseLayoutSize; + for (const HWND hWnd : windows) + { + if (!::IsWindow(hWnd)) + continue; + + ResetOwnerDrawCachedSurfaceTree(hWnd); + OwnerDraw::UpdateControlPosition(hWnd, &baseSize); + ApplyRenderLayoutToWindowTree(hWnd); + } + + RenderDX::SetOwnerDrawRawWindowCoordinates(false); + + for (const HWND hWnd : windows) + { + if (!::IsWindow(hWnd)) + continue; + + UI::CenterWindow(hWnd); + WWUI::SyncListBoxScrollBarPositions(hWnd); + WWUI::SyncStaticMoviePositions(hWnd); + ResetOwnerDrawCachedSurfaceTree(hWnd); + } + + RenderDX::SetOwnerDrawRawWindowCoordinates(restoreRawWindowCoordinates); +} + static void FinishDialogInitialization(HWND hWnd) { if (!SessionClass::Instance.CurrentlyInGame) @@ -1214,7 +1614,12 @@ static void FinishDialogInitialization(HWND hWnd) ::EnumChildWindows(hWnd, OwnerDraw::ClassifyLayoutBand, 0); ::EnumChildWindows(hWnd, OwnerDraw::ResetControlDrawModeAndTimerProc, 0); + ApplyRenderLayoutToWindowTree(hWnd); + RenderDX::SetOwnerDrawRawWindowCoordinates(false); UI::CenterWindow(hWnd); + WWUI::SyncListBoxScrollBarPositions(hWnd); + ResetOwnerDrawCachedSurfaceTree(hWnd); + WWUI::SyncStaticMoviePositions(hWnd); ::SetFocus(hWnd); } @@ -1241,6 +1646,7 @@ static void RegisterDialogControls(HWND hWnd, int dialogID) static LRESULT HandleInitDialog(HWND hWnd, LPARAM lParam) { ++Unsorted::WSDialogCount; + RenderDX::SetOwnerDrawRawWindowCoordinates(true); if (lParam) { @@ -1260,6 +1666,18 @@ static LRESULT HandleInitDialog(HWND hWnd, LPARAM lParam) return 0; } +HWND __fastcall WWUI::RegisterOwnerDrawWindow(HWND hWnd, int dialogID) +{ + const bool restoreRawWindowCoordinates = RenderDX::IsOwnerDrawUsingRawWindowCoordinates(); + + RenderDX::SetOwnerDrawRawWindowCoordinates(true); + RegisterDialogControls(hWnd, dialogID); + FinishDialogInitialization(hWnd); + RenderDX::SetOwnerDrawRawWindowCoordinates(restoreRawWindowCoordinates); + + return hWnd; +} + static LRESULT HandlePaint(HWND hWnd) { auto pData = FindOwnerDrawData(hWnd); @@ -1294,23 +1712,17 @@ static LRESULT HandleTooltipRefresh(HWND hWnd, LPARAM lParam) if (!tooltipHwnd) return 0; - const int screenX = LOWORD(lParam); - const int screenY = HIWORD(lParam); - - RECT dialogRect; - ::GetWindowRect(hWnd, &dialogRect); - - const POINT clientPoint - { - screenX - dialogRect.left, - screenY - dialogRect.top - }; + const POINT screenPoint { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; + POINT clientPoint = screenPoint; + ::ScreenToClient(hWnd, &clientPoint); WideWstring tooltipText; if (const auto controlHwnd = ::ChildWindowFromPointEx(hWnd, clientPoint, CWP_SKIPINVISIBLE)) { - const LPARAM pointParam = MAKELPARAM(LOWORD(lParam), HIWORD(lParam)); + POINT controlPoint = screenPoint; + ::ScreenToClient(controlHwnd, &controlPoint); + const LPARAM pointParam = MAKELPARAM(static_cast(controlPoint.x), static_cast(controlPoint.y)); const auto hitCode = ::SendMessageA(controlHwnd, WW_QUERYTOOLTIPHIT, 0, pointParam); tooltipText = QueryTooltipText(hWnd, controlHwnd, hitCode); @@ -1337,12 +1749,15 @@ static LRESULT HandleTooltipRefresh(HWND hWnd, LPARAM lParam) } } - ::SendMessageA(tooltipHwnd, WW_SETTEXTW, 0, reinterpret_cast(GetWideTextBuffer(tooltipText))); + SetTooltipTextIfChanged(tooltipHwnd, tooltipText); return 0; } -LRESULT __fastcall WWUI::OwnerDrawStandardWndProc(HWND hWnd, UINT message, WPARAM, LPARAM lParam) +LRESULT __fastcall WWUI::OwnerDrawStandardWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (RenderDX::HandleFullscreenToggleMessage(message, wParam, lParam)) + return 0; + if (message <= WM_NCHITTEST) { switch (message) @@ -1394,6 +1809,9 @@ LRESULT CALLBACK WWUI::OwnerDrawWindowProc(HWND hWnd, UINT message, WPARAM wPara if (message == WM_SETCURSOR) return 1; + if (RenderDX::HandleFullscreenToggleMessage(message, wParam, lParam)) + return 0; + const bool isPaintMessage = message == WM_PAINT; const auto updatePrimaryAfterPaint = [isPaintMessage]() { @@ -1551,6 +1969,14 @@ LRESULT CALLBACK WWUI::OwnerDrawWindowProc(HWND hWnd, UINT message, WPARAM wPara switch (message) { + case WM_MOVE: + case WM_SIZE: + case WM_WINDOWPOSCHANGED: + ResetOwnerDrawCachedSurfaceTree(hWnd); + WWUI::SyncListBoxScrollBarPositions(hWnd); + WWUI::SyncStaticMoviePositions(hWnd); + break; + case WM_ERASEBKGND: return complete(1); @@ -1633,7 +2059,8 @@ LRESULT CALLBACK WWUI::OwnerDrawWindowProc(HWND hWnd, UINT message, WPARAM wPara } else if (callSelectedHandler && pSelectedWndProc) { - result = CallSelectedHandler(pSelectedWndProc, hWnd, message, wParam, lParam); + const LPARAM selectedLParam = GetSelectedHandlerLParam(hWnd, message, lParam, pData, pSelectedWndProc); + result = CallSelectedHandler(pSelectedWndProc, hWnd, message, wParam, selectedLParam); } if (message == WM_NCDESTROY) diff --git a/src/OwnerDraw/OwnerDraw.h b/src/OwnerDraw/OwnerDraw.h index 88cf97218f..80d3c6ab89 100644 --- a/src/OwnerDraw/OwnerDraw.h +++ b/src/OwnerDraw/OwnerDraw.h @@ -21,4 +21,11 @@ namespace WWUI LRESULT CALLBACK RadioCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); LRESULT CALLBACK InputCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); LRESULT CALLBACK SysListViewCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + HWND __fastcall RegisterOwnerDrawWindow(HWND hWnd, int dialogID); + void CaptureOwnerDrawWindowRects(); + void ApplyOwnerDrawWindowRects(); + void RelayoutWindowsAfterDisplayModeChange(); + bool HasActiveOwnerDrawDialog(); + void SyncStaticMoviePositions(HWND rootHwnd); + void SyncListBoxScrollBarPositions(HWND rootHwnd); } diff --git a/src/OwnerDraw/ScrollBar.cpp b/src/OwnerDraw/ScrollBar.cpp index e48a23cc37..16af5a3c4e 100644 --- a/src/OwnerDraw/ScrollBar.cpp +++ b/src/OwnerDraw/ScrollBar.cpp @@ -40,7 +40,7 @@ void EnsureScrollBarCache( if (data.CacheSurface) { if (data.CacheSurface->GetWidth() != localRect.Width || data.CacheSurface->GetHeight() != localRect.Height) - DeleteSurfaceObject(data.CacheSurface); + ResetOwnerDrawCachedSurface(data); } if (data.CacheSurface || localRect.Width <= 0 || localRect.Height <= 0) @@ -149,10 +149,63 @@ static void PaintScrollBar( DrawScrollArrow(DSurface::Alternate, downButtonRect, false, downPressed, disabled); } +static WNDPROC GetComboDropWindowProc() +{ + return reinterpret_cast(0x60D540); +} + +static bool IsComboDropNotifyHwnd(HWND hWnd) +{ + return hWnd + && (hWnd == OwnerDraw::ComboDropActiveDropHwnd + || FindWindowProc(OwnerDraw::DialogProcs, hWnd) == GetComboDropWindowProc() + || FindWindowProc(OwnerDraw::SubclassProcs, hWnd) == GetComboDropWindowProc()); +} + +static bool TryGetComboDropForwardedMousePoint( + HWND hWnd, + OwnerDrawDialogElement& data, + const RECT& clientRect, + LPARAM lParam, + POINT& point) +{ + const HWND notifyHwnd = data.AsScrollBar().NotifyHwnd(); + if (!IsComboDropNotifyHwnd(notifyHwnd)) + return false; + + point = POINT { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; + if (point.x >= clientRect.left + && point.x <= clientRect.right + && point.y >= clientRect.top + && point.y <= clientRect.bottom) + { + return false; + } + + RECT notifyRect {}; + RECT scrollRect {}; + if (!OwnerDraw::GetRectangle(notifyHwnd, ¬ifyRect) || !OwnerDraw::GetRectangle(hWnd, &scrollRect)) + return false; + + point.x += notifyRect.left - scrollRect.left; + point.y += notifyRect.top - scrollRect.top; + return true; +} + +static POINT GetScrollBarMousePoint(HWND hWnd, OwnerDrawDialogElement& data, const RECT& clientRect, LPARAM lParam) +{ + POINT point {}; + if (TryGetComboDropForwardedMousePoint(hWnd, data, clientRect, lParam, point)) + return point; + + return RenderDX::MouseLParamToRenderLocalPoint(hWnd, lParam); +} + LRESULT CALLBACK WWUI::ScrollBarCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { RECT clientRect {}; - ::GetClientRect(hWnd, &clientRect); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect)) + ::GetClientRect(hWnd, &clientRect); RECT scrollBarRect {}; OwnerDraw::GetRectangle(hWnd, &scrollBarRect); @@ -210,7 +263,7 @@ LRESULT CALLBACK WWUI::ScrollBarCtrl(HWND hWnd, UINT message, WPARAM wParam, LPA { POINT point {}; ::GetCursorPos(&point); - ::ScreenToClient(hWnd, &point); + point = RenderDX::ScreenToRenderLocalPoint(hWnd, point); thumbTop = point.y - thumbHeight / 2; if (thumbTop < ScrollBarButtonHeight) @@ -332,7 +385,7 @@ LRESULT CALLBACK WWUI::ScrollBarCtrl(HWND hWnd, UINT message, WPARAM wParam, LPA { POINT point {}; ::GetCursorPos(&point); - ::ScreenToClient(hWnd, &point); + point = RenderDX::ScreenToRenderLocalPoint(hWnd, point); upButtonPressed = false; downButtonPressed = false; @@ -368,14 +421,7 @@ LRESULT CALLBACK WWUI::ScrollBarCtrl(HWND hWnd, UINT message, WPARAM wParam, LPA case WM_MOUSEMOVE: if (isThumbDragging) { - RECT invalidateRect - { - scrollBarLeft, - clientRect.top, - clientRect.right, - clientRect.bottom - }; - ::InvalidateRect(hWnd, &invalidateRect, FALSE); + ::InvalidateRect(hWnd, nullptr, FALSE); } if (wParam & MK_LBUTTON) @@ -412,8 +458,9 @@ LRESULT CALLBACK WWUI::ScrollBarCtrl(HWND hWnd, UINT message, WPARAM wParam, LPA } { - const int clickX = LOWORD(lParam); - const int clickY = HIWORD(lParam); + const POINT point = GetScrollBarMousePoint(hWnd, data, clientRect, lParam); + const int clickX = point.x; + const int clickY = point.y; const int repeatCount = message == WM_LBUTTONDBLCLK ? 2 : 1; upButtonPressed = false; diff --git a/src/OwnerDraw/Slider.cpp b/src/OwnerDraw/Slider.cpp index b829e95b7f..fe745a8567 100644 --- a/src/OwnerDraw/Slider.cpp +++ b/src/OwnerDraw/Slider.cpp @@ -205,7 +205,8 @@ static void PaintSlider( LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { RECT clientRect {}; - ::GetClientRect(hWnd, &clientRect); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates() || !RenderDX::GetClientRectInRender(hWnd, &clientRect)) + ::GetClientRect(hWnd, &clientRect); RECT ownerRect {}; OwnerDraw::GetRectangle(hWnd, &ownerRect); @@ -226,8 +227,30 @@ LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM int stepValue = data.AsSlider().StepValue(); int showValueLabel = data.AsSlider().ShowValueLabel(); - int valueLabelWidth = showValueLabel ? SliderValueLabelWidth : 0; - const int trackTravel = SliderTrackTravel(clientRect, valueLabelWidth); + int valueLabelWidth = 0; + int trackTravel = 1; + auto updateTrackMetrics = [&]() + { + if (!stepValue) + { + valueLabelWidth = SliderValueLabelWidth; + stepValue = 1; + showValueLabel = 1; + } + else + { + valueLabelWidth = showValueLabel ? SliderValueLabelWidth : 0; + } + + trackTravel = SliderTrackTravel(clientRect, valueLabelWidth); + }; + + auto updateThumbFromPosition = [&]() + { + thumbOffsetPixels = SliderThumbOffsetFromPosition(positionOffset, trackTravel, rangeSpan); + }; + + updateTrackMetrics(); if (!rangeSpan) { @@ -239,7 +262,7 @@ LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM rangeSpan = 100; positionOffset = static_cast(CallSelectedHandler(pOriginalWndProc, hWnd, WW_SLIDER_GETPOS, 0, 0)) - rangeMin; - thumbOffsetPixels = SliderThumbOffsetFromPosition(positionOffset, trackTravel, rangeSpan); + updateThumbFromPosition(); data.AsSlider().ThumbOffsetPixels() = thumbOffsetPixels; data.AsSlider().RangeSpan() = rangeSpan; @@ -249,12 +272,8 @@ LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM data.AsSlider().ShowValueLabel() = showValueLabel; } - if (!stepValue) - { - valueLabelWidth = SliderValueLabelWidth; - stepValue = 1; - showValueLabel = 1; - } + if (!isThumbDragging) + updateThumbFromPosition(); int thumbHitLeftX = clientRect.left + thumbOffsetPixels + 1; int thumbHitRightX = thumbHitLeftX + SliderGripHitWidth; @@ -263,7 +282,7 @@ LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM { POINT point {}; ::GetCursorPos(&point); - ::ScreenToClient(hWnd, &point); + point = RenderDX::ScreenToRenderLocalPoint(hWnd, point); SliderUpdateFromGripX( SliderClampedGripX(point.x, clientRect, valueLabelWidth), @@ -285,10 +304,15 @@ LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM auto writeBack = [&]() -> LRESULT { - const bool valueChanged = + const bool notifyChanged = positionOffset != data.AsSlider().PositionOffset() || rangeSpan != data.AsSlider().RangeSpan() || rangeMin != data.AsSlider().RangeMin(); + const bool visualChanged = + notifyChanged + || thumbOffsetPixels != data.AsSlider().ThumbOffsetPixels() + || stepValue != data.AsSlider().StepValue() + || showValueLabel != data.AsSlider().ShowValueLabel(); data.AsSlider().IsMouseTracking() = isMouseTracking; data.AsSlider().IsThumbDragging() = isThumbDragging; @@ -299,9 +323,11 @@ LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM data.AsSlider().StepValue() = stepValue; data.AsSlider().ShowValueLabel() = showValueLabel; - if (valueChanged) - { + if (visualChanged) ::InvalidateRect(hWnd, nullptr, FALSE); + + if (notifyChanged) + { ::SendMessageA(::GetParent(hWnd), WM_HSCROLL, MAKELONG(SB_THUMBTRACK, positionOffset + rangeMin), reinterpret_cast(hWnd)); if (playClickSound == 1 && !data.AsSlider().SuppressClickSound() && RulesClass::Instance) @@ -342,7 +368,7 @@ LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM case WM_MOUSEMOVE: if (isThumbDragging) - ::InvalidateRect(hWnd, &clientRect, FALSE); + ::InvalidateRect(hWnd, nullptr, FALSE); if (wParam & MK_LBUTTON) return writeBack(); @@ -368,23 +394,26 @@ LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM ::ReleaseCapture(); } - if (HIWORD(lParam) > clientRect.bottom - SliderMouseHitBottomInset) { - const int clickX = LOWORD(lParam); - if (clickX < thumbHitLeftX || clickX >= thumbHitRightX) + const POINT point = RenderDX::MouseLParamToRenderLocalPoint(hWnd, lParam); + if (point.y > clientRect.bottom - SliderMouseHitBottomInset) { - SliderUpdateFromGripX( - SliderClampedGripX(clickX, clientRect, valueLabelWidth), - rangeSpan, - rangeMin, - stepValue, - trackTravel, - positionOffset, - thumbOffsetPixels); - } - else if (message == WM_LBUTTONDOWN) - { - isThumbDragging = 1; + const int clickX = point.x; + if (clickX < thumbHitLeftX || clickX >= thumbHitRightX) + { + SliderUpdateFromGripX( + SliderClampedGripX(clickX, clientRect, valueLabelWidth), + rangeSpan, + rangeMin, + stepValue, + trackTravel, + positionOffset, + thumbOffsetPixels); + } + else if (message == WM_LBUTTONDOWN) + { + isThumbDragging = 1; + } } } return writeBack(); @@ -399,7 +428,7 @@ LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM positionOffset = requestedOffset; playClickSound = 0; - thumbOffsetPixels = SliderThumbOffsetFromPosition(positionOffset, trackTravel, rangeSpan); + updateThumbFromPosition(); return writeBack(); } @@ -409,19 +438,23 @@ LRESULT CALLBACK WWUI::SliderCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM if (positionOffset > rangeSpan) positionOffset = rangeSpan; - if (positionOffset < rangeMin) - positionOffset = rangeMin; + if (positionOffset < 0) + positionOffset = 0; playClickSound = 0; - thumbOffsetPixels = SliderThumbOffsetFromPosition(positionOffset, trackTravel, rangeSpan); + updateThumbFromPosition(); return writeBack(); case WW_SLIDER_SETSTEP: stepValue = static_cast(lParam); + updateTrackMetrics(); + updateThumbFromPosition(); return writeBack(); case WW_SLIDER_SHOWVALUE: showValueLabel = static_cast(lParam); + updateTrackMetrics(); + updateThumbFromPosition(); return writeBack(); case WW_SLIDER_SUPPRESSCLICK: diff --git a/src/OwnerDraw/Static.cpp b/src/OwnerDraw/Static.cpp index 01167ca857..63484e5466 100644 --- a/src/OwnerDraw/Static.cpp +++ b/src/OwnerDraw/Static.cpp @@ -20,7 +20,7 @@ static UINT GetStaticAnimationTimerInterval(HWND parentHwnd, HWND controlHwnd) return 100; } - if (dialogId == 148 && (controlId == 1770 || controlId == 1771 || controlId == 1772)) + if (dialogId == 148 && (controlId == 1770 || controlId == 1771 || controlId == 1772 || controlId == 1773)) return 100; if ((dialogId == 259 || dialogId == 3015) && controlId == 1835) @@ -49,6 +49,47 @@ static void DestroyStaticMovieAux(OwnerDrawDialogElement& data) DeleteUnknownGameObject(data.AsStatic().MovieAuxHandle()); } +static void SyncStaticMoviePosition(HWND hWnd, OwnerDrawDialogElement& data); + +static int GetParentDialogId(HWND hWnd) +{ + if (const HWND parentHwnd = ::GetParent(hWnd)) + { + if (auto pParentData = FindOwnerDrawData(parentHwnd)) + return pParentData->DialogID; + } + + return 0; +} + +static bool IsCampaignDescriptionStatic(HWND hWnd) +{ + if (GetParentDialogId(hWnd) != 148) + return false; + + const int controlId = ::GetDlgCtrlID(hWnd); + return controlId >= 1959 && controlId <= 1962; +} + +static void AdjustStaticTextPaintRect(HWND hWnd, OwnerDrawDialogElement& data, RECT& rect) +{ + if (!IsCampaignDescriptionStatic(hWnd)) + return; + + const int minimumHeight = std::max(BitFontHeight(data.AsStatic().Font()) + 6, 30); + if (rect.bottom - rect.top < minimumHeight) + rect.bottom = rect.top + minimumHeight; +} + +static bool GetStaticPaintRect(HWND hWnd, OwnerDrawDialogElement& data, RECT& rect) +{ + if (!OwnerDraw::GetRectangle(hWnd, &rect)) + return false; + + AdjustStaticTextPaintRect(hWnd, data, rect); + return true; +} + static void DetachStaticMovie(HWND hWnd, OwnerDrawDialogElement& data) { DestroyStaticMovie(data); @@ -77,7 +118,24 @@ static LRESULT LoadStaticMovie(HWND hWnd, OwnerDrawDialogElement& data, const ch if (pMovie->VTable && pMovie->VTable->SetPosition) pMovie->VTable->SetPosition(pMovie, ownerRect.left, ownerRect.top); - ::MoveWindow(hWnd, ownerRect.left, ownerRect.top, pMovie->Width, pMovie->Height, FALSE); + int x = ownerRect.left; + int y = ownerRect.top; + if (const HWND parentHwnd = ::GetParent(hWnd)) + { + RECT parentRect {}; + if (OwnerDraw::GetRectangle(parentHwnd, &parentRect)) + { + x -= parentRect.left; + y -= parentRect.top; + } + } + + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + ::MoveWindow(hWnd, x, y, pMovie->Width, pMovie->Height, FALSE); + else + RenderDX::MoveWindowInRender(hWnd, x, y, pMovie->Width, pMovie->Height, FALSE); + + SyncStaticMoviePosition(hWnd, data); ::SetTimer(hWnd, 0x65, 0x22, nullptr); return 0; } @@ -95,18 +153,51 @@ static const char* GetStaticMovieName(int index) return MovieInfo::Array.Items[index].Name; } +static void SyncStaticMoviePosition(HWND hWnd, OwnerDrawDialogElement& data) +{ + auto pMovie = data.AsStatic().MovieHandle(); + if (!pMovie || !pMovie->VTable || !pMovie->VTable->SetPosition) + return; + + RECT ownerRect {}; + if (!OwnerDraw::GetRectangle(hWnd, &ownerRect)) + return; + + pMovie->VTable->SetPosition(pMovie, ownerRect.left, ownerRect.top); +} + +void WWUI::SyncStaticMoviePositions(HWND rootHwnd) +{ + if (!rootHwnd) + { + for (auto it = OwnerDraw::Dialogs.begin(); it != OwnerDraw::Dialogs.end(); ++it) + { + const HWND hWnd = it->Key; + if (::IsWindow(hWnd)) + SyncStaticMoviePosition(hWnd, it->Value); + } + + return; + } + + if (auto pData = FindOwnerDrawData(rootHwnd)) + SyncStaticMoviePosition(rootHwnd, *pData); + + for (HWND child = ::GetWindow(rootHwnd, GW_CHILD); child; child = ::GetWindow(child, GW_HWNDNEXT)) + SyncStaticMoviePositions(child); +} + static bool EnsureStaticBackground(HWND hWnd, OwnerDrawDialogElement& data) { if (data.AsStatic().CachedBackground() || !DSurface::Alternate) return data.AsStatic().CachedBackground() != nullptr; RECT ownerRect {}; - RECT clientRect {}; - OwnerDraw::GetRectangle(hWnd, &ownerRect); - ::GetClientRect(hWnd, &clientRect); + if (!GetStaticPaintRect(hWnd, data, ownerRect)) + return false; - const int width = clientRect.right + 1; - const int height = clientRect.bottom + 1; + const int width = ownerRect.right - ownerRect.left + 1; + const int height = ownerRect.bottom - ownerRect.top + 1; if (width <= 0 || height <= 0) return false; @@ -128,12 +219,11 @@ static void RestoreStaticBackground(HWND hWnd, OwnerDrawDialogElement& data, boo return; RECT ownerRect {}; - RECT clientRect {}; - OwnerDraw::GetRectangle(hWnd, &ownerRect); - ::GetClientRect(hWnd, &clientRect); + if (!GetStaticPaintRect(hWnd, data, ownerRect)) + return; - const int width = clientRect.right + (inclusiveBounds ? 1 : 0); - const int height = clientRect.bottom + (inclusiveBounds ? 1 : 0); + const int width = ownerRect.right - ownerRect.left + (inclusiveBounds ? 1 : 0); + const int height = ownerRect.bottom - ownerRect.top + (inclusiveBounds ? 1 : 0); if (width <= 0 || height <= 0) return; @@ -144,8 +234,7 @@ static void RestoreStaticBackground(HWND hWnd, OwnerDrawDialogElement& data, boo static void ResetStaticBackground(HWND hWnd, OwnerDrawDialogElement& data) { - if (data.AsStatic().CachedBackground()) - DeleteSurfaceObject(data.AsStatic().CachedBackground()); + ResetOwnerDrawCachedSurface(data); ::InvalidateRect(hWnd, nullptr, FALSE); } @@ -290,7 +379,8 @@ static LRESULT PaintStatic(HWND hWnd, OwnerDrawDialogElement& data) EnsureStaticBackground(hWnd, data); RECT ownerRect {}; - OwnerDraw::GetRectangle(hWnd, &ownerRect); + if (!GetStaticPaintRect(hWnd, data, ownerRect)) + return 0; if (data.AsStatic().FillBackground()) { @@ -341,8 +431,7 @@ static LRESULT PaintStatic(HWND hWnd, OwnerDrawDialogElement& data) static void DestroyStaticResources(HWND hWnd, OwnerDrawDialogElement& data) { - if (data.AsStatic().CachedBackground()) - DeleteSurfaceObject(data.AsStatic().CachedBackground()); + ResetOwnerDrawCachedSurface(data); if (data.AsStatic().OwnsShape() && data.AsStatic().Shape()) { @@ -455,6 +544,7 @@ LRESULT CALLBACK WWUI::StaticCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARAM case WM_SIZE: case WM_WINDOWPOSCHANGED: ResetStaticBackground(hWnd, data); + SyncStaticMoviePosition(hWnd, data); return 0; case WM_DESTROY: diff --git a/src/OwnerDraw/Tab.cpp b/src/OwnerDraw/Tab.cpp index 469c400754..25ce703da2 100644 --- a/src/OwnerDraw/Tab.cpp +++ b/src/OwnerDraw/Tab.cpp @@ -177,19 +177,22 @@ static void DrawTabItem(HWND hWnd, OwnerDrawDialogElement& data, const RECT& con DrawTabItemMiddle(variant, drawLeft, drawTop, drawWidth); DrawTabItemCaps(variant, drawLeft, drawTop, drawWidth); DrawTabItemText(hWnd, data, tabIndex, tabIndex == selectedIndex, drawLeft, drawTop, drawWidth, drawHeight); - ::ValidateRect(hWnd, &itemRect); + ::ValidateRect(hWnd, nullptr); } static void InitializeTabCtrl(HWND hWnd) { - RECT controlRect {}; - OwnerDraw::GetRectangle(hWnd, &controlRect); - int tabHeight = 0; if (auto pLeftUp = GetPCXSurface("tab_tlu.pcx")) tabHeight = std::max(pLeftUp->GetHeight() - 1, 0); - ::SendMessageA(hWnd, TCM_SETITEMSIZE, 0, MAKELPARAM(0x59, tabHeight)); + const RECT renderItemRect { 0, 0, 0x59, tabHeight }; + const RECT clientItemRect = RenderDX::RenderToClientRect(renderItemRect); + ::SendMessageA( + hWnd, + TCM_SETITEMSIZE, + 0, + MAKELPARAM(clientItemRect.right - clientItemRect.left, clientItemRect.bottom - clientItemRect.top)); } static LRESULT PaintTabCtrl(HWND hWnd, OwnerDrawDialogElement& data) @@ -202,6 +205,7 @@ static LRESULT PaintTabCtrl(HWND hWnd, OwnerDrawDialogElement& data) RECT firstItemRect {}; ::SendMessageA(hWnd, TCM_GETITEMRECT, 0, reinterpret_cast(&firstItemRect)); ::SendMessageA(hWnd, TCM_GETITEMRECT, 0, reinterpret_cast(&firstItemRect)); + firstItemRect = RenderDX::ClientToRenderLocalRect(hWnd, firstItemRect); RECT panelRect = controlRect; panelRect.top += firstItemRect.bottom - firstItemRect.top + 3; @@ -244,6 +248,7 @@ static LRESULT PaintTabCtrl(HWND hWnd, OwnerDrawDialogElement& data) break; } + itemRect = RenderDX::ClientToRenderLocalRect(hWnd, itemRect); DrawTabItem(hWnd, data, controlRect, tabIndex, selectedIndex, itemRect); if (tabIndex == selectedIndex) diff --git a/src/Render/Functions.cpp b/src/Render/Functions.cpp index ef25fe802a..93ac698f04 100644 --- a/src/Render/Functions.cpp +++ b/src/Render/Functions.cpp @@ -6,6 +6,7 @@ #include "Renderer.h" #include "Mouse.h" #include "Options.h" +#include "../OwnerDraw/OwnerDraw.h" #include #include @@ -15,9 +16,25 @@ #include #include +#include #include #include +DEFINE_REFERENCE(RectangleStruct, DisplayVisibleRect, 0x886FB0u) + +static bool OwnerDrawRectsAlreadyCaptured = false; + +static void ReleasePrimarySurface() +{ + auto pPrimary = DSurface::Primary; + if (!pPrimary) + return; + + DSurface::Primary = nullptr; + Debug::Log("[RenderDX] Deleting old primary surface\n"); + GameDelete(pPrimary); +} + bool __fastcall RenderDX::AllocateSurfaces(const RectangleStruct& hiddenRect, const RectangleStruct& compositeRect, const RectangleStruct& tileRect, const RectangleStruct& sidebarRect, bool hiddenFirst) { Debug::Log("[RenderDX] Allocating new surfaces\n"); @@ -98,8 +115,12 @@ bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bitsPerP return false; } + WWUI::CaptureOwnerDrawWindowRects(); + OwnerDrawRectsAlreadyCaptured = true; + ResetVideoMode(); if (!DXRenderer::Instance().CreateRenderer(width, height, bitsPerPixel)) { + OwnerDrawRectsAlreadyCaptured = false; Debug::Log("[RenderDX] Failed to create renderer\n"); return false; } @@ -107,8 +128,11 @@ bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bitsPerP Drawing::RenderWidth = width; Drawing::RenderHeight = height; Drawing::RenderBitsPerPixel = bitsPerPixel; + DSurface::ViewBounds = RectangleStruct { 0, 0, width, height }; + DisplayVisibleRect = DSurface::ViewBounds; RenderDX::UpdateScale(); + OwnerDrawRectsAlreadyCaptured = false; return true; } @@ -116,6 +140,7 @@ bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bitsPerP void __fastcall RenderDX::ResetVideoMode() { Debug::Log("[RenderDX] Resetting video mode\n"); + ReleasePrimarySurface(); DXRenderer::Instance().DestroyRenderer(); Drawing::RenderWidth = 0; @@ -139,6 +164,26 @@ static void RecalcMouseWindowRegion(bool rebuildCursor) { DXMouse::Instance->RebuildCursorImage(); } +bool __fastcall RenderDX::HandleFullscreenToggleMessage(UINT message, WPARAM wParam, LPARAM lParam) { + if (wParam != VK_RETURN || !(lParam & (1 << 29))) + return false; + + if (message == WM_SYSKEYDOWN) { + DXRenderer::Instance().ToggleFullscreen(); + RecalcMouseWindowRegion(true); + return true; + } + + return message == WM_SYSKEYUP || message == WM_SYSCHAR; +} + +using ReinitMenuLayoutRectsFunc = void(__fastcall*)(int width, int height); + +static void ReinitMenuLayoutRects(int width, int height) +{ + reinterpret_cast(0x72E1B0)(width, height); +} + static void ApplyWindowResize(int width, int height) { DXRenderer::Instance().ResizeWindow(width, height); RecalcMouseWindowRegion(true); @@ -162,13 +207,14 @@ static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, L { // Scale mouse inputs before they are processed by SDL or the game. if (RenderDX::ShouldScale()) { - int x = GET_X_LPARAM(lParam); - int y = GET_Y_LPARAM(lParam); + POINT point { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; - x = RenderDX::ClientToRenderX(x); - y = RenderDX::ClientToRenderY(y); + if (message == WM_MOUSEWHEEL) + RenderDX::ScreenToRenderPoint(&point, true); + else + point = RenderDX::ClientToRenderPoint(point, true); - lParam = MAKELPARAM(x, y); + lParam = MAKELPARAM(static_cast(point.x), static_cast(point.y)); } break; } @@ -237,16 +283,11 @@ static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, L } case WM_SYSKEYDOWN: + case WM_SYSKEYUP: + case WM_SYSCHAR: { - // Handle Alt+Enter for fullscreen toggle - if (wParam == VK_RETURN && (lParam & (1 << 29))) { - DXRenderer::Instance().ToggleFullscreen(); - if (DXMouse::Instance) { - DXMouse::Instance->RecalcCaptureRegion(); - DXMouse::Instance->RebuildCursorImage(); - } + if (RenderDX::HandleFullscreenToggleMessage(message, wParam, lParam)) return 0; // handled - } break; } @@ -334,17 +375,30 @@ bool __fastcall RenderDX::UpdateScreen(Surface* pSurface) { } bool __fastcall RenderDX::ShouldScale() { - return Unsorted::SpecialDialog == 0 && Unsorted::WSDialogCount == 0; + return true; +} + +static RectangleStruct GetSidebarClipBounds(const RectangleStruct& viewRect) { + constexpr int sidebarWidth = 168; + constexpr int bottomBarHeight = 32; + + return RectangleStruct + { + GameOptionsClass::Instance.SidebarMode ? 0 : sidebarWidth, + 0, + viewRect.Width - sidebarWidth, + viewRect.Height - bottomBarHeight + }; } static void RebuildDisplayState(const RectangleStruct& viewRect) { - auto sidebarRect = viewRect; - sidebarRect.X = GameOptionsClass::Instance.SidebarMode ? 0 : 168; - sidebarRect.Y = 16; - sidebarRect.Width -= 168; - sidebarRect.Height -= 16; + constexpr int sidebarWidth = 168; + const auto sidebarRect = GetSidebarClipBounds(viewRect); + const RectangleStruct tacticalSurfaceRect { 0, 0, sidebarRect.Width, viewRect.Height }; + const RectangleStruct sidebarSurfaceRect { 0, 0, sidebarWidth, viewRect.Height }; DSurface::ViewBounds = viewRect; + DisplayVisibleRect = viewRect; Drawing::RenderWidth = viewRect.Width; Drawing::RenderHeight = viewRect.Height; @@ -352,9 +406,9 @@ static void RebuildDisplayState(const RectangleStruct& viewRect) { RenderDX::AllocateSurfaces( viewRect, - RectangleStruct { 0, 0, sidebarRect.Width, viewRect.Height }, - RectangleStruct { 0, 0, sidebarRect.Width, viewRect.Height }, - RectangleStruct { 0, 0, 168, viewRect.Height }, + tacticalSurfaceRect, + tacticalSurfaceRect, + sidebarSurfaceRect, false ); DSurface::Temp = DSurface::Hidden; @@ -363,12 +417,16 @@ static void RebuildDisplayState(const RectangleStruct& viewRect) { DXMouse::Instance->RebuildCursorImage(); } + ReinitMenuLayoutRects(viewRect.Width, viewRect.Height); + SidebarClass::Instance.Set_View_Dimensions(sidebarRect); SidebarClass::Instance.Init_IO(); SidebarClass::Instance.Activate(1); SidebarClass::Instance.InitGUI(); SidebarClass::Instance.MarkNeedsRedraw(2); // REDRAW_ALL DXMouse::Instance->ShowMouse(); + + WWUI::RelayoutWindowsAfterDisplayModeChange(); } bool __fastcall RenderDX::ChangeDisplayMode(int width, int height) { @@ -394,12 +452,7 @@ bool __fastcall RenderDX::ChangeDisplayMode(int width, int height) { DXMouse::Instance->HideMouse(); - // Delete the old primary surface - if (DSurface::Primary) { - Debug::Log("[RenderDX] Deleting old primary surface\n"); - GameDelete(DSurface::Primary); - DSurface::Primary = nullptr; - } + ReleasePrimarySurface(); if (DXRenderer::Instance().IsWindowed()) { int windowWidth = width; @@ -430,7 +483,7 @@ bool __fastcall RenderDX::ChangeDisplayMode(int width, int height) { Debug::Log("[RenderDX] Restore window to (%d, %d) with size %dx%d\n", oldWindowX, oldWindowY, oldWindowWidth, oldWindowHeight); } - if (oldRect.X > 0 && oldRect.Y > 0 && oldRenderWidth > 0 && oldRenderHeight > 0) { + if (oldRect.Width > 0 && oldRect.Height > 0 && oldRenderWidth > 0 && oldRenderHeight > 0) { Debug::Log("[RenderDX] Restoring old display mode.\n"); if (!SetVideoMode(Game::hWnd, oldRenderWidth, oldRenderHeight, oldRenderBpp)) { Debug::Log("[RenderDX] Failed to restore old display mode.\n"); @@ -459,6 +512,7 @@ static float ScaleX = 1.0f; static float ScaleY = 1.0f; static float ViewportX = 0.0f; static float ViewportY = 0.0f; +static bool OwnerDrawRawWindowCoordinates = false; float __fastcall RenderDX::GetXScale() { return ScaleX; @@ -468,33 +522,426 @@ float __fastcall RenderDX::GetYScale() { return ScaleY; } +static int ClampRenderX(int x) { + if (Drawing::RenderWidth <= 0) + return x; + + return std::clamp(x, 0, Drawing::RenderWidth - 1); +} + +static int ClampRenderY(int y) { + if (Drawing::RenderHeight <= 0) + return y; + + return std::clamp(y, 0, Drawing::RenderHeight - 1); +} + +static RECT ClampRenderRect(const RECT& rect) { + RECT result = rect; + if (Drawing::RenderWidth > 0) { + result.left = std::clamp(result.left, 0, static_cast(Drawing::RenderWidth)); + result.right = std::clamp(result.right, 0, static_cast(Drawing::RenderWidth)); + } + if (Drawing::RenderHeight > 0) { + result.top = std::clamp(result.top, 0, static_cast(Drawing::RenderHeight)); + result.bottom = std::clamp(result.bottom, 0, static_cast(Drawing::RenderHeight)); + } + return result; +} + +static int ClientToRenderXRounded(int x) +{ + if (Drawing::RenderWidth <= 0) + return x; + + return static_cast(std::lround((static_cast(x) - ViewportX) * ScaleX)); +} + +static int ClientToRenderYRounded(int y) +{ + if (Drawing::RenderHeight <= 0) + return y; + + return static_cast(std::lround((static_cast(y) - ViewportY) * ScaleY)); +} + +static RECT ClientToRenderRectRounded(const RECT& rect) +{ + return RECT + { + ClientToRenderXRounded(rect.left), + ClientToRenderYRounded(rect.top), + ClientToRenderXRounded(rect.right), + ClientToRenderYRounded(rect.bottom) + }; +} + int __fastcall RenderDX::ClientToRenderX(int x) { if (Drawing::RenderWidth <= 0) return x; - return std::clamp(static_cast((x - ViewportX) * ScaleX), 0, Drawing::RenderWidth - 1); + return ClampRenderX(ClientToRenderXUnclamped(x)); } int __fastcall RenderDX::ClientToRenderY(int y) { if (Drawing::RenderHeight <= 0) return y; - return std::clamp(static_cast((y - ViewportY) * ScaleY), 0, Drawing::RenderHeight - 1); + return ClampRenderY(ClientToRenderYUnclamped(y)); +} + +int __fastcall RenderDX::ClientToRenderXUnclamped(int x) { + if (Drawing::RenderWidth <= 0) + return x; + + return static_cast((static_cast(x) - ViewportX) * ScaleX); +} + +int __fastcall RenderDX::ClientToRenderYUnclamped(int y) { + if (Drawing::RenderHeight <= 0) + return y; + + return static_cast((static_cast(y) - ViewportY) * ScaleY); +} + +int __fastcall RenderDX::RenderToClientX(int x) { + if (Drawing::RenderWidth <= 0 || ScaleX == 0.0f) + return x; + + return static_cast(std::lround(ViewportX + static_cast(x) / ScaleX)); +} + +int __fastcall RenderDX::RenderToClientY(int y) { + if (Drawing::RenderHeight <= 0 || ScaleY == 0.0f) + return y; + + return static_cast(std::lround(ViewportY + static_cast(y) / ScaleY)); +} + +POINT __fastcall RenderDX::ClientToRenderPoint(POINT point, bool clamp) { + point.x = clamp ? ClientToRenderX(point.x) : ClientToRenderXUnclamped(point.x); + point.y = clamp ? ClientToRenderY(point.y) : ClientToRenderYUnclamped(point.y); + return point; +} + +POINT __fastcall RenderDX::RenderToClientPoint(POINT point) { + point.x = RenderToClientX(point.x); + point.y = RenderToClientY(point.y); + return point; +} + +RECT __fastcall RenderDX::ClientToRenderRect(const RECT& rect, bool clamp) { + RECT result + { + ClientToRenderXUnclamped(rect.left), + ClientToRenderYUnclamped(rect.top), + ClientToRenderXUnclamped(rect.right), + ClientToRenderYUnclamped(rect.bottom) + }; + + return clamp ? ClampRenderRect(result) : result; +} + +RECT __fastcall RenderDX::RenderToClientRect(const RECT& rect) { + if (Drawing::RenderWidth <= 0 || Drawing::RenderHeight <= 0 || ScaleX == 0.0f || ScaleY == 0.0f) + return rect; + + return RECT + { + static_cast(std::floor(ViewportX + static_cast(rect.left) / ScaleX)), + static_cast(std::floor(ViewportY + static_cast(rect.top) / ScaleY)), + static_cast(std::ceil(ViewportX + static_cast(rect.right) / ScaleX)), + static_cast(std::ceil(ViewportY + static_cast(rect.bottom) / ScaleY)) + }; +} + +bool __fastcall RenderDX::ScreenToRenderPoint(LPPOINT pPoint, bool clamp) { + if (!pPoint) + return false; + + if (!::ScreenToClient(Game::hWnd, pPoint)) + return false; + + *pPoint = ClientToRenderPoint(*pPoint, clamp); + return true; +} + +bool __fastcall RenderDX::RenderToScreenPoint(LPPOINT pPoint) { + if (!pPoint) + return false; + + *pPoint = RenderToClientPoint(*pPoint); + return ::ClientToScreen(Game::hWnd, pPoint) != FALSE; +} + +bool __fastcall RenderDX::GetWindowRectInRender(HWND hWnd, LPRECT pRect) { + if (!hWnd || !pRect) + return false; + + if (hWnd == Game::hWnd) { + pRect->left = 0; + pRect->top = 0; + pRect->right = Drawing::RenderWidth; + pRect->bottom = Drawing::RenderHeight; + return true; + } + + RECT windowRect {}; + if (!::GetWindowRect(hWnd, &windowRect)) + return false; + + POINT topLeft { windowRect.left, windowRect.top }; + POINT bottomRight { windowRect.right, windowRect.bottom }; + if (!::ScreenToClient(Game::hWnd, &topLeft) || !::ScreenToClient(Game::hWnd, &bottomRight)) + return false; + + const RECT clientRect { topLeft.x, topLeft.y, bottomRight.x, bottomRight.y }; + *pRect = ClientToRenderRectRounded(clientRect); + return true; +} + +bool __fastcall RenderDX::GetClientRectInRender(HWND hWnd, LPRECT pRect) { + if (!hWnd || !pRect) + return false; + + if (hWnd == Game::hWnd) { + pRect->left = 0; + pRect->top = 0; + pRect->right = Drawing::RenderWidth; + pRect->bottom = Drawing::RenderHeight; + return true; + } + + RECT clientRect {}; + if (!::GetClientRect(hWnd, &clientRect)) + return false; + + POINT topLeft { clientRect.left, clientRect.top }; + POINT bottomRight { clientRect.right, clientRect.bottom }; + if (!::ClientToScreen(hWnd, &topLeft) || !::ClientToScreen(hWnd, &bottomRight)) + return false; + + if (!::ScreenToClient(Game::hWnd, &topLeft) || !::ScreenToClient(Game::hWnd, &bottomRight)) + return false; + + const RECT renderRect = ClientToRenderRectRounded(RECT { topLeft.x, topLeft.y, bottomRight.x, bottomRight.y }); + pRect->left = 0; + pRect->top = 0; + pRect->right = renderRect.right - renderRect.left; + pRect->bottom = renderRect.bottom - renderRect.top; + return true; +} + +POINT __fastcall RenderDX::ScreenToRenderLocalPoint(HWND hWnd, POINT point) { + if (!ScreenToRenderPoint(&point, false)) + return point; + + RECT windowRect {}; + if (GetWindowRectInRender(hWnd, &windowRect)) { + point.x -= windowRect.left; + point.y -= windowRect.top; + } + + return point; +} + +RECT __fastcall RenderDX::ClientToRenderLocalRect(HWND hWnd, const RECT& rect) { + POINT topLeft { rect.left, rect.top }; + POINT bottomRight { rect.right, rect.bottom }; + if (!::ClientToScreen(hWnd, &topLeft) || !::ClientToScreen(hWnd, &bottomRight)) + return rect; + + topLeft = ScreenToRenderLocalPoint(hWnd, topLeft); + bottomRight = ScreenToRenderLocalPoint(hWnd, bottomRight); + + return RECT { topLeft.x, topLeft.y, bottomRight.x, bottomRight.y }; +} + +RECT __fastcall RenderDX::RenderLocalToClientRect(HWND hWnd, const RECT& rect) { + RECT windowRect {}; + if (!GetWindowRectInRender(hWnd, &windowRect)) + return rect; + + const RECT renderRect + { + windowRect.left + rect.left, + windowRect.top + rect.top, + windowRect.left + rect.right, + windowRect.top + rect.bottom + }; + + RECT clientRect {}; + if (!RenderRectToClient(hWnd, renderRect, &clientRect)) + return rect; + + return clientRect; +} + +POINT __fastcall RenderDX::MouseLParamToRenderLocalPoint(HWND hWnd, LPARAM lParam) { + POINT point { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; + if (!::ClientToScreen(hWnd, &point)) + return point; + + return ScreenToRenderLocalPoint(hWnd, point); +} + +LPARAM __fastcall RenderDX::MouseLParamToRenderLocal(HWND hWnd, LPARAM lParam) { + const POINT point = MouseLParamToRenderLocalPoint(hWnd, lParam); + return MAKELPARAM(static_cast(point.x), static_cast(point.y)); +} + +bool __fastcall RenderDX::RenderRectToClient(HWND referenceHwnd, const RECT& renderRect, LPRECT pClientRect) { + if (!pClientRect) + return false; + + POINT topLeft { renderRect.left, renderRect.top }; + POINT bottomRight { renderRect.right, renderRect.bottom }; + if (!RenderToScreenPoint(&topLeft) || !RenderToScreenPoint(&bottomRight)) + return false; + + if (referenceHwnd) { + if (!::ScreenToClient(referenceHwnd, &topLeft) || !::ScreenToClient(referenceHwnd, &bottomRight)) + return false; + } + + pClientRect->left = topLeft.x; + pClientRect->top = topLeft.y; + pClientRect->right = bottomRight.x; + pClientRect->bottom = bottomRight.y; + return true; +} + +static bool GetParentRenderOrigin(HWND hWnd, POINT& origin) { + origin = { 0, 0 }; + + if (!hWnd || hWnd == Game::hWnd) + return true; + + RECT parentRect {}; + if (!RenderDX::GetWindowRectInRender(hWnd, &parentRect)) + return false; + + origin.x = parentRect.left; + origin.y = parentRect.top; + return true; +} + +static bool GetWindowRectInParentRender(HWND hWnd, LPRECT pRect) { + if (!RenderDX::GetWindowRectInRender(hWnd, pRect)) + return false; + + POINT parentOrigin {}; + if (!GetParentRenderOrigin(::GetParent(hWnd), parentOrigin)) + return false; + + pRect->left -= parentOrigin.x; + pRect->right -= parentOrigin.x; + pRect->top -= parentOrigin.y; + pRect->bottom -= parentOrigin.y; + return true; +} + +static bool RenderLocalRectToWindowRect(HWND hWnd, const RECT& localRect, LPRECT pWindowRect) { + const HWND parentHwnd = ::GetParent(hWnd); + POINT parentOrigin {}; + if (!GetParentRenderOrigin(parentHwnd, parentOrigin)) + return false; + + const RECT renderRect + { + parentOrigin.x + localRect.left, + parentOrigin.y + localRect.top, + parentOrigin.x + localRect.right, + parentOrigin.y + localRect.bottom + }; + + return RenderDX::RenderRectToClient(parentHwnd, renderRect, pWindowRect); +} + +BOOL __fastcall RenderDX::MoveWindowInRender(HWND hWnd, int x, int y, int width, int height, BOOL repaint) { + if (!hWnd) + return FALSE; + + RECT clientRect {}; + const RECT renderRect { x, y, x + width, y + height }; + if (!RenderLocalRectToWindowRect(hWnd, renderRect, &clientRect)) + return FALSE; + + return ::MoveWindow( + hWnd, + clientRect.left, + clientRect.top, + clientRect.right - clientRect.left, + clientRect.bottom - clientRect.top, + repaint); +} + +BOOL __fastcall RenderDX::SetWindowPosInRender(HWND hWnd, HWND hWndInsertAfter, int x, int y, int cx, int cy, UINT flags) { + if (!hWnd) + return FALSE; + + RECT renderRect {}; + if (!GetWindowRectInParentRender(hWnd, &renderRect)) + return FALSE; + + if (!(flags & SWP_NOMOVE)) { + const int width = renderRect.right - renderRect.left; + const int height = renderRect.bottom - renderRect.top; + renderRect.left = x; + renderRect.top = y; + renderRect.right = x + width; + renderRect.bottom = y + height; + } + + if (!(flags & SWP_NOSIZE)) { + renderRect.right = renderRect.left + cx; + renderRect.bottom = renderRect.top + cy; + } + + RECT clientRect {}; + if (!RenderLocalRectToWindowRect(hWnd, renderRect, &clientRect)) + return FALSE; + + const int clientWidth = clientRect.right - clientRect.left; + const int clientHeight = clientRect.bottom - clientRect.top; + return ::SetWindowPos( + hWnd, + hWndInsertAfter, + clientRect.left, + clientRect.top, + clientWidth, + clientHeight, + flags); +} + +bool __fastcall RenderDX::IsOwnerDrawUsingRawWindowCoordinates() { + return OwnerDrawRawWindowCoordinates; +} + +void __fastcall RenderDX::SetOwnerDrawRawWindowCoordinates(bool enabled) { + OwnerDrawRawWindowCoordinates = enabled; } void __fastcall RenderDX::UpdateScale() { + if (!OwnerDrawRectsAlreadyCaptured) + WWUI::CaptureOwnerDrawWindowRects(); + const float viewportWidth = DXRenderer::Instance().GetViewportWidth(); const float viewportHeight = DXRenderer::Instance().GetViewportHeight(); - ViewportX = DXRenderer::Instance().GetViewportX(); - ViewportY = DXRenderer::Instance().GetViewportY(); + const float viewportX = DXRenderer::Instance().GetViewportX(); + const float viewportY = DXRenderer::Instance().GetViewportY(); if (Drawing::RenderWidth <= 0 || Drawing::RenderHeight <= 0 || viewportWidth <= 0.0f || viewportHeight <= 0.0f) { ResetScale(); + WWUI::ApplyOwnerDrawWindowRects(); return; } + ViewportX = viewportX; + ViewportY = viewportY; ScaleX = static_cast(Drawing::RenderWidth) / viewportWidth; ScaleY = static_cast(Drawing::RenderHeight) / viewportHeight; + WWUI::ApplyOwnerDrawWindowRects(); } void __fastcall RenderDX::ResetScale() { @@ -544,6 +991,11 @@ int* __fastcall RenderDX::EnumDisplayModes(DWORD minWidth, DWORD minHeight, DWOR void __fastcall RenderDX::MainProcHandlePaint() { if (DXMouse::Instance && DSurface::Primary && DSurface::Hidden && DSurface::Composite) { if (Unsorted::ScenarioStarted) { + if (WWUI::HasActiveOwnerDrawDialog()) { + RenderDX::UpdateScreen(DSurface::Primary); + return; + } + GScreenClass::UpdatePrimarySurface(DXMouse::Instance->IsCaptured(), DSurface::Composite, nullptr); SidebarClass::Instance.BlitSidebar(true); } diff --git a/src/Render/Functions.h b/src/Render/Functions.h index 6d679a00e8..92bb2111c8 100644 --- a/src/Render/Functions.h +++ b/src/Render/Functions.h @@ -18,6 +18,29 @@ class RenderDX { static float __fastcall GetYScale(); static int __fastcall ClientToRenderX(int x); static int __fastcall ClientToRenderY(int y); + static int __fastcall ClientToRenderXUnclamped(int x); + static int __fastcall ClientToRenderYUnclamped(int y); + static int __fastcall RenderToClientX(int x); + static int __fastcall RenderToClientY(int y); + static POINT __fastcall ClientToRenderPoint(POINT point, bool clamp); + static POINT __fastcall RenderToClientPoint(POINT point); + static RECT __fastcall ClientToRenderRect(const RECT& rect, bool clamp); + static RECT __fastcall RenderToClientRect(const RECT& rect); + static bool __fastcall ScreenToRenderPoint(LPPOINT pPoint, bool clamp); + static bool __fastcall RenderToScreenPoint(LPPOINT pPoint); + static bool __fastcall GetWindowRectInRender(HWND hWnd, LPRECT pRect); + static bool __fastcall GetClientRectInRender(HWND hWnd, LPRECT pRect); + static POINT __fastcall MouseLParamToRenderLocalPoint(HWND hWnd, LPARAM lParam); + static LPARAM __fastcall MouseLParamToRenderLocal(HWND hWnd, LPARAM lParam); + static POINT __fastcall ScreenToRenderLocalPoint(HWND hWnd, POINT point); + static RECT __fastcall ClientToRenderLocalRect(HWND hWnd, const RECT& rect); + static RECT __fastcall RenderLocalToClientRect(HWND hWnd, const RECT& rect); + static bool __fastcall RenderRectToClient(HWND referenceHwnd, const RECT& renderRect, LPRECT pClientRect); + static BOOL __fastcall MoveWindowInRender(HWND hWnd, int x, int y, int width, int height, BOOL repaint); + static BOOL __fastcall SetWindowPosInRender(HWND hWnd, HWND hWndInsertAfter, int x, int y, int cx, int cy, UINT flags); + static bool __fastcall IsOwnerDrawUsingRawWindowCoordinates(); + static void __fastcall SetOwnerDrawRawWindowCoordinates(bool enabled); + static bool __fastcall HandleFullscreenToggleMessage(UINT message, WPARAM wParam, LPARAM lParam); static void __fastcall UpdateScale(); static void __fastcall ResetScale(); static int* __fastcall EnumDisplayModes(DWORD minWidth, DWORD minHeight, DWORD maxWidth, DWORD maxHeight, DWORD bitDepth); diff --git a/src/Render/Mouse.cpp b/src/Render/Mouse.cpp index e0607fde59..d83d92dacf 100644 --- a/src/Render/Mouse.cpp +++ b/src/Render/Mouse.cpp @@ -143,18 +143,11 @@ void DXMouse::ProcessMouse() { if (!::GetCursorPos(&pt)) return; - if (!::ScreenToClient(Game::hWnd, &pt)) + if (!RenderDX::ScreenToRenderPoint(&pt, true)) return; - if (RenderDX::ShouldScale()) { - MouseX = RenderDX::ClientToRenderX(pt.x); - MouseY = RenderDX::ClientToRenderY(pt.y); - } - else { - MouseX = pt.x; - MouseY = pt.y; - } - + MouseX = pt.x; + MouseY = pt.y; } void DXMouse::RecalcCaptureRegion() { @@ -319,8 +312,5 @@ HCURSOR DXMouse::BuildCursor(const CursorData& data, int hotspotX, int hotspotY) } int DXMouse::GetCursorScale() { - if (!RenderDX::ShouldScale()) - return 1; - return std::max(1, static_cast(std::round(1.0f / RenderDX::GetYScale()))); } diff --git a/src/Render/Surface.cpp b/src/Render/Surface.cpp index f8d10f10b5..644c7a43b5 100644 --- a/src/Render/Surface.cpp +++ b/src/Render/Surface.cpp @@ -327,7 +327,7 @@ DXSurface* __fastcall DXSurface::CreatePrimary() { Debug::Log("[RenderDX] D3D11 surface created as primary surface.\n"); - auto surface = new DXSurface(Drawing::RenderWidth, Drawing::RenderHeight); + auto surface = GameCreate(Drawing::RenderWidth, Drawing::RenderHeight); Drawing::RedShiftLeft = 11; Drawing::RedShiftRight = 3; diff --git a/src/Render/WWUI.Hooks.cpp b/src/Render/WWUI.Hooks.cpp index bb37268b62..1217104ff2 100644 --- a/src/Render/WWUI.Hooks.cpp +++ b/src/Render/WWUI.Hooks.cpp @@ -1,101 +1,154 @@ #include +#include "Functions.h" + #include #include #include +#include + #ifdef CALL #undef CALL #endif -static BOOL WINAPI ClientToScreenHook(HWND hWnd, LPPOINT lpPoint) +static BOOL WINAPI ClientToScreenHook(HWND, LPPOINT) { + // The game also uses this IAT entry while blitting game and movie surfaces. + // Keep its historical surface-space semantics; explicit Phobos helpers use real Win32 conversion. return TRUE; } DEFINE_PATCH_TYPED(void*, 0x7E14B8, ClientToScreenHook); +static BOOL WINAPI GetClientRectHook(HWND hWnd, LPRECT rect) +{ + if (hWnd == Game::hWnd && rect && Drawing::RenderWidth > 0 && Drawing::RenderHeight > 0) + { + rect->left = 0; + rect->top = 0; + rect->right = Drawing::RenderWidth; + rect->bottom = Drawing::RenderHeight; + return TRUE; + } + + return ::GetClientRect(hWnd, rect); +} +DEFINE_PATCH_TYPED(void*, 0x7E14C4, GetClientRectHook); + static void __fastcall CenterWindowIn(HWND window, HWND parent) { - RECT parentRect; - ::GetClientRect(parent, &parentRect); + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + { + RECT parentRect {}; + ::GetClientRect(parent, &parentRect); + + if (parent == Game::hWnd) + { + parentRect.right = Drawing::RenderWidth; + parentRect.bottom = Drawing::RenderHeight; + } + + RECT rect {}; + ::GetClientRect(window, &rect); + int x = (parentRect.right - rect.right + 1) / 2; + int y = (parentRect.bottom - rect.bottom + 1) / 2; + x = std::max(x, 0); + y = std::max(y, 0); + + ::SetWindowPos(window, nullptr, x, y, -1, -1, SWP_NOSIZE | SWP_NOZORDER); + return; + } + + RECT parentRect {}; if (parent == Game::hWnd) { parentRect.right = Drawing::RenderWidth; parentRect.bottom = Drawing::RenderHeight; } + else if (!RenderDX::GetClientRectInRender(parent, &parentRect)) + { + return; + } - ::ClientToScreen(parent, reinterpret_cast(&parentRect)); - ::ClientToScreen(parent, reinterpret_cast(&parentRect.right)); - parentRect.right -= parentRect.left; - parentRect.bottom -= parentRect.top; + RECT rect {}; + if (!RenderDX::GetClientRectInRender(window, &rect)) + return; - RECT rect; - ::GetClientRect(window, &rect); - ::ClientToScreen(window, reinterpret_cast(&rect)); - ::ClientToScreen(window, reinterpret_cast(&rect.right)); - rect.right -= rect.left; - rect.bottom -= rect.top; - int x = (parentRect.right - rect.right + 1) / 2; - int y = (parentRect.bottom - rect.bottom + 1) / 2; + const int parentWidth = parentRect.right - parentRect.left; + const int parentHeight = parentRect.bottom - parentRect.top; + const int width = rect.right - rect.left; + const int height = rect.bottom - rect.top; + int x = (parentWidth - width + 1) / 2; + int y = (parentHeight - height + 1) / 2; x = std::max(x, 0); y = std::max(y, 0); - ::SetWindowPos(window, nullptr, x, y, -1, -1, SWP_NOSIZE | SWP_NOZORDER); + RenderDX::SetWindowPosInRender(window, nullptr, x, y, -1, -1, SWP_NOSIZE | SWP_NOZORDER); } DEFINE_FUNCTION_JUMP(LJMP, 0x777080, CenterWindowIn); +static BOOL GetRawWindowRectInRenderLayout(HWND hWnd, LPRECT rect); + static BOOL __fastcall MoveDialog(HWND window, int x, int y) { - int xPos; - int yPos; + RECT windowRect {}; + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + { + if (!GetRawWindowRectInRenderLayout(window, &windowRect)) + return FALSE; + } + else if (!RenderDX::GetWindowRectInRender(window, &windowRect)) + { + return FALSE; + } - RECT screenRect; - screenRect.left = 0; - screenRect.top = 0; - screenRect.right = Drawing::RenderWidth; - screenRect.bottom = Drawing::RenderHeight; + const int width = windowRect.right - windowRect.left; + const int height = windowRect.bottom - windowRect.top; + const int xPos = x == -1 ? windowRect.left : x; + const int yPos = y == -1 ? windowRect.top : y; + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + return ::MoveWindow(window, xPos, yPos, width, height, FALSE); - ::ClientToScreen(Game::hWnd, reinterpret_cast(&screenRect)); - ::ClientToScreen(Game::hWnd, reinterpret_cast(&screenRect.right)); + return RenderDX::MoveWindowInRender(window, xPos, yPos, width, height, FALSE); +} +DEFINE_FUNCTION_JUMP(LJMP, 0x623170, MoveDialog); - RECT windowRect; - ::GetWindowRect(window, &windowRect); +static BOOL GetRawWindowRectInRenderLayout(HWND hWnd, LPRECT rect) +{ + if (!hWnd || !rect) + return FALSE; - windowRect.right -= windowRect.left; - windowRect.bottom -= windowRect.top; + if (hWnd == Game::hWnd) + { + rect->left = 0; + rect->top = 0; + rect->right = Drawing::RenderWidth; + rect->bottom = Drawing::RenderHeight; + return TRUE; + } - if (x == -1) - xPos = windowRect.left - screenRect.left; - else - xPos = x; - windowRect.left = xPos; + if (!::GetWindowRect(hWnd, rect)) + return FALSE; - if (y == -1) - yPos = windowRect.top - screenRect.top; - else - yPos = y; - windowRect.top = yPos; + POINT origin { 0, 0 }; + if (!::ClientToScreen(Game::hWnd, &origin)) + return FALSE; - return ::MoveWindow(window, windowRect.left, windowRect.top, windowRect.right, windowRect.bottom, FALSE); + rect->left -= origin.x; + rect->right -= origin.x; + rect->top -= origin.y; + rect->bottom -= origin.y; + return TRUE; } -DEFINE_FUNCTION_JUMP(LJMP, 0x623170, MoveDialog); static BOOL __fastcall WinDialogGetRectangle(HWND hWnd, LPRECT rect) { - BOOL result = ::GetWindowRect(hWnd, rect); - if (result) - { - RECT client; - ::GetClientRect(Game::hWnd, &client); - ::ClientToScreen(Game::hWnd, reinterpret_cast(&client)); - rect->left -= client.left; - rect->right -= client.left; - rect->top -= client.top; - rect->bottom -= client.top; - } - return result; + if (RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + return GetRawWindowRectInRenderLayout(hWnd, rect); + + return RenderDX::GetWindowRectInRender(hWnd, rect); } DEFINE_FUNCTION_JUMP(LJMP, 0x775690, WinDialogGetRectangle); @@ -105,6 +158,125 @@ static BOOL __fastcall GetWindowRectHook(HWND hWnd, LPRECT rect) } DEFINE_FUNCTION_JUMP(CALL, 0x610E77, GetWindowRectHook); +static BOOL WINAPI OwnerDrawPaintGetClientRectHook(HWND hWnd, LPRECT rect) +{ + if (!RenderDX::IsOwnerDrawUsingRawWindowCoordinates() && RenderDX::GetClientRectInRender(hWnd, rect)) + return TRUE; + + return ::GetClientRect(hWnd, rect); +} +DEFINE_FUNCTION_JUMP(CALL6, 0x621EF3, OwnerDrawPaintGetClientRectHook); + +static BOOL WINAPI ComboDropGetClientRectHook(HWND hWnd, LPRECT rect) +{ + if (!RenderDX::IsOwnerDrawUsingRawWindowCoordinates() && RenderDX::GetClientRectInRender(hWnd, rect)) + return TRUE; + + return ::GetClientRect(hWnd, rect); +} +DEFINE_FUNCTION_JUMP(CALL6, 0x60D58E, ComboDropGetClientRectHook); + +static BOOL WINAPI ComboDropGetWindowRectHook(HWND hWnd, LPRECT rect) +{ + if (!RenderDX::IsOwnerDrawUsingRawWindowCoordinates() && RenderDX::GetWindowRectInRender(hWnd, rect)) + return TRUE; + + return ::GetWindowRect(hWnd, rect); +} +DEFINE_FUNCTION_JUMP(CALL6, 0x60E049, ComboDropGetWindowRectHook); + +static BOOL WINAPI ComboDropSetWindowPosHook( + HWND hWnd, + HWND hWndInsertAfter, + int x, + int y, + int cx, + int cy, + UINT flags) +{ + if (!RenderDX::IsOwnerDrawUsingRawWindowCoordinates()) + return RenderDX::SetWindowPosInRender(hWnd, hWndInsertAfter, x, y, cx, cy, flags); + + return ::SetWindowPos(hWnd, hWndInsertAfter, x, y, cx, cy, flags); +} +DEFINE_FUNCTION_JUMP(CALL6, 0x60E84A, ComboDropSetWindowPosHook); +DEFINE_FUNCTION_JUMP(CALL6, 0x60F1BE, ComboDropSetWindowPosHook); + +static bool IsComboDropMousePointMessage(UINT message) +{ + return message >= WM_MOUSEFIRST && message <= WM_MBUTTONDBLCLK; +} + +static WNDPROC GetComboDropWindowProc() +{ + return reinterpret_cast(0x60D540); +} + +static LRESULT CALLBACK ComboDropWindowProcHook(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (RenderDX::HandleFullscreenToggleMessage(message, wParam, lParam)) + return 0; + + if (!RenderDX::IsOwnerDrawUsingRawWindowCoordinates() && IsComboDropMousePointMessage(message)) + lParam = RenderDX::MouseLParamToRenderLocal(hWnd, lParam); + + return GetComboDropWindowProc()(hWnd, message, wParam, lParam); +} +DEFINE_PATCH_TYPED(void*, 0x60D4A2, ComboDropWindowProcHook); + +static HWND WINAPI ComboDropCreateWindowExAHook( + DWORD exStyle, + LPCSTR className, + LPCSTR windowName, + DWORD style, + int x, + int y, + int width, + int height, + HWND parentHwnd, + HMENU menu, + HINSTANCE instance, + LPVOID param) +{ + if (!RenderDX::IsOwnerDrawUsingRawWindowCoordinates() && parentHwnd) + { + RECT parentRect {}; + RECT clientRect {}; + const RECT localRect { x, y, x + width, y + height }; + if (RenderDX::GetWindowRectInRender(parentHwnd, &parentRect) + && RenderDX::RenderRectToClient( + parentHwnd, + RECT { + parentRect.left + localRect.left, + parentRect.top + localRect.top, + parentRect.left + localRect.right, + parentRect.top + localRect.bottom + }, + &clientRect)) + { + x = clientRect.left; + y = clientRect.top; + width = clientRect.right - clientRect.left; + height = clientRect.bottom - clientRect.top; + } + } + + return ::CreateWindowExA( + exStyle, + className, + windowName, + style, + x, + y, + width, + height, + parentHwnd, + menu, + instance, + param); +} +DEFINE_FUNCTION_JUMP(CALL6, 0x60E72F, ComboDropCreateWindowExAHook); + static BOOL __fastcall MoveIngameWindowControls(HWND hWnd) { if (!SessionClass::Instance.CurrentlyInGame) @@ -114,7 +286,7 @@ static BOOL __fastcall MoveIngameWindowControls(HWND hWnd) RECT rect; RECT parentRect; - if (!parent || !::GetWindowRect(hWnd, &rect) || !::GetWindowRect(parent, &parentRect)) + if (!parent || !RenderDX::GetWindowRectInRender(hWnd, &rect) || !RenderDX::GetWindowRectInRender(parent, &parentRect)) return FALSE; int x = rect.left - parentRect.left + (parentRect.right - parentRect.left - 800) / 2; @@ -124,6 +296,6 @@ static BOOL __fastcall MoveIngameWindowControls(HWND hWnd) if (y < 0) y = 0; - return ::MoveWindow(hWnd, x, y, rect.right - rect.left, rect.bottom - rect.top, FALSE); + return RenderDX::MoveWindowInRender(hWnd, x, y, rect.right - rect.left, rect.bottom - rect.top, FALSE); } DEFINE_FUNCTION_JUMP(LJMP, 0x60B7A0, MoveIngameWindowControls); From e55f1b6cc5c0a65c1d9a982d316e5cca89abfe5b Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Fri, 29 May 2026 03:24:13 +0800 Subject: [PATCH 17/21] Automatically set HighDpiAwareness before creating main window --- src/Render/Functions.cpp | 409 ++++++++++++++++++++++++++++----------- src/Render/Functions.h | 1 + 2 files changed, 301 insertions(+), 109 deletions(-) diff --git a/src/Render/Functions.cpp b/src/Render/Functions.cpp index 93ac698f04..e2506d4a4f 100644 --- a/src/Render/Functions.cpp +++ b/src/Render/Functions.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -24,6 +25,87 @@ DEFINE_REFERENCE(RectangleStruct, DisplayVisibleRect, 0x886FB0u) static bool OwnerDrawRectsAlreadyCaptured = false; +void __fastcall RenderDX::SetHighDPIAwareness() +{ + Debug::Log("[RenderDX] Setting high DPI awareness\n"); + + using SetProcessDpiAwarenessContextFunc = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT); + using SetProcessDpiAwarenessFunc = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS); + using SetProcessDPIAwareFunc = BOOL(WINAPI*)(); + + const HMODULE hUser32 = ::GetModuleHandleA("user32.dll"); + const HMODULE hShcore = ::GetModuleHandleA("shcore.dll"); + + // Try to set the highest level of DPI awareness available, but don't fail if it's not supported (e.g. on Windows 7) + if (hUser32) + { + const auto FnSetProcessDpiAwarenessContext = reinterpret_cast( + ::GetProcAddress(hUser32, "SetProcessDpiAwarenessContext")); + + if (FnSetProcessDpiAwarenessContext) + { + Debug::Log("[RenderDX] Setting DPI awareness context to PER_MONITOR_AWARE_V2\n"); + if (!FnSetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) + { + DWORD error = ::GetLastError(); + if (error == ERROR_ACCESS_DENIED) + Debug::Log("[RenderDX] SetProcessDpiAwarenessContext failed: Access denied (already set to a different context)\n"); + else + Debug::Log("[RenderDX] SetProcessDpiAwarenessContext failed: %d\n", error); + } + else + { + + Debug::Log("[RenderDX] SetProcessDpiAwarenessContext succeeded\n"); + return; + } + } + } + + // If SetProcessDpiAwarenessContext is not available or failed, try SetProcessDpiAwareness + if (hShcore) + { + const auto FnSetProcessDpiAwareness = reinterpret_cast( + ::GetProcAddress(hShcore, "SetProcessDpiAwareness")); + + if (FnSetProcessDpiAwareness) + { + Debug::Log("[RenderDX] Setting process DPI awareness to PROCESS_PER_MONITOR_DPI_AWARE\n"); + const HRESULT result = FnSetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); + if (FAILED(result)) + { + if (result == E_ACCESSDENIED) + Debug::Log("[RenderDX] SetProcessDpiAwareness failed: Access denied (already set to a different level of awareness)\n"); + else + Debug::Log("[RenderDX] SetProcessDpiAwareness failed: 0x%08X\n", static_cast(result)); + } + else + { + Debug::Log("[RenderDX] SetProcessDpiAwareness succeeded\n"); + return; + } + } + } + + // If neither of the above are available, fall back to SetProcessDPIAware (Windows 7 and earlier) + if (hUser32) + { + const auto FnSetProcessDPIAware = reinterpret_cast( + ::GetProcAddress(hUser32, "SetProcessDPIAware")); + + if (FnSetProcessDPIAware) + { + Debug::Log("[RenderDX] Setting process DPI awareness with SetProcessDPIAware\n"); + if (!FnSetProcessDPIAware()) + Debug::Log("[RenderDX] SetProcessDPIAware failed: %d\n", ::GetLastError()); + else + Debug::Log("[RenderDX] SetProcessDPIAware succeeded\n"); + } + } + + Debug::Log("[RenderDX] Failed to set high DPI awareness. The application may not scale correctly on high DPI displays.\n"); +} + static void ReleasePrimarySurface() { auto pPrimary = DSurface::Primary; @@ -35,70 +117,82 @@ static void ReleasePrimarySurface() GameDelete(pPrimary); } -bool __fastcall RenderDX::AllocateSurfaces(const RectangleStruct& hiddenRect, const RectangleStruct& compositeRect, const RectangleStruct& tileRect, const RectangleStruct& sidebarRect, bool hiddenFirst) { +bool __fastcall RenderDX::AllocateSurfaces(const RectangleStruct& hiddenRect, const RectangleStruct& compositeRect, const RectangleStruct& tileRect, const RectangleStruct& sidebarRect, bool hiddenFirst) +{ Debug::Log("[RenderDX] Allocating new surfaces\n"); - if (DSurface::Alternate) { + if (DSurface::Alternate) + { Debug::Log("[RenderDX] Deleting AlternateSurface\n"); GameDelete(DSurface::Alternate); DSurface::Alternate = nullptr; } - if (DSurface::Hidden) { + if (DSurface::Hidden) + { Debug::Log("[RenderDX] Deleting HiddenSurface\n"); GameDelete(DSurface::Hidden); DSurface::Hidden = nullptr; } - if (DSurface::Composite) { + if (DSurface::Composite) + { Debug::Log("[RenderDX] Deleting CompositeSurface\n"); GameDelete(DSurface::Composite); DSurface::Composite = nullptr; } - if (DSurface::Tile) { + if (DSurface::Tile) + { Debug::Log("[RenderDX] Deleting TileSurface\n"); GameDelete(DSurface::Tile); DSurface::Tile = nullptr; } - if (DSurface::Sidebar) { + if (DSurface::Sidebar) + { Debug::Log("[RenderDX] Deleting SidebarSurface\n"); GameDelete(DSurface::Sidebar); DSurface::Sidebar = nullptr; } - if (hiddenFirst && hiddenRect.Width > 0 && hiddenRect.Height > 0) { + if (hiddenFirst && hiddenRect.Width > 0 && hiddenRect.Height > 0) + { DSurface::Hidden = GameCreate(hiddenRect.Width, hiddenRect.Height); DSurface::Hidden->Fill(0); Debug::Log("[RenderDX] HiddenSurface (%dx%d)\n", hiddenRect.Width, hiddenRect.Height); } - if (compositeRect.Width > 0 && compositeRect.Height > 0) { + if (compositeRect.Width > 0 && compositeRect.Height > 0) + { DSurface::Composite = GameCreate(compositeRect.Width, compositeRect.Height); DSurface::Composite->Fill(0); Debug::Log("[RenderDX] CompositeSurface (%dx%d)\n", compositeRect.Width, compositeRect.Height); } - if (tileRect.Width > 0 && tileRect.Height > 0) { + if (tileRect.Width > 0 && tileRect.Height > 0) + { DSurface::Tile = GameCreate(tileRect.Width, tileRect.Height); DSurface::Tile->Fill(0); Debug::Log("[RenderDX] TileSurface (%dx%d)\n", tileRect.Width, tileRect.Height); } - if (sidebarRect.Width > 0 && sidebarRect.Height > 0) { + if (sidebarRect.Width > 0 && sidebarRect.Height > 0) + { DSurface::Sidebar = GameCreate(sidebarRect.Width, sidebarRect.Height); DSurface::Sidebar->Fill(0); Debug::Log("[RenderDX] SidebarSurface (%dx%d)\n", sidebarRect.Width, sidebarRect.Height); } - if (!hiddenFirst && hiddenRect.Width > 0 && hiddenRect.Height > 0) { + if (!hiddenFirst && hiddenRect.Width > 0 && hiddenRect.Height > 0) + { DSurface::Hidden = GameCreate(hiddenRect.Width, hiddenRect.Height); DSurface::Hidden->Fill(0); Debug::Log("[RenderDX] HiddenSurface (%dx%d)\n", hiddenRect.Width, hiddenRect.Height); } - if (hiddenRect.Width > 0 && hiddenRect.Height > 0) { + if (hiddenRect.Width > 0 && hiddenRect.Height > 0) + { DSurface::Alternate = GameCreate(hiddenRect.Width, hiddenRect.Height); DSurface::Alternate->Fill(0); Debug::Log("[RenderDX] AlternateSurface (%dx%d)\n", hiddenRect.Width, hiddenRect.Height); @@ -107,10 +201,12 @@ bool __fastcall RenderDX::AllocateSurfaces(const RectangleStruct& hiddenRect, co return true; } -bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bitsPerPixel) { +bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bitsPerPixel) +{ Debug::Log("[RenderDX] Setting video mode to %dx%d@%d\n", width, height, bitsPerPixel); - if (!DXRenderer::Instance().IsRendererReady()) { + if (!DXRenderer::Instance().IsRendererReady()) + { Debug::Log("[RenderDX] Renderer is not ready\n"); return false; } @@ -119,7 +215,8 @@ bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bitsPerP OwnerDrawRectsAlreadyCaptured = true; ResetVideoMode(); - if (!DXRenderer::Instance().CreateRenderer(width, height, bitsPerPixel)) { + if (!DXRenderer::Instance().CreateRenderer(width, height, bitsPerPixel)) + { OwnerDrawRectsAlreadyCaptured = false; Debug::Log("[RenderDX] Failed to create renderer\n"); return false; @@ -137,7 +234,8 @@ bool __fastcall RenderDX::SetVideoMode(HWND, int width, int height, int bitsPerP return true; } -void __fastcall RenderDX::ResetVideoMode() { +void __fastcall RenderDX::ResetVideoMode() +{ Debug::Log("[RenderDX] Resetting video mode\n"); ReleasePrimarySurface(); @@ -155,7 +253,8 @@ static bool DeferredWindowResize = false; static int DeferredWindowWidth = 0; static int DeferredWindowHeight = 0; -static void RecalcMouseWindowRegion(bool rebuildCursor) { +static void RecalcMouseWindowRegion(bool rebuildCursor) +{ if (!DXMouse::Instance) return; @@ -164,11 +263,13 @@ static void RecalcMouseWindowRegion(bool rebuildCursor) { DXMouse::Instance->RebuildCursorImage(); } -bool __fastcall RenderDX::HandleFullscreenToggleMessage(UINT message, WPARAM wParam, LPARAM lParam) { +bool __fastcall RenderDX::HandleFullscreenToggleMessage(UINT message, WPARAM wParam, LPARAM lParam) +{ if (wParam != VK_RETURN || !(lParam & (1 << 29))) return false; - if (message == WM_SYSKEYDOWN) { + if (message == WM_SYSKEYDOWN) + { DXRenderer::Instance().ToggleFullscreen(); RecalcMouseWindowRegion(true); return true; @@ -184,13 +285,16 @@ static void ReinitMenuLayoutRects(int width, int height) reinterpret_cast(0x72E1B0)(width, height); } -static void ApplyWindowResize(int width, int height) { +static void ApplyWindowResize(int width, int height) +{ DXRenderer::Instance().ResizeWindow(width, height); RecalcMouseWindowRegion(true); } -static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - switch (message) { +static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { case WM_MOUSEMOVE: case WM_LBUTTONDOWN: case WM_LBUTTONUP: @@ -206,7 +310,8 @@ static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, L case WM_XBUTTONUP: { // Scale mouse inputs before they are processed by SDL or the game. - if (RenderDX::ShouldScale()) { + if (RenderDX::ShouldScale()) + { POINT point { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; if (message == WM_MOUSEWHEEL) @@ -221,7 +326,8 @@ static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, L case WM_MOVE: { - if (DXMouse::Instance) { + if (DXMouse::Instance) + { DXMouse::Instance->RecalcCaptureRegion(); } return 0; // handled @@ -240,12 +346,14 @@ static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, L { WindowResizeInProgress = false; - if (DeferredWindowResize) { + if (DeferredWindowResize) + { int width = DeferredWindowWidth; int height = DeferredWindowHeight; RECT clientRect {}; - if (::GetClientRect(hWnd, &clientRect)) { + if (::GetClientRect(hWnd, &clientRect)) + { width = clientRect.right - clientRect.left; height = clientRect.bottom - clientRect.top; } @@ -265,17 +373,20 @@ static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, L { const int width = LOWORD(lParam); const int height = HIWORD(lParam); - if (wParam == SIZE_MINIMIZED || width == 0 || height == 0) { + if (wParam == SIZE_MINIMIZED || width == 0 || height == 0) + { DeferredWindowResize = false; RecalcMouseWindowRegion(false); } - else if (WindowResizeInProgress) { + else if (WindowResizeInProgress) + { DeferredWindowResize = true; DeferredWindowWidth = width; DeferredWindowHeight = height; RecalcMouseWindowRegion(false); } - else { + else + { ApplyWindowResize(width, height); } @@ -294,7 +405,8 @@ static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, L case WM_SETCURSOR: { // Prevent the system from setting the cursor when it's over our window, since we handle it ourselves. - if (LOWORD(lParam) == HTCLIENT) { + if (LOWORD(lParam) == HTCLIENT) + { if (DXMouse::Instance) DXMouse::Instance->SetCachedCursor(); return TRUE; // handled @@ -306,14 +418,17 @@ static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, L { if (RenderOptions::Config().PauseGameWhenLoseFocus) break; // goto the original window procedure to allow the game to pause when losing focus - if (hWnd == Game::hWnd) { + if (hWnd == Game::hWnd) + { Unsorted::GameInFocus = true; // game is always active - if (wParam) { + if (wParam) + { Debug::Log("[RenderDX] Game window activated\n"); if (DXMouse::Instance) DXMouse::Instance->CaptureMouse(); } - else { + else + { Debug::Log("[RenderDX] Game window deactivated\n"); if (DXMouse::Instance) DXMouse::Instance->ReleaseMouse(); @@ -327,22 +442,28 @@ static LRESULT CALLBACK MainWindowProc(HWND hWnd, UINT message, WPARAM wParam, L return reinterpret_cast(0x7775C0)(hWnd, message, wParam, lParam); } -void __fastcall RenderDX::CreateMainWindow(HINSTANCE instance, int cmdShow, int width, int height) { +void __fastcall RenderDX::CreateMainWindow(HINSTANCE instance, int cmdShow, int width, int height) +{ Debug::Log("[RenderDX] Creating main window\n"); - if (!DXRenderer::Instance().CreateMainWindow(instance, cmdShow, width, height, MainWindowProc)) { + SetHighDPIAwareness(); + if (!DXRenderer::Instance().CreateMainWindow(instance, cmdShow, width, height, MainWindowProc)) + { Debug::Log("[RenderDX] Failed to create main window\n"); ::MessageBoxA(nullptr, "Failed to create main window", "Error", MB_ICONERROR); ::ExitProcess(0xC0DEBEEF); } } -void __fastcall RenderDX::DestroyMainWindow() { +void __fastcall RenderDX::DestroyMainWindow() +{ Debug::Log("[RenderDX] Destroying main window\n"); DXRenderer::Instance().DestroyMainWindow(); } -bool __fastcall RenderDX::UpdateScreen(Surface* pSurface) { - if (!pSurface) { +bool __fastcall RenderDX::UpdateScreen(Surface* pSurface) +{ + if (!pSurface) + { Debug::Log("[RenderDX] UpdateScreen called with null surface\n"); return false; } @@ -351,8 +472,10 @@ bool __fastcall RenderDX::UpdateScreen(Surface* pSurface) { DXRenderer::Instance().SetRenderScale(shouldScale); // Retrieve the game surface data - if (void* pPixels = pSurface->Lock(0, 0)) { - if (!DXRenderer::Instance().UploadSurfaceToTexture(pPixels, pSurface->GetPitch())) { + if (void* pPixels = pSurface->Lock(0, 0)) + { + if (!DXRenderer::Instance().UploadSurfaceToTexture(pPixels, pSurface->GetPitch())) + { Debug::Log("[RenderDX] Failed to upload surface to texture\n"); pSurface->Unlock(); return false; @@ -363,7 +486,8 @@ bool __fastcall RenderDX::UpdateScreen(Surface* pSurface) { static bool scaled = ShouldScale(); // Extra process on scaling change - if (scaled != shouldScale) { + if (scaled != shouldScale) + { scaled = shouldScale; if (DXMouse::Instance) DXMouse::Instance->RebuildCursorImage(); @@ -374,11 +498,13 @@ bool __fastcall RenderDX::UpdateScreen(Surface* pSurface) { return true; } -bool __fastcall RenderDX::ShouldScale() { +bool __fastcall RenderDX::ShouldScale() +{ return true; } -static RectangleStruct GetSidebarClipBounds(const RectangleStruct& viewRect) { +static RectangleStruct GetSidebarClipBounds(const RectangleStruct& viewRect) +{ constexpr int sidebarWidth = 168; constexpr int bottomBarHeight = 32; @@ -391,7 +517,8 @@ static RectangleStruct GetSidebarClipBounds(const RectangleStruct& viewRect) { }; } -static void RebuildDisplayState(const RectangleStruct& viewRect) { +static void RebuildDisplayState(const RectangleStruct& viewRect) +{ constexpr int sidebarWidth = 168; const auto sidebarRect = GetSidebarClipBounds(viewRect); const RectangleStruct tacticalSurfaceRect { 0, 0, sidebarRect.Width, viewRect.Height }; @@ -413,7 +540,8 @@ static void RebuildDisplayState(const RectangleStruct& viewRect) { ); DSurface::Temp = DSurface::Hidden; - if (DXMouse::Instance) { + if (DXMouse::Instance) + { DXMouse::Instance->RebuildCursorImage(); } @@ -429,13 +557,16 @@ static void RebuildDisplayState(const RectangleStruct& viewRect) { WWUI::RelayoutWindowsAfterDisplayModeChange(); } -bool __fastcall RenderDX::ChangeDisplayMode(int width, int height) { +bool __fastcall RenderDX::ChangeDisplayMode(int width, int height) +{ Debug::Log("[RenderDX] Changing display mode to %dx%d\n", width, height); // Save current window position RectangleStruct oldRect = DSurface::ViewBounds; - if (oldRect.Width <= 0 || oldRect.Height <= 0) { - if (Drawing::RenderWidth > 0 && Drawing::RenderHeight > 0) { + if (oldRect.Width <= 0 || oldRect.Height <= 0) + { + if (Drawing::RenderWidth > 0 && Drawing::RenderHeight > 0) + { Debug::Log("[RenderDX] Current view bounds are invalid, using RenderWidth/RenderHeight\n"); oldRect = RectangleStruct { 0, 0, Drawing::RenderWidth, Drawing::RenderHeight }; } @@ -454,7 +585,8 @@ bool __fastcall RenderDX::ChangeDisplayMode(int width, int height) { ReleasePrimarySurface(); - if (DXRenderer::Instance().IsWindowed()) { + if (DXRenderer::Instance().IsWindowed()) + { int windowWidth = width; int windowHeight = height; @@ -477,22 +609,27 @@ bool __fastcall RenderDX::ChangeDisplayMode(int width, int height) { } // Recreate all intermediates - if (!SetVideoMode(Game::hWnd, width, height, 16)) { - if (DXRenderer::Instance().IsWindowed()) { + if (!SetVideoMode(Game::hWnd, width, height, 16)) + { + if (DXRenderer::Instance().IsWindowed()) + { DXRenderer::Instance().MoveWindow(oldWindowX, oldWindowY, oldWindowWidth, oldWindowHeight); Debug::Log("[RenderDX] Restore window to (%d, %d) with size %dx%d\n", oldWindowX, oldWindowY, oldWindowWidth, oldWindowHeight); } - if (oldRect.Width > 0 && oldRect.Height > 0 && oldRenderWidth > 0 && oldRenderHeight > 0) { + if (oldRect.Width > 0 && oldRect.Height > 0 && oldRenderWidth > 0 && oldRenderHeight > 0) + { Debug::Log("[RenderDX] Restoring old display mode.\n"); - if (!SetVideoMode(Game::hWnd, oldRenderWidth, oldRenderHeight, oldRenderBpp)) { + if (!SetVideoMode(Game::hWnd, oldRenderWidth, oldRenderHeight, oldRenderBpp)) + { Debug::Log("[RenderDX] Failed to restore old display mode.\n"); DXMouse::Instance->ShowMouse(); return false; } RebuildDisplayState(oldRect); } - else { + else + { Debug::Log("[RenderDX] Old view bounds are invalid, cannot restore\n"); } @@ -514,35 +651,42 @@ static float ViewportX = 0.0f; static float ViewportY = 0.0f; static bool OwnerDrawRawWindowCoordinates = false; -float __fastcall RenderDX::GetXScale() { +float __fastcall RenderDX::GetXScale() +{ return ScaleX; } -float __fastcall RenderDX::GetYScale() { +float __fastcall RenderDX::GetYScale() +{ return ScaleY; } -static int ClampRenderX(int x) { +static int ClampRenderX(int x) +{ if (Drawing::RenderWidth <= 0) return x; return std::clamp(x, 0, Drawing::RenderWidth - 1); } -static int ClampRenderY(int y) { +static int ClampRenderY(int y) +{ if (Drawing::RenderHeight <= 0) return y; return std::clamp(y, 0, Drawing::RenderHeight - 1); } -static RECT ClampRenderRect(const RECT& rect) { +static RECT ClampRenderRect(const RECT& rect) +{ RECT result = rect; - if (Drawing::RenderWidth > 0) { + if (Drawing::RenderWidth > 0) + { result.left = std::clamp(result.left, 0, static_cast(Drawing::RenderWidth)); result.right = std::clamp(result.right, 0, static_cast(Drawing::RenderWidth)); } - if (Drawing::RenderHeight > 0) { + if (Drawing::RenderHeight > 0) + { result.top = std::clamp(result.top, 0, static_cast(Drawing::RenderHeight)); result.bottom = std::clamp(result.bottom, 0, static_cast(Drawing::RenderHeight)); } @@ -576,61 +720,70 @@ static RECT ClientToRenderRectRounded(const RECT& rect) }; } -int __fastcall RenderDX::ClientToRenderX(int x) { +int __fastcall RenderDX::ClientToRenderX(int x) +{ if (Drawing::RenderWidth <= 0) return x; return ClampRenderX(ClientToRenderXUnclamped(x)); } -int __fastcall RenderDX::ClientToRenderY(int y) { +int __fastcall RenderDX::ClientToRenderY(int y) +{ if (Drawing::RenderHeight <= 0) return y; return ClampRenderY(ClientToRenderYUnclamped(y)); } -int __fastcall RenderDX::ClientToRenderXUnclamped(int x) { +int __fastcall RenderDX::ClientToRenderXUnclamped(int x) +{ if (Drawing::RenderWidth <= 0) return x; return static_cast((static_cast(x) - ViewportX) * ScaleX); } -int __fastcall RenderDX::ClientToRenderYUnclamped(int y) { +int __fastcall RenderDX::ClientToRenderYUnclamped(int y) +{ if (Drawing::RenderHeight <= 0) return y; return static_cast((static_cast(y) - ViewportY) * ScaleY); } -int __fastcall RenderDX::RenderToClientX(int x) { +int __fastcall RenderDX::RenderToClientX(int x) +{ if (Drawing::RenderWidth <= 0 || ScaleX == 0.0f) return x; return static_cast(std::lround(ViewportX + static_cast(x) / ScaleX)); } -int __fastcall RenderDX::RenderToClientY(int y) { +int __fastcall RenderDX::RenderToClientY(int y) +{ if (Drawing::RenderHeight <= 0 || ScaleY == 0.0f) return y; return static_cast(std::lround(ViewportY + static_cast(y) / ScaleY)); } -POINT __fastcall RenderDX::ClientToRenderPoint(POINT point, bool clamp) { +POINT __fastcall RenderDX::ClientToRenderPoint(POINT point, bool clamp) +{ point.x = clamp ? ClientToRenderX(point.x) : ClientToRenderXUnclamped(point.x); point.y = clamp ? ClientToRenderY(point.y) : ClientToRenderYUnclamped(point.y); return point; } -POINT __fastcall RenderDX::RenderToClientPoint(POINT point) { +POINT __fastcall RenderDX::RenderToClientPoint(POINT point) +{ point.x = RenderToClientX(point.x); point.y = RenderToClientY(point.y); return point; } -RECT __fastcall RenderDX::ClientToRenderRect(const RECT& rect, bool clamp) { +RECT __fastcall RenderDX::ClientToRenderRect(const RECT& rect, bool clamp) +{ RECT result { ClientToRenderXUnclamped(rect.left), @@ -642,7 +795,8 @@ RECT __fastcall RenderDX::ClientToRenderRect(const RECT& rect, bool clamp) { return clamp ? ClampRenderRect(result) : result; } -RECT __fastcall RenderDX::RenderToClientRect(const RECT& rect) { +RECT __fastcall RenderDX::RenderToClientRect(const RECT& rect) +{ if (Drawing::RenderWidth <= 0 || Drawing::RenderHeight <= 0 || ScaleX == 0.0f || ScaleY == 0.0f) return rect; @@ -655,7 +809,8 @@ RECT __fastcall RenderDX::RenderToClientRect(const RECT& rect) { }; } -bool __fastcall RenderDX::ScreenToRenderPoint(LPPOINT pPoint, bool clamp) { +bool __fastcall RenderDX::ScreenToRenderPoint(LPPOINT pPoint, bool clamp) +{ if (!pPoint) return false; @@ -666,7 +821,8 @@ bool __fastcall RenderDX::ScreenToRenderPoint(LPPOINT pPoint, bool clamp) { return true; } -bool __fastcall RenderDX::RenderToScreenPoint(LPPOINT pPoint) { +bool __fastcall RenderDX::RenderToScreenPoint(LPPOINT pPoint) +{ if (!pPoint) return false; @@ -674,11 +830,13 @@ bool __fastcall RenderDX::RenderToScreenPoint(LPPOINT pPoint) { return ::ClientToScreen(Game::hWnd, pPoint) != FALSE; } -bool __fastcall RenderDX::GetWindowRectInRender(HWND hWnd, LPRECT pRect) { +bool __fastcall RenderDX::GetWindowRectInRender(HWND hWnd, LPRECT pRect) +{ if (!hWnd || !pRect) return false; - if (hWnd == Game::hWnd) { + if (hWnd == Game::hWnd) + { pRect->left = 0; pRect->top = 0; pRect->right = Drawing::RenderWidth; @@ -700,11 +858,13 @@ bool __fastcall RenderDX::GetWindowRectInRender(HWND hWnd, LPRECT pRect) { return true; } -bool __fastcall RenderDX::GetClientRectInRender(HWND hWnd, LPRECT pRect) { +bool __fastcall RenderDX::GetClientRectInRender(HWND hWnd, LPRECT pRect) +{ if (!hWnd || !pRect) return false; - if (hWnd == Game::hWnd) { + if (hWnd == Game::hWnd) + { pRect->left = 0; pRect->top = 0; pRect->right = Drawing::RenderWidth; @@ -732,12 +892,14 @@ bool __fastcall RenderDX::GetClientRectInRender(HWND hWnd, LPRECT pRect) { return true; } -POINT __fastcall RenderDX::ScreenToRenderLocalPoint(HWND hWnd, POINT point) { +POINT __fastcall RenderDX::ScreenToRenderLocalPoint(HWND hWnd, POINT point) +{ if (!ScreenToRenderPoint(&point, false)) return point; RECT windowRect {}; - if (GetWindowRectInRender(hWnd, &windowRect)) { + if (GetWindowRectInRender(hWnd, &windowRect)) + { point.x -= windowRect.left; point.y -= windowRect.top; } @@ -745,7 +907,8 @@ POINT __fastcall RenderDX::ScreenToRenderLocalPoint(HWND hWnd, POINT point) { return point; } -RECT __fastcall RenderDX::ClientToRenderLocalRect(HWND hWnd, const RECT& rect) { +RECT __fastcall RenderDX::ClientToRenderLocalRect(HWND hWnd, const RECT& rect) +{ POINT topLeft { rect.left, rect.top }; POINT bottomRight { rect.right, rect.bottom }; if (!::ClientToScreen(hWnd, &topLeft) || !::ClientToScreen(hWnd, &bottomRight)) @@ -757,7 +920,8 @@ RECT __fastcall RenderDX::ClientToRenderLocalRect(HWND hWnd, const RECT& rect) { return RECT { topLeft.x, topLeft.y, bottomRight.x, bottomRight.y }; } -RECT __fastcall RenderDX::RenderLocalToClientRect(HWND hWnd, const RECT& rect) { +RECT __fastcall RenderDX::RenderLocalToClientRect(HWND hWnd, const RECT& rect) +{ RECT windowRect {}; if (!GetWindowRectInRender(hWnd, &windowRect)) return rect; @@ -777,7 +941,8 @@ RECT __fastcall RenderDX::RenderLocalToClientRect(HWND hWnd, const RECT& rect) { return clientRect; } -POINT __fastcall RenderDX::MouseLParamToRenderLocalPoint(HWND hWnd, LPARAM lParam) { +POINT __fastcall RenderDX::MouseLParamToRenderLocalPoint(HWND hWnd, LPARAM lParam) +{ POINT point { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; if (!::ClientToScreen(hWnd, &point)) return point; @@ -785,12 +950,14 @@ POINT __fastcall RenderDX::MouseLParamToRenderLocalPoint(HWND hWnd, LPARAM lPara return ScreenToRenderLocalPoint(hWnd, point); } -LPARAM __fastcall RenderDX::MouseLParamToRenderLocal(HWND hWnd, LPARAM lParam) { +LPARAM __fastcall RenderDX::MouseLParamToRenderLocal(HWND hWnd, LPARAM lParam) +{ const POINT point = MouseLParamToRenderLocalPoint(hWnd, lParam); return MAKELPARAM(static_cast(point.x), static_cast(point.y)); } -bool __fastcall RenderDX::RenderRectToClient(HWND referenceHwnd, const RECT& renderRect, LPRECT pClientRect) { +bool __fastcall RenderDX::RenderRectToClient(HWND referenceHwnd, const RECT& renderRect, LPRECT pClientRect) +{ if (!pClientRect) return false; @@ -799,7 +966,8 @@ bool __fastcall RenderDX::RenderRectToClient(HWND referenceHwnd, const RECT& ren if (!RenderToScreenPoint(&topLeft) || !RenderToScreenPoint(&bottomRight)) return false; - if (referenceHwnd) { + if (referenceHwnd) + { if (!::ScreenToClient(referenceHwnd, &topLeft) || !::ScreenToClient(referenceHwnd, &bottomRight)) return false; } @@ -811,7 +979,8 @@ bool __fastcall RenderDX::RenderRectToClient(HWND referenceHwnd, const RECT& ren return true; } -static bool GetParentRenderOrigin(HWND hWnd, POINT& origin) { +static bool GetParentRenderOrigin(HWND hWnd, POINT& origin) +{ origin = { 0, 0 }; if (!hWnd || hWnd == Game::hWnd) @@ -826,7 +995,8 @@ static bool GetParentRenderOrigin(HWND hWnd, POINT& origin) { return true; } -static bool GetWindowRectInParentRender(HWND hWnd, LPRECT pRect) { +static bool GetWindowRectInParentRender(HWND hWnd, LPRECT pRect) +{ if (!RenderDX::GetWindowRectInRender(hWnd, pRect)) return false; @@ -841,7 +1011,8 @@ static bool GetWindowRectInParentRender(HWND hWnd, LPRECT pRect) { return true; } -static bool RenderLocalRectToWindowRect(HWND hWnd, const RECT& localRect, LPRECT pWindowRect) { +static bool RenderLocalRectToWindowRect(HWND hWnd, const RECT& localRect, LPRECT pWindowRect) +{ const HWND parentHwnd = ::GetParent(hWnd); POINT parentOrigin {}; if (!GetParentRenderOrigin(parentHwnd, parentOrigin)) @@ -858,7 +1029,8 @@ static bool RenderLocalRectToWindowRect(HWND hWnd, const RECT& localRect, LPRECT return RenderDX::RenderRectToClient(parentHwnd, renderRect, pWindowRect); } -BOOL __fastcall RenderDX::MoveWindowInRender(HWND hWnd, int x, int y, int width, int height, BOOL repaint) { +BOOL __fastcall RenderDX::MoveWindowInRender(HWND hWnd, int x, int y, int width, int height, BOOL repaint) +{ if (!hWnd) return FALSE; @@ -876,7 +1048,8 @@ BOOL __fastcall RenderDX::MoveWindowInRender(HWND hWnd, int x, int y, int width, repaint); } -BOOL __fastcall RenderDX::SetWindowPosInRender(HWND hWnd, HWND hWndInsertAfter, int x, int y, int cx, int cy, UINT flags) { +BOOL __fastcall RenderDX::SetWindowPosInRender(HWND hWnd, HWND hWndInsertAfter, int x, int y, int cx, int cy, UINT flags) +{ if (!hWnd) return FALSE; @@ -884,7 +1057,8 @@ BOOL __fastcall RenderDX::SetWindowPosInRender(HWND hWnd, HWND hWndInsertAfter, if (!GetWindowRectInParentRender(hWnd, &renderRect)) return FALSE; - if (!(flags & SWP_NOMOVE)) { + if (!(flags & SWP_NOMOVE)) + { const int width = renderRect.right - renderRect.left; const int height = renderRect.bottom - renderRect.top; renderRect.left = x; @@ -893,7 +1067,8 @@ BOOL __fastcall RenderDX::SetWindowPosInRender(HWND hWnd, HWND hWndInsertAfter, renderRect.bottom = y + height; } - if (!(flags & SWP_NOSIZE)) { + if (!(flags & SWP_NOSIZE)) + { renderRect.right = renderRect.left + cx; renderRect.bottom = renderRect.top + cy; } @@ -914,15 +1089,18 @@ BOOL __fastcall RenderDX::SetWindowPosInRender(HWND hWnd, HWND hWndInsertAfter, flags); } -bool __fastcall RenderDX::IsOwnerDrawUsingRawWindowCoordinates() { +bool __fastcall RenderDX::IsOwnerDrawUsingRawWindowCoordinates() +{ return OwnerDrawRawWindowCoordinates; } -void __fastcall RenderDX::SetOwnerDrawRawWindowCoordinates(bool enabled) { +void __fastcall RenderDX::SetOwnerDrawRawWindowCoordinates(bool enabled) +{ OwnerDrawRawWindowCoordinates = enabled; } -void __fastcall RenderDX::UpdateScale() { +void __fastcall RenderDX::UpdateScale() +{ if (!OwnerDrawRectsAlreadyCaptured) WWUI::CaptureOwnerDrawWindowRects(); @@ -931,7 +1109,8 @@ void __fastcall RenderDX::UpdateScale() { const float viewportX = DXRenderer::Instance().GetViewportX(); const float viewportY = DXRenderer::Instance().GetViewportY(); - if (Drawing::RenderWidth <= 0 || Drawing::RenderHeight <= 0 || viewportWidth <= 0.0f || viewportHeight <= 0.0f) { + if (Drawing::RenderWidth <= 0 || Drawing::RenderHeight <= 0 || viewportWidth <= 0.0f || viewportHeight <= 0.0f) + { ResetScale(); WWUI::ApplyOwnerDrawWindowRects(); return; @@ -944,29 +1123,34 @@ void __fastcall RenderDX::UpdateScale() { WWUI::ApplyOwnerDrawWindowRects(); } -void __fastcall RenderDX::ResetScale() { +void __fastcall RenderDX::ResetScale() +{ ScaleX = 1.0f; ScaleY = 1.0f; ViewportX = 0.0f; ViewportY = 0.0f; } -int* __fastcall RenderDX::EnumDisplayModes(DWORD minWidth, DWORD minHeight, DWORD maxWidth, DWORD maxHeight, DWORD) { +int* __fastcall RenderDX::EnumDisplayModes(DWORD minWidth, DWORD minHeight, DWORD maxWidth, DWORD maxHeight, DWORD) +{ std::vector> modes; - DEVMODE devmode{}; + DEVMODE devmode {}; DWORD modeIndex = 0; - while (::EnumDisplaySettingsA(nullptr, modeIndex++, &devmode)) { + while (::EnumDisplaySettingsA(nullptr, modeIndex++, &devmode)) + { const DWORD w = devmode.dmPelsWidth; const DWORD h = devmode.dmPelsHeight; const DWORD bpp = devmode.dmBitsPerPel; - if (w >= minWidth && h >= minHeight && w <= maxWidth && h <= maxHeight && bpp == 32) { + if (w >= minWidth && h >= minHeight && w <= maxWidth && h <= maxHeight && bpp == 32) + { modes.emplace_back(static_cast(w), static_cast(h)); } } - if (modes.empty()) { + if (modes.empty()) + { return nullptr; } @@ -980,7 +1164,8 @@ int* __fastcall RenderDX::EnumDisplayModes(DWORD minWidth, DWORD minHeight, DWOR std::memset(list, 0, bytes); int* ptr = list; - for (const auto& mode : modes) { + for (const auto& mode : modes) + { *ptr++ = mode.first; *ptr++ = mode.second; } @@ -988,10 +1173,14 @@ int* __fastcall RenderDX::EnumDisplayModes(DWORD minWidth, DWORD minHeight, DWOR return list; } -void __fastcall RenderDX::MainProcHandlePaint() { - if (DXMouse::Instance && DSurface::Primary && DSurface::Hidden && DSurface::Composite) { - if (Unsorted::ScenarioStarted) { - if (WWUI::HasActiveOwnerDrawDialog()) { +void __fastcall RenderDX::MainProcHandlePaint() +{ + if (DXMouse::Instance && DSurface::Primary && DSurface::Hidden && DSurface::Composite) + { + if (Unsorted::ScenarioStarted) + { + if (WWUI::HasActiveOwnerDrawDialog()) + { RenderDX::UpdateScreen(DSurface::Primary); return; } @@ -999,10 +1188,12 @@ void __fastcall RenderDX::MainProcHandlePaint() { GScreenClass::UpdatePrimarySurface(DXMouse::Instance->IsCaptured(), DSurface::Composite, nullptr); SidebarClass::Instance.BlitSidebar(true); } - else if (Game::IsMoviePlaying()) { + else if (Game::IsMoviePlaying()) + { Game::BlitMovie(); } - else { + else + { GScreenClass::UpdatePrimarySurface(DXMouse::Instance->IsCaptured(), DSurface::Hidden, nullptr); } } diff --git a/src/Render/Functions.h b/src/Render/Functions.h index 92bb2111c8..cd20dd6030 100644 --- a/src/Render/Functions.h +++ b/src/Render/Functions.h @@ -8,6 +8,7 @@ class RenderDX { public: static bool __fastcall AllocateSurfaces(const RectangleStruct& hiddenRect, const RectangleStruct& compositeRect, const RectangleStruct& tileRect, const RectangleStruct& sidebarRect, bool hiddenFirst); static bool __fastcall SetVideoMode(HWND, int width, int height, int bitsPerPixel); + static void __fastcall SetHighDPIAwareness(); static void __fastcall ResetVideoMode(); static void __fastcall CreateMainWindow(HINSTANCE instance, int cmdShow, int width, int height); static void __fastcall DestroyMainWindow(); From 0c757463343c9e204ce12ba89373589cc173987b Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Fri, 29 May 2026 05:41:32 +0800 Subject: [PATCH 18/21] Fix wrong OptionDialog control position --- src/OwnerDraw/ComboBox.cpp | 8 ++++---- src/OwnerDraw/ListBox.cpp | 6 +++--- src/Render/WWUI.Hooks.cpp | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/OwnerDraw/ComboBox.cpp b/src/OwnerDraw/ComboBox.cpp index 4dba7c15e2..2007906da6 100644 --- a/src/OwnerDraw/ComboBox.cpp +++ b/src/OwnerDraw/ComboBox.cpp @@ -314,8 +314,8 @@ static LRESULT AddOrInsertComboString( LPARAM lParam, bool wideText) { - char narrowText[5120] {}; - wchar_t wideBuffer[5120] {}; + char narrowText[2048] {}; + wchar_t wideBuffer[2048] {}; const LPARAM nativeTextParam = [&]() -> LPARAM { @@ -370,7 +370,7 @@ static LRESULT FindComboString(OwnerDrawDialogElement& data, WNDPROC pOriginalWn (void)data; (void)message; - wchar_t needle[5120] {}; + wchar_t needle[2048] {}; if (wideText) { const auto pText = reinterpret_cast(lParam); @@ -467,7 +467,7 @@ static LRESULT SetComboSelection(OwnerDrawDialogElement& data, WNDPROC pOriginal } else { - char buffer[5120] {}; + char buffer[2048] {}; WideToCharString(buffer, std::size(buffer), pEntry->Text ? pEntry->Text : L""); ::SendMessageA(hWnd, WW_SETTEXTA, 0, reinterpret_cast(buffer)); } diff --git a/src/OwnerDraw/ListBox.cpp b/src/OwnerDraw/ListBox.cpp index 650f4571da..8e88303291 100644 --- a/src/OwnerDraw/ListBox.cpp +++ b/src/OwnerDraw/ListBox.cpp @@ -1010,8 +1010,8 @@ LRESULT CALLBACK WWUI::ListBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARA auto addOrInsertString = [&](bool wideText, bool insert) -> LRESULT { - char narrowText[5120] {}; - wchar_t wideBuffer[5120] {}; + char narrowText[2048] {}; + wchar_t wideBuffer[2048] {}; const LPARAM nativeTextParam = [&]() -> LPARAM { @@ -1063,7 +1063,7 @@ LRESULT CALLBACK WWUI::ListBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPARA auto findString = [&](bool wideText, bool exact, bool select) -> LRESULT { - wchar_t needle[5120] {}; + wchar_t needle[2048] {}; if (wideText) { const auto pText = reinterpret_cast(lParam); diff --git a/src/Render/WWUI.Hooks.cpp b/src/Render/WWUI.Hooks.cpp index 1217104ff2..8bce15970f 100644 --- a/src/Render/WWUI.Hooks.cpp +++ b/src/Render/WWUI.Hooks.cpp @@ -286,7 +286,7 @@ static BOOL __fastcall MoveIngameWindowControls(HWND hWnd) RECT rect; RECT parentRect; - if (!parent || !RenderDX::GetWindowRectInRender(hWnd, &rect) || !RenderDX::GetWindowRectInRender(parent, &parentRect)) + if (!::GetWindowRect(hWnd, &rect) || !::GetWindowRect(parent, &parentRect)) return FALSE; int x = rect.left - parentRect.left + (parentRect.right - parentRect.left - 800) / 2; @@ -296,6 +296,6 @@ static BOOL __fastcall MoveIngameWindowControls(HWND hWnd) if (y < 0) y = 0; - return RenderDX::MoveWindowInRender(hWnd, x, y, rect.right - rect.left, rect.bottom - rect.top, FALSE); + return ::MoveWindow(hWnd, x, y, rect.right - rect.left, rect.bottom - rect.top, FALSE); } DEFINE_FUNCTION_JUMP(LJMP, 0x60B7A0, MoveIngameWindowControls); From d6e8776384dca110427046140ce3334fac48c63a Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Fri, 29 May 2026 05:59:21 +0800 Subject: [PATCH 19/21] Remove gamemd comment in StringTable::LoadString --- src/OwnerDraw/OwnerDraw.cpp | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/OwnerDraw/OwnerDraw.cpp b/src/OwnerDraw/OwnerDraw.cpp index 4dfab60f90..d717e896dd 100644 --- a/src/OwnerDraw/OwnerDraw.cpp +++ b/src/OwnerDraw/OwnerDraw.cpp @@ -1242,17 +1242,9 @@ static void UpdateTooltipTextOnMouseMove(HWND hWnd, LPARAM lParam) if (IsEmpty(tooltipText)) { if (const auto label = OwnerDraw::GetTooltipStringLabel(parentHwnd, hWnd)) - { - tooltipText = StringTable::LoadString( - label, - nullptr, - "D:\\ra2mdpost\\ownrdraw.cpp", - 1957); - } + tooltipText = StringTable::LoadString(label); else - { tooltipText = L""; - } } } @@ -1734,17 +1726,9 @@ static LRESULT HandleTooltipRefresh(HWND hWnd, LPARAM lParam) if (IsEmpty(tooltipText)) { if (const auto label = OwnerDraw::GetTooltipStringLabel(hWnd, controlHwnd)) - { - tooltipText = StringTable::LoadString( - label, - nullptr, - "D:\\ra2mdpost\\ownrdraw.cpp", - 1957); - } + tooltipText = StringTable::LoadString(label); else - { tooltipText = L""; - } } } } From f8134c10d297e8cfca8cc1d4d8a58a792cce261a Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Fri, 29 May 2026 06:21:36 +0800 Subject: [PATCH 20/21] Tidy OwnerDrawStandardWndProc --- src/OwnerDraw/OwnerDraw.cpp | 58 +++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/OwnerDraw/OwnerDraw.cpp b/src/OwnerDraw/OwnerDraw.cpp index d717e896dd..93b24a3be4 100644 --- a/src/OwnerDraw/OwnerDraw.cpp +++ b/src/OwnerDraw/OwnerDraw.cpp @@ -1742,48 +1742,50 @@ LRESULT __fastcall WWUI::OwnerDrawStandardWndProc(HWND hWnd, UINT message, WPARA if (RenderDX::HandleFullscreenToggleMessage(message, wParam, lParam)) return 0; - if (message <= WM_NCHITTEST) + switch (message) { - switch (message) - { - case WM_DESTROY: - UI::RemoveModelessDialog(hWnd); - --Unsorted::WSDialogCount; - ::SetFocus(Game::hWnd); - return 0; - - case WM_PAINT: - return HandlePaint(hWnd); + case WM_DESTROY: + UI::RemoveModelessDialog(hWnd); + --Unsorted::WSDialogCount; + ::SetFocus(Game::hWnd); + return 0; - case WM_ERASEBKGND: - return 1; + case WM_PAINT: + return HandlePaint(hWnd); - case WM_DRAWITEM: - OwnerDraw::DrawItem(reinterpret_cast(lParam)); - return 1; + case WM_ERASEBKGND: + return 1; - case WM_NCHITTEST: - return HandleTooltipRefresh(hWnd, lParam); + case WM_DRAWITEM: + OwnerDraw::DrawItem(reinterpret_cast(lParam)); + return 1; - default: - return 0; - } - } + case WM_NCHITTEST: + return HandleTooltipRefresh(hWnd, lParam); - if (message == WM_INITDIALOG) + case WM_INITDIALOG: return HandleInitDialog(hWnd, lParam); - if (message >= WM_CTLCOLORMSGBOX && message <= WM_CTLCOLORSTATIC) + case WM_CTLCOLORMSGBOX: + case WM_CTLCOLOREDIT: + case WM_CTLCOLORLISTBOX: + case WM_CTLCOLORBTN: + case WM_CTLCOLORDLG: + case WM_CTLCOLORSCROLLBAR: + case WM_CTLCOLORSTATIC: return reinterpret_cast(::GetStockObject(BLACK_BRUSH)); - if (message == WW_INITDIALOG) - { + case WW_INITDIALOG: ::SendMessageA(hWnd, WW_BRINGTOTOP, reinterpret_cast(hWnd), 1); return 0; - } - if (message == WW_TRANSITION_COMPLETE) + case WW_TRANSITION_COMPLETE: ::EnumChildWindows(hWnd, OwnerDraw::SendTransitionCompleteToCustomTextChildProc, 0); + return 0; + + default: + return 0; + } return 0; } From 99a902825686fe69152c65a1b0e84a807a068ccb Mon Sep 17 00:00:00 2001 From: secsome <302702960@qq.com> Date: Fri, 29 May 2026 06:42:18 +0800 Subject: [PATCH 21/21] Sync with YRpp changes --- src/OwnerDraw/ComboBox.cpp | 6 +++--- src/OwnerDraw/OwnerDraw.cpp | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/OwnerDraw/ComboBox.cpp b/src/OwnerDraw/ComboBox.cpp index 2007906da6..ebbf9fc478 100644 --- a/src/OwnerDraw/ComboBox.cpp +++ b/src/OwnerDraw/ComboBox.cpp @@ -653,7 +653,7 @@ LRESULT CALLBACK WWUI::ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPAR if (!pEntry) return CB_ERR; - if (message == CB_GETITEMDATA || message == WW_GETITEMDATA) + if (message == CB_GETITEMDATA || message == LB_GETITEMDATA) return pEntry->ItemData; pEntry->ItemData = static_cast(lParam); @@ -735,8 +735,8 @@ LRESULT CALLBACK WWUI::ComboBoxCtrl(HWND hWnd, UINT message, WPARAM wParam, LPAR case CB_GETITEMDATA: case CB_SETITEMDATA: - case WW_GETITEMDATA: - case WW_SETITEMDATA: + case LB_GETITEMDATA: + case LB_SETITEMDATA: return handleItemData(); case WW_INITDIALOG: diff --git a/src/OwnerDraw/OwnerDraw.cpp b/src/OwnerDraw/OwnerDraw.cpp index 93b24a3be4..7cfeda5822 100644 --- a/src/OwnerDraw/OwnerDraw.cpp +++ b/src/OwnerDraw/OwnerDraw.cpp @@ -1782,9 +1782,6 @@ LRESULT __fastcall WWUI::OwnerDrawStandardWndProc(HWND hWnd, UINT message, WPARA case WW_TRANSITION_COMPLETE: ::EnumChildWindows(hWnd, OwnerDraw::SendTransitionCompleteToCustomTextChildProc, 0); return 0; - - default: - return 0; } return 0;