From b3d8d0aea96842fc0f72e6059ae93cab81b518d2 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Fri, 29 May 2026 00:47:28 -0700 Subject: [PATCH] overlay: implement dx11 backend (#707) ## Link to GitHub Issue or related Pull Request, if one exists Fixes #134 ## Description of change Add ImGui DX11 backend implementation, and integrate into the spice overlay. The tricky part is that some of the Unity games: 1. Launch multiple windows, and 2. Can be resized (even full screen games launch at a certain resolution and then expand to fit desktop resolution) 3. lazy load DX11 DLLs This PR also adds screenshot functionality, but screen resize is not implemented. ## Testing Seems to be working for most games. Need to do final tests for: - [x] CCJ (attract movie plays as well) - [x] Busou Shinki (title movie plays) - [x] MFG - [x] QuizKnock - [x] Polaris Chord - [x] check dx9 for regression --- src/spice2x/CMakeLists.txt | 19 + src/spice2x/build_all.sh | 4 +- src/spice2x/external/imgui/CMakeLists.txt | 7 + .../imgui/backends/imgui_impl_dx11.cpp | 697 ++++++++++++++++++ .../external/imgui/backends/imgui_impl_dx11.h | 52 ++ .../graphics/backends/d3d11/d3d11_backend.cpp | 275 +++++++ .../graphics/backends/d3d11/d3d11_backend.h | 24 + .../graphics/backends/d3d11/d3d11_factory.cpp | 102 +++ .../graphics/backends/d3d11/d3d11_internal.h | 59 ++ .../backends/d3d11/d3d11_screenshot.cpp | 165 +++++ .../backends/d3d11/d3d11_swapchain.cpp | 289 ++++++++ .../backends/d3d11/d3d11_vtable_capture.cpp | 175 +++++ src/spice2x/hooks/graphics/graphics.cpp | 2 + src/spice2x/launcher/shutdown.cpp | 5 + src/spice2x/overlay/imgui/impl_spice.cpp | 15 + src/spice2x/overlay/imgui/impl_spice.h | 1 + src/spice2x/overlay/overlay.cpp | 122 ++- src/spice2x/overlay/overlay.h | 35 + 18 files changed, 2044 insertions(+), 4 deletions(-) create mode 100644 src/spice2x/external/imgui/backends/imgui_impl_dx11.cpp create mode 100644 src/spice2x/external/imgui/backends/imgui_impl_dx11.h create mode 100644 src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp create mode 100644 src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.h create mode 100644 src/spice2x/hooks/graphics/backends/d3d11/d3d11_factory.cpp create mode 100644 src/spice2x/hooks/graphics/backends/d3d11/d3d11_internal.h create mode 100644 src/spice2x/hooks/graphics/backends/d3d11/d3d11_screenshot.cpp create mode 100644 src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp create mode 100644 src/spice2x/hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 336c4f7..32cc734 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -235,6 +235,13 @@ add_compile_definitions( _WIN32_IE=0x0400 ) +# SPICE_XP: set by build_all.sh for the WinXP-compat toolchains. Gates out +# the DX11 overlay backend (WinXP lacks D3DCOMPILER_47.dll). +option(SPICE_XP "Build for the WinXP-compat toolchain" OFF) +if(SPICE_XP) + add_compile_definitions(SPICE_XP=1) +endif() + # acioemu log #set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DACIOEMU_LOG") @@ -517,6 +524,11 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_texture.cpp + hooks/graphics/backends/d3d11/d3d11_backend.cpp + hooks/graphics/backends/d3d11/d3d11_swapchain.cpp + hooks/graphics/backends/d3d11/d3d11_factory.cpp + hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp + hooks/graphics/backends/d3d11/d3d11_screenshot.cpp hooks/input/dinput8/fake_backend.cpp hooks/input/dinput8/fake_device.cpp hooks/input/dinput8/hook.cpp @@ -714,6 +726,10 @@ target_link_libraries(spicetools_spice64 PUBLIC winscard) set_target_properties(spicetools_spice64 PROPERTIES PREFIX "") set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64") target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1) +if(NOT SPICE_XP) + target_link_libraries(spicetools_spice64 PUBLIC d3d11 dxgi d3dcompiler) + target_compile_definitions(spicetools_spice64 PRIVATE SPICE_D3D11=1) +endif() IF(NOT MSVC) set_target_properties(spicetools_spice64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64") @@ -733,6 +749,9 @@ set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "") set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux") target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1) target_compile_definitions(spicetools_spice64_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1) +# spice64_linux is never built under the WinXP toolchain, so dx11 is always on. +target_link_libraries(spicetools_spice64_linux PUBLIC d3d11 dxgi d3dcompiler) +target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE_D3D11=1) IF(NOT MSVC) set_target_properties(spicetools_spice64_linux PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64") diff --git a/src/spice2x/build_all.sh b/src/spice2x/build_all.sh index 635036c..3030ebf 100755 --- a/src/spice2x/build_all.sh +++ b/src/spice2x/build_all.sh @@ -172,7 +172,7 @@ time ( fi mkdir -p ${BUILDDIR_WINXP_32} pushd ${BUILDDIR_WINXP_32} > /dev/null - CXXFLAGS="$CXXFLAGS -DSPICE_XP=1" cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_XP32} + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DSPICE_XP=ON "$OLDPWD" && ninja ${TARGETS_XP32} popd > /dev/null # 64 bit Windows XP @@ -185,7 +185,7 @@ time ( fi mkdir -p ${BUILDDIR_WINXP_64} pushd ${BUILDDIR_WINXP_64} > /dev/null - CXXFLAGS="$CXXFLAGS -DSPICE_XP=1" cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_XP64} + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DSPICE_XP=ON "$OLDPWD" && ninja ${TARGETS_XP64} popd > /dev/null else echo "" diff --git a/src/spice2x/external/imgui/CMakeLists.txt b/src/spice2x/external/imgui/CMakeLists.txt index a66e9b7..7e92231 100644 --- a/src/spice2x/external/imgui/CMakeLists.txt +++ b/src/spice2x/external/imgui/CMakeLists.txt @@ -21,5 +21,12 @@ set(IMGUI_SOURCES misc/cpp/imgui_stdlib.cpp ) +# spice2x: DX11 backend is only built for non-XP toolchains. WinXP lacks +# D3DCOMPILER_47.dll, and including this TU would pull in D3DCompile imports. +if(NOT SPICE_XP) + list(APPEND IMGUI_HEADERS backends/imgui_impl_dx11.h) + list(APPEND IMGUI_SOURCES backends/imgui_impl_dx11.cpp) +endif() + add_library(imgui STATIC ${IMGUI_HEADERS} ${IMGUI_SOURCES}) target_include_directories(imgui PRIVATE ${PROJECT_SOURCE_DIR}) diff --git a/src/spice2x/external/imgui/backends/imgui_impl_dx11.cpp b/src/spice2x/external/imgui/backends/imgui_impl_dx11.cpp new file mode 100644 index 0000000..8aa7756 --- /dev/null +++ b/src/spice2x/external/imgui/backends/imgui_impl_dx11.cpp @@ -0,0 +1,697 @@ +// dear imgui: Renderer Backend for DirectX11 +// This needs to be used along with a Platform Backend (e.g. Win32) + +// Implemented features: +// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef! +// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset). +// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures). +// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'. + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +// CHANGELOG +// (minor and older changes stripped away, please see git history for details) +// 2026-01-19: DirectX11: Added 'SamplerNearest' in ImGui_ImplDX11_RenderState. Renamed 'SamplerDefault' to 'SamplerLinear'. +// 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown. +// 2025-06-11: DirectX11: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas. +// 2025-05-07: DirectX11: Honor draw_data->FramebufferScale to allow for custom backends and experiment using it (consistently with other renderer backends, even though in normal condition it is not set under Windows). +// 2025-01-06: DirectX11: Expose VertexConstantBuffer in ImGui_ImplDX11_RenderState. Reset projection matrix in ImDrawCallback_ResetRenderState handler. +// 2024-10-07: DirectX11: Changed default texture sampler to Clamp instead of Repeat/Wrap. +// 2024-10-07: DirectX11: Expose selected render state in ImGui_ImplDX11_RenderState, which you can access in 'void* platform_io.Renderer_RenderState' during draw callbacks. +// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11. +// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX). +// 2021-05-19: DirectX11: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement) +// 2021-02-18: DirectX11: Change blending equation to preserve alpha in output buffer. +// 2019-08-01: DirectX11: Fixed code querying the Geometry Shader state (would generally error with Debug layer enabled). +// 2019-07-21: DirectX11: Backup, clear and restore Geometry Shader is any is bound when calling ImGui_ImplDX11_RenderDrawData. Clearing Hull/Domain/Compute shaders without backup/restore. +// 2019-05-29: DirectX11: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag. +// 2019-04-30: DirectX11: Added support for special ImDrawCallback_ResetRenderState callback to reset render state. +// 2018-12-03: Misc: Added #pragma comment statement to automatically link with d3dcompiler.lib when using D3DCompile(). +// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window. +// 2018-08-01: DirectX11: Querying for IDXGIFactory instead of IDXGIFactory1 to increase compatibility. +// 2018-07-13: DirectX11: Fixed unreleased resources in Init and Shutdown functions. +// 2018-06-08: Misc: Extracted imgui_impl_dx11.cpp/.h away from the old combined DX11+Win32 example. +// 2018-06-08: DirectX11: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle. +// 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplDX11_RenderDrawData() in the .h file so you can call it yourself. +// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves. +// 2016-05-07: DirectX11: Disabling depth-write. + +#include "imgui.h" +#ifndef IMGUI_DISABLE +#include "imgui_impl_dx11.h" + +// DirectX +#include +#include +#include +#ifdef _MSC_VER +#pragma comment(lib, "d3dcompiler") // Automatically link with d3dcompiler.lib as we are using D3DCompile() below. +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#endif + +// DirectX11 data +struct ImGui_ImplDX11_Texture +{ + ID3D11Texture2D* pTexture; + ID3D11ShaderResourceView* pTextureView; +}; + +struct ImGui_ImplDX11_Data +{ + ID3D11Device* pd3dDevice; + ID3D11DeviceContext* pd3dDeviceContext; + IDXGIFactory* pFactory; + ID3D11Buffer* pVB; + ID3D11Buffer* pIB; + ID3D11VertexShader* pVertexShader; + ID3D11InputLayout* pInputLayout; + ID3D11Buffer* pVertexConstantBuffer; + ID3D11PixelShader* pPixelShader; + ID3D11SamplerState* pTexSamplerLinear; + ID3D11SamplerState* pTexSamplerNearest; + ID3D11RasterizerState* pRasterizerState; + ID3D11BlendState* pBlendState; + ID3D11DepthStencilState* pDepthStencilState; + int VertexBufferSize; + int IndexBufferSize; + + ImGui_ImplDX11_Data() { memset((void*)this, 0, sizeof(*this)); VertexBufferSize = 5000; IndexBufferSize = 10000; } +}; + +struct VERTEX_CONSTANT_BUFFER_DX11 +{ + float mvp[4][4]; +}; + +// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts +// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts. +static ImGui_ImplDX11_Data* ImGui_ImplDX11_GetBackendData() +{ + return ImGui::GetCurrentContext() ? (ImGui_ImplDX11_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; +} + +// Functions +static void ImGui_ImplDX11_SetupRenderState(const ImDrawData* draw_data, ID3D11DeviceContext* device_ctx) +{ + ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); + + // Setup viewport + D3D11_VIEWPORT vp = {}; + vp.Width = draw_data->DisplaySize.x * draw_data->FramebufferScale.x; + vp.Height = draw_data->DisplaySize.y * draw_data->FramebufferScale.y; + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + vp.TopLeftX = vp.TopLeftY = 0; + device_ctx->RSSetViewports(1, &vp); + + // Setup orthographic projection matrix into our constant buffer + // Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps. + D3D11_MAPPED_SUBRESOURCE mapped_resource; + if (device_ctx->Map(bd->pVertexConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource) == S_OK) + { + VERTEX_CONSTANT_BUFFER_DX11* constant_buffer = (VERTEX_CONSTANT_BUFFER_DX11*)mapped_resource.pData; + float L = draw_data->DisplayPos.x; + float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x; + float T = draw_data->DisplayPos.y; + float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y; + float mvp[4][4] = + { + { 2.0f/(R-L), 0.0f, 0.0f, 0.0f }, + { 0.0f, 2.0f/(T-B), 0.0f, 0.0f }, + { 0.0f, 0.0f, 0.5f, 0.0f }, + { (R+L)/(L-R), (T+B)/(B-T), 0.5f, 1.0f }, + }; + memcpy(&constant_buffer->mvp, mvp, sizeof(mvp)); + device_ctx->Unmap(bd->pVertexConstantBuffer, 0); + } + + // Setup shader and vertex buffers + unsigned int stride = sizeof(ImDrawVert); + unsigned int offset = 0; + device_ctx->IASetInputLayout(bd->pInputLayout); + device_ctx->IASetVertexBuffers(0, 1, &bd->pVB, &stride, &offset); + device_ctx->IASetIndexBuffer(bd->pIB, sizeof(ImDrawIdx) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT, 0); + device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + device_ctx->VSSetShader(bd->pVertexShader, nullptr, 0); + device_ctx->VSSetConstantBuffers(0, 1, &bd->pVertexConstantBuffer); + device_ctx->PSSetShader(bd->pPixelShader, nullptr, 0); + device_ctx->PSSetSamplers(0, 1, &bd->pTexSamplerLinear); + device_ctx->GSSetShader(nullptr, nullptr, 0); + device_ctx->HSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used.. + device_ctx->DSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used.. + device_ctx->CSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used.. + + // Setup render state + const float blend_factor[4] = { 0.f, 0.f, 0.f, 0.f }; + device_ctx->OMSetBlendState(bd->pBlendState, blend_factor, 0xffffffff); + device_ctx->OMSetDepthStencilState(bd->pDepthStencilState, 0); + device_ctx->RSSetState(bd->pRasterizerState); +} + +// Render function +void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data) +{ + // Avoid rendering when minimized + if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f) + return; + + ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); + ID3D11DeviceContext* device = bd->pd3dDeviceContext; + + // Catch up with texture updates. Most of the times, the list will have 1 element with an OK status, aka nothing to do. + // (This almost always points to ImGui::GetPlatformIO().Textures[] but is part of ImDrawData to allow overriding or disabling texture updates). + if (draw_data->Textures != nullptr) + for (ImTextureData* tex : *draw_data->Textures) + if (tex->Status != ImTextureStatus_OK) + ImGui_ImplDX11_UpdateTexture(tex); + + // Create and grow vertex/index buffers if needed + if (!bd->pVB || bd->VertexBufferSize < draw_data->TotalVtxCount) + { + if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; } + bd->VertexBufferSize = draw_data->TotalVtxCount + 5000; + D3D11_BUFFER_DESC desc = {}; + desc.Usage = D3D11_USAGE_DYNAMIC; + desc.ByteWidth = bd->VertexBufferSize * sizeof(ImDrawVert); + desc.BindFlags = D3D11_BIND_VERTEX_BUFFER; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + desc.MiscFlags = 0; + if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVB) < 0) + return; + } + if (!bd->pIB || bd->IndexBufferSize < draw_data->TotalIdxCount) + { + if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; } + bd->IndexBufferSize = draw_data->TotalIdxCount + 10000; + D3D11_BUFFER_DESC desc = {}; + desc.Usage = D3D11_USAGE_DYNAMIC; + desc.ByteWidth = bd->IndexBufferSize * sizeof(ImDrawIdx); + desc.BindFlags = D3D11_BIND_INDEX_BUFFER; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pIB) < 0) + return; + } + + // Upload vertex/index data into a single contiguous GPU buffer + D3D11_MAPPED_SUBRESOURCE vtx_resource, idx_resource; + if (device->Map(bd->pVB, 0, D3D11_MAP_WRITE_DISCARD, 0, &vtx_resource) != S_OK) + return; + if (device->Map(bd->pIB, 0, D3D11_MAP_WRITE_DISCARD, 0, &idx_resource) != S_OK) + return; + ImDrawVert* vtx_dst = (ImDrawVert*)vtx_resource.pData; + ImDrawIdx* idx_dst = (ImDrawIdx*)idx_resource.pData; + for (const ImDrawList* draw_list : draw_data->CmdLists) + { + memcpy(vtx_dst, draw_list->VtxBuffer.Data, draw_list->VtxBuffer.Size * sizeof(ImDrawVert)); + memcpy(idx_dst, draw_list->IdxBuffer.Data, draw_list->IdxBuffer.Size * sizeof(ImDrawIdx)); + vtx_dst += draw_list->VtxBuffer.Size; + idx_dst += draw_list->IdxBuffer.Size; + } + device->Unmap(bd->pVB, 0); + device->Unmap(bd->pIB, 0); + + // Backup DX state that will be modified to restore it afterwards (unfortunately this is very ugly looking and verbose. Close your eyes!) + struct BACKUP_DX11_STATE + { + UINT ScissorRectsCount, ViewportsCount; + D3D11_RECT ScissorRects[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE]; + D3D11_VIEWPORT Viewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE]; + ID3D11RasterizerState* RS; + ID3D11BlendState* BlendState; + FLOAT BlendFactor[4]; + UINT SampleMask; + UINT StencilRef; + ID3D11DepthStencilState* DepthStencilState; + ID3D11ShaderResourceView* PSShaderResource; + ID3D11SamplerState* PSSampler; + ID3D11PixelShader* PS; + ID3D11VertexShader* VS; + ID3D11GeometryShader* GS; + UINT PSInstancesCount, VSInstancesCount, GSInstancesCount; + ID3D11ClassInstance *PSInstances[256], *VSInstances[256], *GSInstances[256]; // 256 is max according to PSSetShader documentation + D3D11_PRIMITIVE_TOPOLOGY PrimitiveTopology; + ID3D11Buffer* IndexBuffer, *VertexBuffer, *VSConstantBuffer; + UINT IndexBufferOffset, VertexBufferStride, VertexBufferOffset; + DXGI_FORMAT IndexBufferFormat; + ID3D11InputLayout* InputLayout; + }; + BACKUP_DX11_STATE old = {}; + old.ScissorRectsCount = old.ViewportsCount = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; + device->RSGetScissorRects(&old.ScissorRectsCount, old.ScissorRects); + device->RSGetViewports(&old.ViewportsCount, old.Viewports); + device->RSGetState(&old.RS); + device->OMGetBlendState(&old.BlendState, old.BlendFactor, &old.SampleMask); + device->OMGetDepthStencilState(&old.DepthStencilState, &old.StencilRef); + device->PSGetShaderResources(0, 1, &old.PSShaderResource); + device->PSGetSamplers(0, 1, &old.PSSampler); + old.PSInstancesCount = old.VSInstancesCount = old.GSInstancesCount = 256; + device->PSGetShader(&old.PS, old.PSInstances, &old.PSInstancesCount); + device->VSGetShader(&old.VS, old.VSInstances, &old.VSInstancesCount); + device->VSGetConstantBuffers(0, 1, &old.VSConstantBuffer); + device->GSGetShader(&old.GS, old.GSInstances, &old.GSInstancesCount); + + device->IAGetPrimitiveTopology(&old.PrimitiveTopology); + device->IAGetIndexBuffer(&old.IndexBuffer, &old.IndexBufferFormat, &old.IndexBufferOffset); + device->IAGetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset); + device->IAGetInputLayout(&old.InputLayout); + + // Setup desired DX state + ImGui_ImplDX11_SetupRenderState(draw_data, device); + + // Setup render state structure (for callbacks and custom texture bindings) + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + ImGui_ImplDX11_RenderState render_state; + render_state.Device = bd->pd3dDevice; + render_state.DeviceContext = bd->pd3dDeviceContext; + render_state.SamplerLinear = bd->pTexSamplerLinear; + render_state.SamplerNearest = bd->pTexSamplerNearest; + render_state.VertexConstantBuffer = bd->pVertexConstantBuffer; + platform_io.Renderer_RenderState = &render_state; + + // Render command lists + // (Because we merged all buffers into a single one, we maintain our own offset into them) + int global_idx_offset = 0; + int global_vtx_offset = 0; + ImVec2 clip_off = draw_data->DisplayPos; + ImVec2 clip_scale = draw_data->FramebufferScale; + for (const ImDrawList* draw_list : draw_data->CmdLists) + { + for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++) + { + const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback != nullptr) + { + // User callback, registered via ImDrawList::AddCallback() + // (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.) + if (pcmd->UserCallback == ImDrawCallback_ResetRenderState) + ImGui_ImplDX11_SetupRenderState(draw_data, device); + else + pcmd->UserCallback(draw_list, pcmd); + } + else + { + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); + ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; + + // Apply scissor/clipping rectangle + const D3D11_RECT r = { (LONG)clip_min.x, (LONG)clip_min.y, (LONG)clip_max.x, (LONG)clip_max.y }; + device->RSSetScissorRects(1, &r); + + // Bind texture, Draw + ID3D11ShaderResourceView* texture_srv = (ID3D11ShaderResourceView*)pcmd->GetTexID(); + device->PSSetShaderResources(0, 1, &texture_srv); + device->DrawIndexed(pcmd->ElemCount, pcmd->IdxOffset + global_idx_offset, pcmd->VtxOffset + global_vtx_offset); + } + } + global_idx_offset += draw_list->IdxBuffer.Size; + global_vtx_offset += draw_list->VtxBuffer.Size; + } + platform_io.Renderer_RenderState = nullptr; + + // Restore modified DX state + device->RSSetScissorRects(old.ScissorRectsCount, old.ScissorRects); + device->RSSetViewports(old.ViewportsCount, old.Viewports); + device->RSSetState(old.RS); if (old.RS) old.RS->Release(); + device->OMSetBlendState(old.BlendState, old.BlendFactor, old.SampleMask); if (old.BlendState) old.BlendState->Release(); + device->OMSetDepthStencilState(old.DepthStencilState, old.StencilRef); if (old.DepthStencilState) old.DepthStencilState->Release(); + device->PSSetShaderResources(0, 1, &old.PSShaderResource); if (old.PSShaderResource) old.PSShaderResource->Release(); + device->PSSetSamplers(0, 1, &old.PSSampler); if (old.PSSampler) old.PSSampler->Release(); + device->PSSetShader(old.PS, old.PSInstances, old.PSInstancesCount); if (old.PS) old.PS->Release(); + for (UINT i = 0; i < old.PSInstancesCount; i++) if (old.PSInstances[i]) old.PSInstances[i]->Release(); + device->VSSetShader(old.VS, old.VSInstances, old.VSInstancesCount); if (old.VS) old.VS->Release(); + device->VSSetConstantBuffers(0, 1, &old.VSConstantBuffer); if (old.VSConstantBuffer) old.VSConstantBuffer->Release(); + device->GSSetShader(old.GS, old.GSInstances, old.GSInstancesCount); if (old.GS) old.GS->Release(); + for (UINT i = 0; i < old.VSInstancesCount; i++) if (old.VSInstances[i]) old.VSInstances[i]->Release(); + device->IASetPrimitiveTopology(old.PrimitiveTopology); + device->IASetIndexBuffer(old.IndexBuffer, old.IndexBufferFormat, old.IndexBufferOffset); if (old.IndexBuffer) old.IndexBuffer->Release(); + device->IASetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset); if (old.VertexBuffer) old.VertexBuffer->Release(); + device->IASetInputLayout(old.InputLayout); if (old.InputLayout) old.InputLayout->Release(); +} + +static void ImGui_ImplDX11_DestroyTexture(ImTextureData* tex) +{ + if (ImGui_ImplDX11_Texture* backend_tex = (ImGui_ImplDX11_Texture*)tex->BackendUserData) + { + IM_ASSERT(backend_tex->pTextureView == (ID3D11ShaderResourceView*)(intptr_t)tex->TexID); + backend_tex->pTextureView->Release(); + backend_tex->pTexture->Release(); + IM_DELETE(backend_tex); + + // Clear identifiers and mark as destroyed (in order to allow e.g. calling InvalidateDeviceObjects while running) + tex->SetTexID(ImTextureID_Invalid); + tex->BackendUserData = nullptr; + } + tex->SetStatus(ImTextureStatus_Destroyed); +} + +void ImGui_ImplDX11_UpdateTexture(ImTextureData* tex) +{ + ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); + if (tex->Status == ImTextureStatus_WantCreate) + { + // Create and upload new texture to graphics system + //IMGUI_DEBUG_LOG("UpdateTexture #%03d: WantCreate %dx%d\n", tex->UniqueID, tex->Width, tex->Height); + IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == nullptr); + IM_ASSERT(tex->Format == ImTextureFormat_RGBA32); + unsigned int* pixels = (unsigned int*)tex->GetPixels(); + ImGui_ImplDX11_Texture* backend_tex = IM_NEW(ImGui_ImplDX11_Texture)(); + + // Create texture + D3D11_TEXTURE2D_DESC desc; + ZeroMemory(&desc, sizeof(desc)); + desc.Width = (UINT)tex->Width; + desc.Height = (UINT)tex->Height; + desc.MipLevels = 1; + desc.ArraySize = 1; +#ifdef IMGUI_USE_BGRA_PACKED_COLOR + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; +#else + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; +#endif + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + desc.CPUAccessFlags = 0; + D3D11_SUBRESOURCE_DATA subResource; + subResource.pSysMem = pixels; + subResource.SysMemPitch = desc.Width * 4; + subResource.SysMemSlicePitch = 0; + bd->pd3dDevice->CreateTexture2D(&desc, &subResource, &backend_tex->pTexture); + IM_ASSERT(backend_tex->pTexture != nullptr && "Backend failed to create texture!"); + + // Create texture view + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc; + ZeroMemory(&srvDesc, sizeof(srvDesc)); +#ifdef IMGUI_USE_BGRA_PACKED_COLOR + srvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; +#else + srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; +#endif + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = desc.MipLevels; + srvDesc.Texture2D.MostDetailedMip = 0; + bd->pd3dDevice->CreateShaderResourceView(backend_tex->pTexture, &srvDesc, &backend_tex->pTextureView); + IM_ASSERT(backend_tex->pTextureView != nullptr && "Backend failed to create texture!"); + + // Store identifiers + tex->SetTexID((ImTextureID)(intptr_t)backend_tex->pTextureView); + tex->SetStatus(ImTextureStatus_OK); + tex->BackendUserData = backend_tex; + } + else if (tex->Status == ImTextureStatus_WantUpdates) + { + // Update selected blocks. We only ever write to textures regions which have never been used before! + // This backend choose to use tex->Updates[] but you can use tex->UpdateRect to upload a single region. + ImGui_ImplDX11_Texture* backend_tex = (ImGui_ImplDX11_Texture*)tex->BackendUserData; + IM_ASSERT(backend_tex->pTextureView == (ID3D11ShaderResourceView*)(intptr_t)tex->TexID); + for (ImTextureRect& r : tex->Updates) + { + D3D11_BOX box = { (UINT)r.x, (UINT)r.y, (UINT)0, (UINT)(r.x + r.w), (UINT)(r.y + r .h), (UINT)1 }; + bd->pd3dDeviceContext->UpdateSubresource(backend_tex->pTexture, 0, &box, tex->GetPixelsAt(r.x, r.y), (UINT)tex->GetPitch(), 0); + } + tex->SetStatus(ImTextureStatus_OK); + } + if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0) + ImGui_ImplDX11_DestroyTexture(tex); +} + +bool ImGui_ImplDX11_CreateDeviceObjects() +{ + ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); + if (!bd->pd3dDevice) + return false; + ImGui_ImplDX11_InvalidateDeviceObjects(); + + // By using D3DCompile() from / d3dcompiler.lib, we introduce a dependency to a given version of d3dcompiler_XX.dll (see D3DCOMPILER_DLL_A) + // If you would like to use this DX11 sample code but remove this dependency you can: + // 1) compile once, save the compiled shader blobs into a file or source code and pass them to CreateVertexShader()/CreatePixelShader() [preferred solution] + // 2) use code to detect any version of the DLL and grab a pointer to D3DCompile from the DLL. + // See https://github.com/ocornut/imgui/pull/638 for sources and details. + + // Create the vertex shader + { + static const char* vertexShader = + "cbuffer vertexBuffer : register(b0) \ + {\ + float4x4 ProjectionMatrix; \ + };\ + struct VS_INPUT\ + {\ + float2 pos : POSITION;\ + float4 col : COLOR0;\ + float2 uv : TEXCOORD0;\ + };\ + \ + struct PS_INPUT\ + {\ + float4 pos : SV_POSITION;\ + float4 col : COLOR0;\ + float2 uv : TEXCOORD0;\ + };\ + \ + PS_INPUT main(VS_INPUT input)\ + {\ + PS_INPUT output;\ + output.pos = mul( ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));\ + output.col = input.col;\ + output.uv = input.uv;\ + return output;\ + }"; + + ID3DBlob* vertexShaderBlob; + if (FAILED(D3DCompile(vertexShader, strlen(vertexShader), nullptr, nullptr, nullptr, "main", "vs_4_0", 0, 0, &vertexShaderBlob, nullptr))) + return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob! + if (bd->pd3dDevice->CreateVertexShader(vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), nullptr, &bd->pVertexShader) != S_OK) + { + vertexShaderBlob->Release(); + return false; + } + + // Create the input layout + D3D11_INPUT_ELEMENT_DESC local_layout[] = + { + { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, pos), D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, uv), D3D11_INPUT_PER_VERTEX_DATA, 0 }, +#ifdef IMGUI_USE_BGRA_PACKED_COLOR + { "COLOR", 0, DXGI_FORMAT_B8G8R8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 }, +#else + { "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 }, +#endif + }; + if (bd->pd3dDevice->CreateInputLayout(local_layout, 3, vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), &bd->pInputLayout) != S_OK) + { + vertexShaderBlob->Release(); + return false; + } + vertexShaderBlob->Release(); + + // Create the constant buffer + { + D3D11_BUFFER_DESC desc = {}; + desc.ByteWidth = sizeof(VERTEX_CONSTANT_BUFFER_DX11); + desc.Usage = D3D11_USAGE_DYNAMIC; + desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + desc.MiscFlags = 0; + bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVertexConstantBuffer); + } + } + + // Create the pixel shader + { + static const char* pixelShader = + "struct PS_INPUT\ + {\ + float4 pos : SV_POSITION;\ + float4 col : COLOR0;\ + float2 uv : TEXCOORD0;\ + };\ + sampler sampler0;\ + Texture2D texture0;\ + \ + float4 main(PS_INPUT input) : SV_Target\ + {\ + float4 out_col = input.col * texture0.Sample(sampler0, input.uv); \ + return out_col; \ + }"; + + ID3DBlob* pixelShaderBlob; + if (FAILED(D3DCompile(pixelShader, strlen(pixelShader), nullptr, nullptr, nullptr, "main", "ps_4_0", 0, 0, &pixelShaderBlob, nullptr))) + return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob! + if (bd->pd3dDevice->CreatePixelShader(pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize(), nullptr, &bd->pPixelShader) != S_OK) + { + pixelShaderBlob->Release(); + return false; + } + pixelShaderBlob->Release(); + } + + // Create the blending setup + { + D3D11_BLEND_DESC desc; + ZeroMemory(&desc, sizeof(desc)); + desc.AlphaToCoverageEnable = false; + desc.RenderTarget[0].BlendEnable = true; + desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; + desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + bd->pd3dDevice->CreateBlendState(&desc, &bd->pBlendState); + } + + // Create the rasterizer state + { + D3D11_RASTERIZER_DESC desc; + ZeroMemory(&desc, sizeof(desc)); + desc.FillMode = D3D11_FILL_SOLID; + desc.CullMode = D3D11_CULL_NONE; + desc.ScissorEnable = true; + desc.DepthClipEnable = true; + bd->pd3dDevice->CreateRasterizerState(&desc, &bd->pRasterizerState); + } + + // Create depth-stencil State + { + D3D11_DEPTH_STENCIL_DESC desc; + ZeroMemory(&desc, sizeof(desc)); + desc.DepthEnable = false; + desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL; + desc.DepthFunc = D3D11_COMPARISON_ALWAYS; + desc.StencilEnable = false; + desc.FrontFace.StencilFailOp = desc.FrontFace.StencilDepthFailOp = desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP; + desc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; + desc.BackFace = desc.FrontFace; + bd->pd3dDevice->CreateDepthStencilState(&desc, &bd->pDepthStencilState); + } + + // Create texture sampler + // (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling) + { + D3D11_SAMPLER_DESC desc; + ZeroMemory(&desc, sizeof(desc)); + desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + desc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + desc.MipLODBias = 0.f; + desc.ComparisonFunc = D3D11_COMPARISON_ALWAYS; + desc.MinLOD = 0.f; + desc.MaxLOD = 0.f; + bd->pd3dDevice->CreateSamplerState(&desc, &bd->pTexSamplerLinear); + desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT; + bd->pd3dDevice->CreateSamplerState(&desc, &bd->pTexSamplerNearest); + } + + return true; +} + +void ImGui_ImplDX11_InvalidateDeviceObjects() +{ + ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); + if (!bd->pd3dDevice) + return; + + // Destroy all textures + for (ImTextureData* tex : ImGui::GetPlatformIO().Textures) + if (tex->RefCount == 1) + ImGui_ImplDX11_DestroyTexture(tex); + + if (bd->pTexSamplerLinear) { bd->pTexSamplerLinear->Release(); bd->pTexSamplerLinear = nullptr; } + if (bd->pTexSamplerNearest) { bd->pTexSamplerNearest->Release(); bd->pTexSamplerNearest = nullptr; } + if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; } + if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; } + if (bd->pBlendState) { bd->pBlendState->Release(); bd->pBlendState = nullptr; } + if (bd->pDepthStencilState) { bd->pDepthStencilState->Release(); bd->pDepthStencilState = nullptr; } + if (bd->pRasterizerState) { bd->pRasterizerState->Release(); bd->pRasterizerState = nullptr; } + if (bd->pPixelShader) { bd->pPixelShader->Release(); bd->pPixelShader = nullptr; } + if (bd->pVertexConstantBuffer) { bd->pVertexConstantBuffer->Release(); bd->pVertexConstantBuffer = nullptr; } + if (bd->pInputLayout) { bd->pInputLayout->Release(); bd->pInputLayout = nullptr; } + if (bd->pVertexShader) { bd->pVertexShader->Release(); bd->pVertexShader = nullptr; } +} + +bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context) +{ + ImGuiIO& io = ImGui::GetIO(); + IMGUI_CHECKVERSION(); + IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); + + // Setup backend capabilities flags + ImGui_ImplDX11_Data* bd = IM_NEW(ImGui_ImplDX11_Data)(); + io.BackendRendererUserData = (void*)bd; + io.BackendRendererName = "imgui_impl_dx11"; + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. + io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render. + + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + platform_io.Renderer_TextureMaxWidth = platform_io.Renderer_TextureMaxHeight = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION; + + // Get factory from device + IDXGIDevice* pDXGIDevice = nullptr; + IDXGIAdapter* pDXGIAdapter = nullptr; + IDXGIFactory* pFactory = nullptr; + + if (device->QueryInterface(IID_PPV_ARGS(&pDXGIDevice)) == S_OK) + if (pDXGIDevice->GetParent(IID_PPV_ARGS(&pDXGIAdapter)) == S_OK) + if (pDXGIAdapter->GetParent(IID_PPV_ARGS(&pFactory)) == S_OK) + { + bd->pd3dDevice = device; + bd->pd3dDeviceContext = device_context; + bd->pFactory = pFactory; + } + if (pDXGIDevice) pDXGIDevice->Release(); + if (pDXGIAdapter) pDXGIAdapter->Release(); + bd->pd3dDevice->AddRef(); + bd->pd3dDeviceContext->AddRef(); + + return true; +} + +void ImGui_ImplDX11_Shutdown() +{ + ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); + IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?"); + ImGuiIO& io = ImGui::GetIO(); + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + + ImGui_ImplDX11_InvalidateDeviceObjects(); + if (bd->pFactory) { bd->pFactory->Release(); } + if (bd->pd3dDevice) { bd->pd3dDevice->Release(); } + if (bd->pd3dDeviceContext) { bd->pd3dDeviceContext->Release(); } + + io.BackendRendererName = nullptr; + io.BackendRendererUserData = nullptr; + io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures); + platform_io.ClearRendererHandlers(); + IM_DELETE(bd); +} + +void ImGui_ImplDX11_NewFrame() +{ + ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); + IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplDX11_Init()?"); + + if (!bd->pVertexShader) + if (!ImGui_ImplDX11_CreateDeviceObjects()) + IM_ASSERT(0 && "ImGui_ImplDX11_CreateDeviceObjects() failed!"); +} + +//----------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE diff --git a/src/spice2x/external/imgui/backends/imgui_impl_dx11.h b/src/spice2x/external/imgui/backends/imgui_impl_dx11.h new file mode 100644 index 0000000..338e009 --- /dev/null +++ b/src/spice2x/external/imgui/backends/imgui_impl_dx11.h @@ -0,0 +1,52 @@ +// dear imgui: Renderer Backend for DirectX11 +// This needs to be used along with a Platform Backend (e.g. Win32) + +// Implemented features: +// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef! +// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset). +// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures). +// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'. + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +#pragma once +#include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE + +struct ID3D11Device; +struct ID3D11DeviceContext; +struct ID3D11SamplerState; +struct ID3D11Buffer; + +// Follow "Getting Started" link and check examples/ folder to learn about using backends! +IMGUI_IMPL_API bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context); +IMGUI_IMPL_API void ImGui_ImplDX11_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplDX11_NewFrame(); +IMGUI_IMPL_API void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data); + +// Use if you want to reset your rendering device without losing Dear ImGui state. +IMGUI_IMPL_API bool ImGui_ImplDX11_CreateDeviceObjects(); +IMGUI_IMPL_API void ImGui_ImplDX11_InvalidateDeviceObjects(); + +// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = nullptr to handle this manually. +IMGUI_IMPL_API void ImGui_ImplDX11_UpdateTexture(ImTextureData* tex); + +// [BETA] Selected render state data shared with callbacks. +// This is temporarily stored in GetPlatformIO().Renderer_RenderState during the ImGui_ImplDX11_RenderDrawData() call. +// (Please open an issue if you feel you need access to more data) +struct ImGui_ImplDX11_RenderState +{ + ID3D11Device* Device; + ID3D11DeviceContext* DeviceContext; + ID3D11SamplerState* SamplerLinear; + ID3D11SamplerState* SamplerNearest; + ID3D11Buffer* VertexConstantBuffer; +}; + +#endif // #ifndef IMGUI_DISABLE diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp new file mode 100644 index 0000000..15389b9 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp @@ -0,0 +1,275 @@ +// dx11 / dxgi hook entrypoint. trampolines d3d11.dll / dxgi.dll exports +// the moment those DLLs appear (LDR notification + poll-thread fallback), +// then drives proactive vtable capture so we don't lose the race against +// the execexe loader. per-vtable hook implementations live in the sibling +// files (d3d11_swapchain / d3d11_factory / d3d11_vtable_capture / +// d3d11_screenshot). +// +// note: never LoadLibrary d3d11/dxgi -- execexe pre-loads them itself and +// fails (error 0xa) if they're already in the loader's module list. +// +// 64-bit only. + +#include "d3d11_backend.h" + +#ifndef SPICE_D3D11 + +void graphics_d3d11_init() {} +void graphics_d3d11_shutdown() {} + +#else + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "d3d11_internal.h" +#include "util/nt_loader.h" + +namespace { + +using D3D11CreateDeviceAndSwapChain_t = HRESULT(WINAPI *)( + IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT, + const D3D_FEATURE_LEVEL *, UINT, UINT, + const DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **, + ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **); +using CreateDXGIFactory_t = HRESULT(WINAPI *)(REFIID, void **); +using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **); +using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **); + +D3D11CreateDeviceAndSwapChain_t D3D11CreateDeviceAndSwapChain_orig = nullptr; +CreateDXGIFactory_t CreateDXGIFactory_orig = nullptr; +CreateDXGIFactory1_t CreateDXGIFactory1_orig = nullptr; +CreateDXGIFactory2_t CreateDXGIFactory2_orig = nullptr; + +std::atomic g_d3d11_exports_hooked { false }; +std::atomic g_dxgi_exports_hooked { false }; + +// ---------------------------------------------------------------------- +// top-level export hooks + +HRESULT WINAPI D3D11CreateDeviceAndSwapChain_hook( + IDXGIAdapter *pAdapter, D3D_DRIVER_TYPE DriverType, HMODULE Software, UINT Flags, + const D3D_FEATURE_LEVEL *pFeatureLevels, UINT FeatureLevels, UINT SDKVersion, + const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, IDXGISwapChain **ppSwapChain, + ID3D11Device **ppDevice, D3D_FEATURE_LEVEL *pFeatureLevel, + ID3D11DeviceContext **ppImmediateContext) +{ + HRESULT res = D3D11CreateDeviceAndSwapChain_orig( + pAdapter, DriverType, Software, Flags, + pFeatureLevels, FeatureLevels, SDKVersion, + pSwapChainDesc, ppSwapChain, ppDevice, pFeatureLevel, ppImmediateContext); + if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { + if (pSwapChainDesc) { + d3d11_hooks::note_main_hwnd(pSwapChainDesc->OutputWindow); + } + d3d11_hooks::install_swapchain_hooks(*ppSwapChain); + } + return res; +} + +#define DEFINE_FACTORY_HOOK(NAME, SIG_PARAMS, ORIG_ARGS) \ + HRESULT WINAPI NAME##_hook SIG_PARAMS { \ + HRESULT res = NAME##_orig ORIG_ARGS; \ + if (SUCCEEDED(res) && ppFactory && *ppFactory) { \ + d3d11_hooks::install_factory_hooks( \ + reinterpret_cast(*ppFactory)); \ + } \ + return res; \ + } + +DEFINE_FACTORY_HOOK(CreateDXGIFactory, + (REFIID riid, void **ppFactory), + (riid, ppFactory)) +DEFINE_FACTORY_HOOK(CreateDXGIFactory1, + (REFIID riid, void **ppFactory), + (riid, ppFactory)) +DEFINE_FACTORY_HOOK(CreateDXGIFactory2, + (UINT Flags, REFIID riid, void **ppFactory), + (Flags, riid, ppFactory)) + +#undef DEFINE_FACTORY_HOOK + +// ---------------------------------------------------------------------- +// export trampoline plumbing + +// serializes trampoline_export() so the LDR notification callback and the +// poll thread don't race each other into MinHook against the same target. +std::mutex g_export_mutex; + +bool trampoline_export(const char *dll, const char *name, void *hook, void **orig) { + std::lock_guard lock(g_export_mutex); + if (*orig) { + return true; + } + HMODULE mod = GetModuleHandleA(dll); + if (!mod) { + return false; + } + void *addr = reinterpret_cast(GetProcAddress(mod, name)); + if (!addr) { + return false; + } + *orig = addr; // trampoline_try reads *orig before overwriting it. + if (!detour::trampoline_try(addr, hook, orig)) { + *orig = nullptr; + return false; + } + log_info("graphics::d3d11", "trampolined {}!{}", dll, name); + return true; +} + +void try_install_d3d11_exports() { + if (g_d3d11_exports_hooked) { + return; + } + if (trampoline_export("d3d11.dll", "D3D11CreateDeviceAndSwapChain", + (void *) D3D11CreateDeviceAndSwapChain_hook, + (void **) &D3D11CreateDeviceAndSwapChain_orig)) { + g_d3d11_exports_hooked = true; + } +} + +void try_install_dxgi_exports() { + if (g_dxgi_exports_hooked) { + return; + } + struct entry { const char *name; void *hook; void **orig; }; + const entry entries[] = { + { "CreateDXGIFactory", (void *) CreateDXGIFactory_hook, + (void **) &CreateDXGIFactory_orig }, + { "CreateDXGIFactory1", (void *) CreateDXGIFactory1_hook, + (void **) &CreateDXGIFactory1_orig }, + { "CreateDXGIFactory2", (void *) CreateDXGIFactory2_hook, + (void **) &CreateDXGIFactory2_orig }, + }; + bool any = false; + for (auto &e : entries) { + any |= trampoline_export("dxgi.dll", e.name, e.hook, e.orig); + } + if (any) { + g_dxgi_exports_hooked = true; + } +} + +void try_capture_if_ready() { + if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) { + d3d11_hooks::try_capture_vtables(); + } +} + +// ---------------------------------------------------------------------- +// LDR notification + polling fallback + +bool dll_name_ends_with(PCUNICODE_STRING name, const wchar_t *suffix) { + if (!name || !name->Buffer) { + return false; + } + const size_t n = name->Length / sizeof(WCHAR); + const size_t s = wcslen(suffix); + return n >= s && _wcsnicmp(name->Buffer + n - s, suffix, s) == 0; +} + +VOID CALLBACK ldr_dll_notification( + ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID /*context*/) +{ + if (reason != LDR_DLL_NOTIFICATION_REASON_LOADED || !data) { + return; + } + if (dll_name_ends_with(data->Loaded.BaseDllName, L"d3d11.dll")) { + try_install_d3d11_exports(); + } else if (dll_name_ends_with(data->Loaded.BaseDllName, L"dxgi.dll")) { + try_install_dxgi_exports(); + } +} + +// execexe maps d3d11/dxgi via a path that bypasses LdrLoadDll, so the +// notification above never fires for those DLLs and we have to poll. +std::atomic g_stop { false }; +std::thread g_poll_thread; +std::mutex g_init_mutex; +PVOID g_ldr_cookie = nullptr; + +void poll_thread() { + using namespace std::chrono_literals; + for (int32_t i = 0; i < 120 && !g_stop.load(); ++i) { + try_install_d3d11_exports(); + try_install_dxgi_exports(); + if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) { + d3d11_hooks::try_capture_vtables(); + return; + } + // sliced so shutdown doesn't have to wait a full second. + for (int32_t s = 0; s < 10 && !g_stop.load(); ++s) { + std::this_thread::sleep_for(100ms); + } + } +} + +} // namespace + +void graphics_d3d11_init() { + // dx11 titles always run under execexe. skipping on pure-dx9 games keeps + // their startup path completely untouched (no exports patched, no poll + // thread, no LDR callback). + if (!GetModuleHandleW(L"execexe.dll")) { + return; + } + + std::lock_guard lock(g_init_mutex); + if (g_poll_thread.joinable()) { + return; // already initialized + } + + log_info("graphics::d3d11", "initializing"); + + // trampoline now if either DLL is already in the PEB. + try_install_d3d11_exports(); + try_install_dxgi_exports(); + try_capture_if_ready(); + + // catches standard LdrLoadDll loads. + auto reg = reinterpret_cast( + GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification")); + if (reg) { + NTSTATUS st = reg(0, ldr_dll_notification, nullptr, &g_ldr_cookie); + if (NT_SUCCESS(st)) { + log_info("graphics::d3d11", "registered LDR DLL notification"); + } else { + g_ldr_cookie = nullptr; + log_warning("graphics::d3d11", + "LdrRegisterDllNotification failed: {:#x}", (unsigned long)st); + } + } + + // catches the execexe loader path that bypasses LdrLoadDll. + g_poll_thread = std::thread(poll_thread); +} + +void graphics_d3d11_shutdown() { + std::lock_guard lock(g_init_mutex); + + // unregister first so the callback can't fire mid-teardown. + if (g_ldr_cookie) { + auto unreg = reinterpret_cast( + GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrUnregisterDllNotification")); + if (unreg) { + unreg(g_ldr_cookie); + } + g_ldr_cookie = nullptr; + } + + g_stop.store(true); + if (g_poll_thread.joinable()) { + g_poll_thread.join(); + } +} + +#endif // SPICE_D3D11 diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.h b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.h new file mode 100644 index 0000000..6e06788 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.h @@ -0,0 +1,24 @@ +#pragma once + +#include "overlay/overlay.h" + +void graphics_d3d11_init(); +void graphics_d3d11_shutdown(); + +#ifdef SPICE_D3D11 + +struct ID3D11Device; +struct ID3D11DeviceContext; +struct ID3D11RenderTargetView; +struct IDXGISwapChain; + +namespace overlay::d3d11 { + + void render(ID3D11Device *device, + ID3D11DeviceContext *context, + IDXGISwapChain *swapchain, + ID3D11RenderTargetView **rtv); + +} + +#endif diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_factory.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_factory.cpp new file mode 100644 index 0000000..381d3b3 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_factory.cpp @@ -0,0 +1,102 @@ +// dx11 factory vtable hooks. patches CreateSwapChain / CreateSwapChainForHwnd +// so we can install_swapchain_hooks against every newly-created swapchain. + +#include "d3d11_backend.h" + +#ifdef SPICE_D3D11 + +#include + +#include +#include +#include +#include + +#include "d3d11_internal.h" + +namespace { + +using CreateSwapChain_t = HRESULT(STDMETHODCALLTYPE *)( + IDXGIFactory *, IUnknown *, DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **); +using CreateSwapChainForHwnd_t = HRESULT(STDMETHODCALLTYPE *)( + IDXGIFactory2 *, IUnknown *, HWND, + const DXGI_SWAP_CHAIN_DESC1 *, + const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *, + IDXGIOutput *, IDXGISwapChain1 **); + +CreateSwapChain_t CreateSwapChain_orig = nullptr; +CreateSwapChainForHwnd_t CreateSwapChainForHwnd_orig = nullptr; + +bool g_factory_hooked = false; +bool g_factory2_hooked = false; +std::mutex g_hook_mutex; + +HRESULT STDMETHODCALLTYPE CreateSwapChain_hook( + IDXGIFactory *factory, IUnknown *pDevice, + DXGI_SWAP_CHAIN_DESC *pDesc, IDXGISwapChain **ppSwapChain) +{ + HRESULT res = CreateSwapChain_orig(factory, pDevice, pDesc, ppSwapChain); + if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { + if (pDesc) { + d3d11_hooks::note_main_hwnd(pDesc->OutputWindow); + } + d3d11_hooks::install_swapchain_hooks(*ppSwapChain); + } + return res; +} + +HRESULT STDMETHODCALLTYPE CreateSwapChainForHwnd_hook( + IDXGIFactory2 *factory, IUnknown *pDevice, HWND hWnd, + const DXGI_SWAP_CHAIN_DESC1 *pDesc, + const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc, + IDXGIOutput *pRestrictToOutput, IDXGISwapChain1 **ppSwapChain) +{ + HRESULT res = CreateSwapChainForHwnd_orig( + factory, pDevice, hWnd, pDesc, pFullscreenDesc, pRestrictToOutput, ppSwapChain); + if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { + d3d11_hooks::note_main_hwnd(hWnd); + d3d11_hooks::install_swapchain_hooks(*ppSwapChain); + } + return res; +} + +// QI-and-hook helper: dedupes the IDXGIFactory / IDXGIFactory2 install paths. +template +void install_on(IUnknown *factory, bool &flag, + size_t vtbl_index, void *hook, void **orig, const char *name) +{ + if (flag) { + return; + } + Iface *f = nullptr; + if (FAILED(factory->QueryInterface(IID_PPV_ARGS(&f))) || !f) { + return; + } + if (d3d11_hooks::hook_vtbl(f, vtbl_index, hook, orig, name)) { + flag = true; + } + f->Release(); +} + +} // namespace + +namespace d3d11_hooks { + +void install_factory_hooks(IUnknown *factory) { + if (!factory) { + return; + } + std::lock_guard lock(g_hook_mutex); + + install_on(factory, g_factory_hooked, 10, + (void *) CreateSwapChain_hook, (void **) &CreateSwapChain_orig, + "IDXGIFactory::CreateSwapChain"); + + install_on(factory, g_factory2_hooked, 15, + (void *) CreateSwapChainForHwnd_hook, (void **) &CreateSwapChainForHwnd_orig, + "IDXGIFactory2::CreateSwapChainForHwnd"); +} + +} + +#endif // SPICE_D3D11 diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_internal.h b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_internal.h new file mode 100644 index 0000000..165288d --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_internal.h @@ -0,0 +1,59 @@ +#pragma once + +// internal glue for the dx11 backend. all symbols gated on SPICE_D3D11. + +#include "overlay/overlay.h" + +#ifdef SPICE_D3D11 + +#include + +#include "util/detour.h" +#include "util/logging.h" + +struct HWND__; typedef HWND__ *HWND; +struct IUnknown; +struct IDXGISwapChain; + +namespace d3d11_hooks { + + void install_swapchain_hooks(IDXGISwapChain *swapchain); + void install_factory_hooks(IUnknown *factory); + void try_capture_vtables(); + + // first non-null swapchain HWND wins; later ones (sub-screens, IME + // helpers) are ignored. the dummy capture window is exempted via + // ignore_hwnd. + void note_main_hwnd(HWND hwnd); + HWND main_hwnd(); + void ignore_hwnd(HWND hwnd); + + // capture backbuffer to PNG if a screenshot was requested. + void try_screenshot(IDXGISwapChain *swapchain); + + // trampoline a virtual method by vtable index. on failure *orig is null. + inline bool hook_vtbl(void *iface, size_t index, + void *hook, void **orig, const char *name) + { + void **vtbl = *reinterpret_cast(iface); + void *target = vtbl[index]; + // trampoline_try reads *orig before overwriting it. + *orig = target; + if (!detour::trampoline_try(target, hook, orig)) { + *orig = nullptr; + log_warning("graphics::d3d11", "failed to hook {}", name); + return false; + } + log_info("graphics::d3d11", "hooked {}", name); + return true; + } + + // minimal COM RAII used by capture / screenshot paths. + struct com_release { + void operator()(IUnknown *p) const { if (p) p->Release(); } + }; + template using com_ptr = std::unique_ptr; + +} + +#endif diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_screenshot.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_screenshot.cpp new file mode 100644 index 0000000..af4ceac --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_screenshot.cpp @@ -0,0 +1,165 @@ +// dx11 screenshot capture. mirrors the d3d9 backend: copy the current +// backbuffer into a staging texture, force alpha=255, write PNG via +// stb_image_write, push to clipboard and notify. + +#include "d3d11_backend.h" + +#ifdef SPICE_D3D11 + +#include + +#include +#include +#include + +#include "d3d11_internal.h" + +#include "external/stb_image_write.h" +#include "hooks/graphics/graphics.h" +#include "misc/clipboard.h" +#include "overlay/notifications.h" +#include "util/fileutils.h" + +using d3d11_hooks::com_ptr; + +namespace { + +// copy the swapchain backbuffer into a CPU-readable staging texture and +// flatten it into an RGBA8 buffer (BGRA backbuffers are swizzled, +// alpha is forced to 255). +bool copy_backbuffer_to_rgba(IDXGISwapChain *swapchain, + ID3D11Device *device, + ID3D11DeviceContext *context, + std::vector &out, + uint32_t &out_w, uint32_t &out_h) +{ + ID3D11Texture2D *raw_bb = nullptr; + if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&raw_bb))) || !raw_bb) { + return false; + } + com_ptr backbuffer(raw_bb); + + D3D11_TEXTURE2D_DESC desc {}; + backbuffer->GetDesc(&desc); + + // MSAA backbuffers can't be CopyResource'd into a non-MS staging target. + com_ptr resolved; + ID3D11Texture2D *source = backbuffer.get(); + if (desc.SampleDesc.Count > 1) { + D3D11_TEXTURE2D_DESC rd = desc; + rd.SampleDesc.Count = 1; + rd.SampleDesc.Quality = 0; + rd.Usage = D3D11_USAGE_DEFAULT; + rd.BindFlags = D3D11_BIND_RENDER_TARGET; + rd.CPUAccessFlags = 0; + rd.MiscFlags = 0; + ID3D11Texture2D *r = nullptr; + if (FAILED(device->CreateTexture2D(&rd, nullptr, &r)) || !r) { + return false; + } + resolved.reset(r); + context->ResolveSubresource(resolved.get(), 0, backbuffer.get(), 0, desc.Format); + source = resolved.get(); + } + + D3D11_TEXTURE2D_DESC sd {}; + sd.Width = desc.Width; + sd.Height = desc.Height; + sd.MipLevels = 1; + sd.ArraySize = 1; + sd.Format = desc.Format; + sd.SampleDesc.Count = 1; + sd.Usage = D3D11_USAGE_STAGING; + sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + ID3D11Texture2D *raw_staging = nullptr; + if (FAILED(device->CreateTexture2D(&sd, nullptr, &raw_staging)) || !raw_staging) { + return false; + } + com_ptr staging(raw_staging); + context->CopyResource(staging.get(), source); + + D3D11_MAPPED_SUBRESOURCE mapped {}; + if (FAILED(context->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped))) { + return false; + } + + // backbuffers from GetDesc are always fully-typed (never _TYPELESS). + const bool is_bgra = desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM + || desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB; + + out.resize(static_cast(desc.Width) * desc.Height * 4); + const uint8_t *src_base = reinterpret_cast(mapped.pData); + for (uint32_t y = 0; y < desc.Height; ++y) { + const uint8_t *row = src_base + static_cast(y) * mapped.RowPitch; + uint8_t *dst = out.data() + static_cast(y) * desc.Width * 4; + for (uint32_t x = 0; x < desc.Width; ++x) { + dst[x * 4 + 0] = row[x * 4 + (is_bgra ? 2 : 0)]; + dst[x * 4 + 1] = row[x * 4 + 1]; + dst[x * 4 + 2] = row[x * 4 + (is_bgra ? 0 : 2)]; + dst[x * 4 + 3] = 255; + } + } + + context->Unmap(staging.get(), 0); + + out_w = desc.Width; + out_h = desc.Height; + return true; +} + +} // namespace + +namespace d3d11_hooks { + +void try_screenshot(IDXGISwapChain *swapchain) { + if (!swapchain || !graphics_screenshot_consume()) { + return; + } + + auto file_path = graphics_screenshot_genpath(); + if (file_path.empty()) { + return; + } + + ID3D11Device *raw_device = nullptr; + if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&raw_device))) || !raw_device) { + return; + } + com_ptr device(raw_device); + ID3D11DeviceContext *raw_ctx = nullptr; + device->GetImmediateContext(&raw_ctx); + if (!raw_ctx) { + return; + } + com_ptr context(raw_ctx); + + std::vector pixels; + uint32_t w = 0, h = 0; + if (!copy_backbuffer_to_rgba(swapchain, device.get(), context.get(), pixels, w, h)) { + log_warning("graphics::d3d11", "screenshot: failed to capture backbuffer"); + overlay::notifications::add( + overlay::notifications::Severity::Error, + "Screenshot failed to capture"); + return; + } + + log_info("graphics::d3d11", "saving screenshot to {}", file_path); + if (stbi_write_png(file_path.c_str(), (int) w, (int) h, 4, + pixels.data(), (int) w * 4)) + { + clipboard::copy_image(file_path); + overlay::notifications::add( + overlay::notifications::Severity::Success, + fmt::format("Screenshot saved: {}", fileutils::basename(file_path))); + } else { + log_warning("graphics::d3d11", "screenshot: stbi_write_png failed"); + overlay::notifications::add( + overlay::notifications::Severity::Error, + "Screenshot failed to save"); + } +} + +} + +#endif // SPICE_D3D11 diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp new file mode 100644 index 0000000..bffe2ec --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp @@ -0,0 +1,289 @@ +// dx11 swapchain vtable hooks + per-frame overlay pump. +// +// dxgi shares vtables across swapchain instances, so we only need to patch +// Present / Present1 / ResizeBuffers once on the first instance we see. +// each frame we lazily attach the overlay to whichever swapchain is +// presenting, then drive its imgui update / new_frame / render cycle. + +#include "d3d11_backend.h" + +#ifdef SPICE_D3D11 + +#include +#include + +#include +#include +#include +#include + +#include "d3d11_internal.h" + +#include "external/imgui/imgui.h" +#include "external/imgui/backends/imgui_impl_dx11.h" +#include "overlay/imgui/impl_spice.h" + +#include "games/io.h" +#include "hooks/graphics/graphics.h" +#include "launcher/launcher.h" +#include "misc/eamuse.h" + +// -------------------------------------------------------------------------- +// overlay render bridge + +namespace overlay::d3d11 { + + // sRGB backbuffers need a UNORM view: ImGui vertex colors are already + // sRGB-encoded, so an extra linear->sRGB conversion would wash the + // overlay out white. + static DXGI_FORMAT to_unorm_view(DXGI_FORMAT fmt) { + switch (fmt) { + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; + default: return fmt; + } + } + + static void ensure_rtv(ID3D11Device *device, + IDXGISwapChain *swapchain, + ID3D11RenderTargetView **rtv) + { + if (*rtv || !device || !swapchain) { + return; + } + ID3D11Texture2D *backbuffer = nullptr; + if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&backbuffer))) || !backbuffer) { + return; + } + D3D11_TEXTURE2D_DESC td {}; + backbuffer->GetDesc(&td); + const DXGI_FORMAT view_fmt = to_unorm_view(td.Format); + if (view_fmt != td.Format) { + D3D11_RENDER_TARGET_VIEW_DESC rtvd {}; + rtvd.Format = view_fmt; + rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + device->CreateRenderTargetView(backbuffer, &rtvd, rtv); + } else { + device->CreateRenderTargetView(backbuffer, nullptr, rtv); + } + backbuffer->Release(); + } + + // bind the backbuffer (lazily creating the RTV) and draw the imgui + // frame on top. reset_invalidate releases *rtv on ResizeBuffers. + void render(ID3D11Device *device, + ID3D11DeviceContext *context, + IDXGISwapChain *swapchain, + ID3D11RenderTargetView **rtv) + { + ensure_rtv(device, swapchain, rtv); + if (!*rtv || !context) { + return; + } + // present happens immediately after, so no need to save the previous + // RT binding (flip-model resets it anyway). + context->OMSetRenderTargets(1, rtv, nullptr); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + } + +} + +// -------------------------------------------------------------------------- +// file-local state + per-frame helpers + +namespace { + +using Present_t = HRESULT(STDMETHODCALLTYPE *)( + IDXGISwapChain *, UINT, UINT); +using ResizeBuffers_t = HRESULT(STDMETHODCALLTYPE *)( + IDXGISwapChain *, UINT, UINT, UINT, DXGI_FORMAT, UINT); +using Present1_t = HRESULT(STDMETHODCALLTYPE *)( + IDXGISwapChain1 *, UINT, UINT, const DXGI_PRESENT_PARAMETERS *); + +Present_t Present_orig = nullptr; +ResizeBuffers_t ResizeBuffers_orig = nullptr; +Present1_t Present1_orig = nullptr; + +bool g_swapchain_hooked = false; +bool g_swapchain1_hooked = false; + +void try_create_overlay(IDXGISwapChain *swapchain) { + if (!swapchain || overlay::OVERLAY) { + return; + } + + DXGI_SWAP_CHAIN_DESC desc {}; + if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) { + return; + } + + // only attach to the main game window; ignore sub-screens / IME helpers. + HWND main = d3d11_hooks::main_hwnd(); + if (main && desc.OutputWindow != main) { + return; + } + + ID3D11Device *device = nullptr; + if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) { + return; + } + ID3D11DeviceContext *context = nullptr; + device->GetImmediateContext(&context); + + if (context) { + overlay::create_d3d11(desc.OutputWindow, device, context, swapchain); + RECT cr {}; + ::GetClientRect(desc.OutputWindow, &cr); + log_info("graphics::d3d11", + "attached overlay to swapchain hwnd=0x{:x} backbuffer={}x{} client={}x{}", + (uintptr_t) desc.OutputWindow, + desc.BufferDesc.Width, desc.BufferDesc.Height, + cr.right - cr.left, cr.bottom - cr.top); + context->Release(); + } + device->Release(); +} + +// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour). +void poll_screenshot_hotkey() { + static bool s_down = false; + auto buttons = games::get_buttons_overlay(eamuse_get_game()); + const bool pressed = buttons + && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) + && GameAPI::Buttons::getState(RI_MGR, + buttons->at(games::OverlayButtons::Screenshot)); + if (pressed && !s_down) { + graphics_screenshot_trigger(); + } + s_down = pressed; +} + +void pump_overlay(IDXGISwapChain *swapchain) { + if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) { + return; + } + + poll_screenshot_hotkey(); + + // size imgui to the backbuffer (not window client). dxgi may upscale + // a small backbuffer into a larger client rect; without this override + // imgui would draw past the RTV and the mouse mapping would be off. + DXGI_SWAP_CHAIN_DESC desc {}; + if (SUCCEEDED(swapchain->GetDesc(&desc))) { + ImGui_ImplSpice_SetDisplaySizeOverride( + (float) desc.BufferDesc.Width, + (float) desc.BufferDesc.Height); + } + + overlay::OVERLAY->update(); + overlay::OVERLAY->new_frame(); + overlay::OVERLAY->render(); + + // after overlay render so toasts/menus end up in the saved image. + d3d11_hooks::try_screenshot(swapchain); +} + +// ---------------------------------------------------------------------- +// swapchain method hooks + +HRESULT STDMETHODCALLTYPE Present_hook( + IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags) +{ + try_create_overlay(swapchain); + pump_overlay(swapchain); + return Present_orig(swapchain, SyncInterval, Flags); +} + +HRESULT STDMETHODCALLTYPE Present1_hook( + IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags, + const DXGI_PRESENT_PARAMETERS *pParams) +{ + try_create_overlay(swapchain); + pump_overlay(swapchain); + return Present1_orig(swapchain, SyncInterval, Flags, pParams); +} + +HRESULT STDMETHODCALLTYPE ResizeBuffers_hook( + IDXGISwapChain *swapchain, UINT BufferCount, UINT Width, UINT Height, + DXGI_FORMAT NewFormat, UINT SwapChainFlags) +{ + const bool ours = overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain); + if (ours) { + log_info("graphics::d3d11", "ResizeBuffers {}x{} fmt={}", + Width, Height, (int32_t) NewFormat); + overlay::OVERLAY->reset_invalidate(); + } + HRESULT res = ResizeBuffers_orig( + swapchain, BufferCount, Width, Height, NewFormat, SwapChainFlags); + if (ours && SUCCEEDED(res)) { + overlay::OVERLAY->reset_recreate(); + } + return res; +} + +} // namespace + +// -------------------------------------------------------------------------- +// d3d11_hooks public surface: main-window tracking + vtable install. + +namespace d3d11_hooks { + +namespace { + std::atomic g_main_hwnd { nullptr }; + std::atomic g_ignored_hwnd { nullptr }; +} + +void note_main_hwnd(HWND hwnd) { + if (!hwnd || hwnd == g_ignored_hwnd.load()) { + return; + } + HWND expected = nullptr; + if (g_main_hwnd.compare_exchange_strong(expected, hwnd)) { + log_info("graphics::d3d11", "main hwnd recorded: 0x{:x}", + (uintptr_t) hwnd); + } +} + +HWND main_hwnd() { + return g_main_hwnd.load(); +} + +void ignore_hwnd(HWND hwnd) { + g_ignored_hwnd.store(hwnd); +} + +// patch IDXGISwapChain::Present + ResizeBuffers and (if implemented) +// IDXGISwapChain1::Present1. idempotent; flag is set only after success +// so failed attempts can be retried on the next swapchain. +void install_swapchain_hooks(IDXGISwapChain *swapchain) { + if (!swapchain) { + return; + } + static std::mutex s_hook_mutex; + std::lock_guard lock(s_hook_mutex); + + if (!g_swapchain_hooked) { + const bool a = hook_vtbl(swapchain, 8, (void *) Present_hook, + (void **) &Present_orig, "IDXGISwapChain::Present"); + const bool b = hook_vtbl(swapchain, 13, (void *) ResizeBuffers_hook, + (void **) &ResizeBuffers_orig, "IDXGISwapChain::ResizeBuffers"); + if (a && b) { + g_swapchain_hooked = true; + } + } + + if (!g_swapchain1_hooked) { + IDXGISwapChain1 *sc1 = nullptr; + if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) { + if (hook_vtbl(sc1, 22, (void *) Present1_hook, + (void **) &Present1_orig, "IDXGISwapChain1::Present1")) { + g_swapchain1_hooked = true; + } + sc1->Release(); + } + } +} + +} + +#endif // SPICE_D3D11 diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp new file mode 100644 index 0000000..28601c2 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp @@ -0,0 +1,175 @@ +// proactive vtable capture for the dx11 backend. +// +// titles under the execexe loader routinely race past our export-level +// trampolines, so the game's first real swapchain never goes through us. +// we sidestep that by creating a throwaway device + swapchain ourselves +// the moment d3d11.dll + dxgi.dll appear, which patches the shared +// IDXGISwapChain[1] / IDXGIFactory[2] vtables ahead of the game. + +#include "d3d11_backend.h" + +#ifdef SPICE_D3D11 + +#include +#include + +#include +#include +#include +#include + +#include "d3d11_internal.h" + +using d3d11_hooks::com_ptr; + +namespace { + +using D3D11CreateDevice_t = HRESULT(WINAPI *)( + IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT, + const D3D_FEATURE_LEVEL *, UINT, UINT, + ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **); +using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **); +using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **); + +std::atomic g_vtables_captured { false }; + +template +Fn resolve(HMODULE mod, const char *name) { + return reinterpret_cast(GetProcAddress(mod, name)); +} + +com_ptr create_factory2(CreateDXGIFactory2_t f2, + CreateDXGIFactory1_t f1) +{ + IDXGIFactory2 *raw = nullptr; + if (f2 && SUCCEEDED(f2(0, IID_PPV_ARGS(&raw))) && raw) { + return com_ptr(raw); + } + IDXGIFactory1 *factory1 = nullptr; + if (f1 && SUCCEEDED(f1(IID_PPV_ARGS(&factory1))) && factory1) { + factory1->QueryInterface(IID_PPV_ARGS(&raw)); + factory1->Release(); + } + return com_ptr(raw); +} + +bool create_dummy_device(D3D11CreateDevice_t create, + com_ptr &device, + com_ptr &context) +{ + static constexpr D3D_FEATURE_LEVEL levels[] = { + D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, + }; + // hardware first, then WARP so headless / unusual configs still work. + for (auto type : { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP }) { + ID3D11Device *d = nullptr; + ID3D11DeviceContext *c = nullptr; + D3D_FEATURE_LEVEL got; + if (SUCCEEDED(create(nullptr, type, nullptr, 0, + levels, ARRAYSIZE(levels), D3D11_SDK_VERSION, + &d, &got, &c)) && d) { + device.reset(d); + context.reset(c); + return true; + } + } + return false; +} + +} // namespace + +namespace d3d11_hooks { + +// create a throwaway device + swapchain to patch the shared vtables before +// the game's loader races past our export trampolines. safe to call +// repeatedly; runs at most once. +void try_capture_vtables() { + if (g_vtables_captured.load()) { + return; + } + + HMODULE d3d11 = GetModuleHandleW(L"d3d11.dll"); + HMODULE dxgi = GetModuleHandleW(L"dxgi.dll"); + if (!d3d11 || !dxgi) { + return; + } + + auto create_device = resolve(d3d11, "D3D11CreateDevice"); + auto f2 = resolve(dxgi, "CreateDXGIFactory2"); + auto f1 = resolve(dxgi, "CreateDXGIFactory1"); + if (!create_device || (!f1 && !f2)) { + return; + } + + // serialize concurrent calls (poll thread + LDR notification). only + // flip g_vtables_captured after success so failed attempts remain + // retriable on the next tick. + static std::atomic in_progress { false }; + if (in_progress.exchange(true)) { + return; + } + struct scope_clear { + std::atomic &flag; + ~scope_clear() { flag.store(false); } + } clear { in_progress }; + + // hidden message-only window; STATIC is always registered by user32. + HWND dummy_hwnd = CreateWindowExW( + 0, L"STATIC", L"", 0, 0, 0, 1, 1, + HWND_MESSAGE, nullptr, GetModuleHandleW(nullptr), nullptr); + if (!dummy_hwnd) { + log_warning("graphics::d3d11", + "vtable capture: CreateWindowExW failed (gle={})", (unsigned long)GetLastError()); + return; + } + auto destroy_hwnd = std::unique_ptr( + dummy_hwnd, &DestroyWindow); + + // if the game's CreateDXGIFactory_hook already raced us, our + // CreateSwapChainForHwnd call below would trip the hook and try to + // record dummy_hwnd as the main window. block that. + ignore_hwnd(dummy_hwnd); + + auto factory2 = create_factory2(f2, f1); + if (!factory2) { + log_warning("graphics::d3d11", "vtable capture: CreateDXGIFactory* failed"); + return; + } + + com_ptr device; + com_ptr context; + if (!create_dummy_device(create_device, device, context)) { + log_warning("graphics::d3d11", "vtable capture: D3D11CreateDevice failed"); + return; + } + + DXGI_SWAP_CHAIN_DESC1 desc {}; + desc.Width = 1; + desc.Height = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + desc.BufferCount = 2; + desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + + IDXGISwapChain1 *raw_sc = nullptr; + HRESULT hr = factory2->CreateSwapChainForHwnd( + device.get(), dummy_hwnd, &desc, nullptr, nullptr, &raw_sc); + if (FAILED(hr) || !raw_sc) { + log_warning("graphics::d3d11", + "vtable capture: CreateSwapChainForHwnd failed (hr={:#x})", (unsigned long)hr); + return; + } + com_ptr swapchain(raw_sc); + + install_swapchain_hooks(swapchain.get()); + install_factory_hooks(factory2.get()); + + g_vtables_captured.store(true); + log_info("graphics::d3d11", "vtable capture complete (via dummy swapchain)"); +} + +} + +#endif // SPICE_D3D11 diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index ca41aa0..9e7f46a 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -18,6 +18,7 @@ #include "games/iidx/iidx.h" #include "games/popn/popn.h" #include "hooks/graphics/backends/d3d9/d3d9_backend.h" +#include "hooks/graphics/backends/d3d11/d3d11_backend.h" #include "launcher/shutdown.h" #include "overlay/overlay.h" #include "touch/touch.h" @@ -859,6 +860,7 @@ void graphics_init() { // init backends graphics_d3d9_init(); + graphics_d3d11_init(); // general hooks ChangeDisplaySettingsA_orig = detour::iat_try("ChangeDisplaySettingsA", ChangeDisplaySettingsA_hook); diff --git a/src/spice2x/launcher/shutdown.cpp b/src/spice2x/launcher/shutdown.cpp index 15a9ca6..a2a616c 100644 --- a/src/spice2x/launcher/shutdown.cpp +++ b/src/spice2x/launcher/shutdown.cpp @@ -8,6 +8,7 @@ #include "rawinput/rawinput.h" #include "hooks/audio/audio.h" #include "hooks/graphics/graphics.h" +#include "hooks/graphics/backends/d3d11/d3d11_backend.h" #include "util/deferlog.h" #include "util/logging.h" @@ -26,6 +27,10 @@ namespace launcher { sdk::fini_sdk_modules(); + // stop dx11 background workers (poll thread, LDR notification) + // before anything else, so they can't race against the teardown. + graphics_d3d11_shutdown(); + // reset monitor settings reset_monitor_on_exit(); diff --git a/src/spice2x/overlay/imgui/impl_spice.cpp b/src/spice2x/overlay/imgui/impl_spice.cpp index 918199d..0c61e3e 100644 --- a/src/spice2x/overlay/imgui/impl_spice.cpp +++ b/src/spice2x/overlay/imgui/impl_spice.cpp @@ -41,6 +41,8 @@ static INT64 g_TicksPerSecond = 0; static ImGuiMouseCursor g_LastMouseCursor = ImGuiMouseCursor_COUNT; static double g_LastMouseMovement = 0.f; static bool g_MouseCursorAutoHide = false; +static float g_DisplaySizeOverrideW = 0.0f; +static float g_DisplaySizeOverrideH = 0.0f; constexpr size_t VKEY_MAX = 255; static std::array g_KeysDown; @@ -179,12 +181,25 @@ void ImGui_ImplSpice_Shutdown() { void ImGui_ImplSpice_UpdateDisplaySize() { + // backends that render to a surface different from the window's client + // area (e.g. dx11 swapchain backbuffer) can override the size here so + // ImGui's viewport matches the actual render target. + if (g_DisplaySizeOverrideW > 0.0f && g_DisplaySizeOverrideH > 0.0f) { + ImGui::GetIO().DisplaySize = ImVec2(g_DisplaySizeOverrideW, g_DisplaySizeOverrideH); + return; + } + // get display size RECT rect; ::GetClientRect(g_hWnd, &rect); ImGui::GetIO().DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top)); } +void ImGui_ImplSpice_SetDisplaySizeOverride(float w, float h) { + g_DisplaySizeOverrideW = w; + g_DisplaySizeOverrideH = h; +} + bool ImGui_ImplSpice_UpdateMouseCursor() { // check if cursor should be changed diff --git a/src/spice2x/overlay/imgui/impl_spice.h b/src/spice2x/overlay/imgui/impl_spice.h index bf53845..63ba59b 100644 --- a/src/spice2x/overlay/imgui/impl_spice.h +++ b/src/spice2x/overlay/imgui/impl_spice.h @@ -6,5 +6,6 @@ IMGUI_IMPL_API bool ImGui_ImplSpice_Init(HWND hWnd); IMGUI_IMPL_API void ImGui_ImplSpice_Shutdown(); IMGUI_IMPL_API void ImGui_ImplSpice_UpdateDisplaySize(); +IMGUI_IMPL_API void ImGui_ImplSpice_SetDisplaySizeOverride(float w, float h); IMGUI_IMPL_API bool ImGui_ImplSpice_UpdateMouseCursor(); IMGUI_IMPL_API void ImGui_ImplSpice_NewFrame(); diff --git a/src/spice2x/overlay/overlay.cpp b/src/spice2x/overlay/overlay.cpp index 46d39cd..c761868 100644 --- a/src/spice2x/overlay/overlay.cpp +++ b/src/spice2x/overlay/overlay.cpp @@ -15,10 +15,19 @@ #include "build/resource.h" #include "external/imgui/backends/imgui_impl_dx9.h" +#ifdef SPICE_D3D11 +#include "external/imgui/backends/imgui_impl_dx11.h" +#include "hooks/graphics/backends/d3d11/d3d11_backend.h" +#endif #include "overlay/imgui/impl_spice.h" #include "overlay/imgui/impl_sw.h" #include "overlay/notifications.h" +#ifdef SPICE_D3D11 +#include +#include +#endif + #include "window.h" #ifdef SPICE64 #include "windows/camera_control.h" @@ -105,6 +114,21 @@ void overlay::create_d3d9(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device) } } +#ifdef SPICE_D3D11 +void overlay::create_d3d11(HWND hWnd, ID3D11Device *device, ID3D11DeviceContext *context, + IDXGISwapChain *swapchain) { + if (!overlay::ENABLED) { + return; + } + + const std::lock_guard lock(OVERLAY_MUTEX); + + if (!overlay::OVERLAY) { + overlay::OVERLAY = std::make_unique(hWnd, device, context, swapchain); + } +} +#endif + void overlay::create_software(HWND hWnd) { if (!overlay::ENABLED) { return; @@ -165,6 +189,27 @@ overlay::SpiceOverlay::SpiceOverlay(HWND hWnd) this->init(); } +#ifdef SPICE_D3D11 +overlay::SpiceOverlay::SpiceOverlay(HWND hWnd, ID3D11Device *d3d11_device, + ID3D11DeviceContext *d3d11_context, + IDXGISwapChain *d3d11_swapchain) + : renderer(OverlayRenderer::D3D11), + hWnd(hWnd), + d3d11_device(d3d11_device), + d3d11_context(d3d11_context), + d3d11_swapchain(d3d11_swapchain) { + log_info("overlay", "initializing (D3D11)"); + + // increment reference counts + this->d3d11_device->AddRef(); + this->d3d11_context->AddRef(); + this->d3d11_swapchain->AddRef(); + + // init + this->init(); +} +#endif + void overlay::SpiceOverlay::init() { // init imgui @@ -317,6 +362,11 @@ void overlay::SpiceOverlay::init() { case OverlayRenderer::D3D9: ImGui_ImplDX9_Init(this->device); break; +#ifdef SPICE_D3D11 + case OverlayRenderer::D3D11: + ImGui_ImplDX11_Init(this->d3d11_device, this->d3d11_context); + break; +#endif case OverlayRenderer::SOFTWARE: imgui_sw::bind_imgui_painting(); break; @@ -327,6 +377,11 @@ void overlay::SpiceOverlay::init() { case OverlayRenderer::D3D9: ImGui_ImplDX9_NewFrame(); break; +#ifdef SPICE_D3D11 + case OverlayRenderer::D3D11: + ImGui_ImplDX11_NewFrame(); + break; +#endif case OverlayRenderer::SOFTWARE: break; } @@ -449,6 +504,21 @@ overlay::SpiceOverlay::~SpiceOverlay() { this->d3d->Release(); break; +#ifdef SPICE_D3D11 + case OverlayRenderer::D3D11: + if (this->d3d11_rtv) { + this->d3d11_rtv->Release(); + this->d3d11_rtv = nullptr; + } + ImGui_ImplDX11_Shutdown(); + + // drop references + this->d3d11_swapchain->Release(); + this->d3d11_context->Release(); + this->d3d11_device->Release(); + + break; +#endif case OverlayRenderer::SOFTWARE: imgui_sw::unbind_imgui_painting(); break; @@ -484,6 +554,15 @@ void overlay::SpiceOverlay::new_frame() { case OverlayRenderer::D3D9: ImGui_ImplDX9_NewFrame(); break; +#ifdef SPICE_D3D11 + case OverlayRenderer::D3D11: + // refresh DisplaySize each frame; the dx11 swapchain hook pushes + // the current backbuffer dimensions into impl_spice via the + // size override so ImGui's viewport tracks ResizeBuffers calls. + ImGui_ImplSpice_UpdateDisplaySize(); + ImGui_ImplDX11_NewFrame(); + break; +#endif case OverlayRenderer::SOFTWARE: ImGui_ImplSpice_UpdateDisplaySize(); break; @@ -526,6 +605,12 @@ void overlay::SpiceOverlay::render() { case OverlayRenderer::D3D9: ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData()); break; +#ifdef SPICE_D3D11 + case OverlayRenderer::D3D11: + overlay::d3d11::render(this->d3d11_device, this->d3d11_context, + this->d3d11_swapchain, &this->d3d11_rtv); + break; +#endif case OverlayRenderer::SOFTWARE: { // get display metrics @@ -705,11 +790,44 @@ bool overlay::SpiceOverlay::hotkeys_triggered() { } void overlay::SpiceOverlay::reset_invalidate() { - ImGui_ImplDX9_InvalidateDeviceObjects(); + if (!overlay::OVERLAY) { + return; + } + switch (overlay::OVERLAY->renderer) { + case OverlayRenderer::D3D9: + ImGui_ImplDX9_InvalidateDeviceObjects(); + break; +#ifdef SPICE_D3D11 + case OverlayRenderer::D3D11: + // for DX11 a ResizeBuffers only invalidates the backbuffer RTV; the imgui + // device objects (shaders, buffers, textures) remain valid. + if (overlay::OVERLAY->d3d11_rtv) { + overlay::OVERLAY->d3d11_rtv->Release(); + overlay::OVERLAY->d3d11_rtv = nullptr; + } + break; +#endif + case OverlayRenderer::SOFTWARE: + break; + } } void overlay::SpiceOverlay::reset_recreate() { - ImGui_ImplDX9_CreateDeviceObjects(); + if (!overlay::OVERLAY) { + return; + } + switch (overlay::OVERLAY->renderer) { + case OverlayRenderer::D3D9: + ImGui_ImplDX9_CreateDeviceObjects(); + break; +#ifdef SPICE_D3D11 + case OverlayRenderer::D3D11: + // RTV is lazily recreated on the next render() + break; +#endif + case OverlayRenderer::SOFTWARE: + break; + } } void overlay::SpiceOverlay::input_char(unsigned int c) { diff --git a/src/spice2x/overlay/overlay.h b/src/spice2x/overlay/overlay.h index 54f1229..d7d595c 100644 --- a/src/spice2x/overlay/overlay.h +++ b/src/spice2x/overlay/overlay.h @@ -10,11 +10,22 @@ #include "external/imgui/imgui.h" +#ifdef SPICE_D3D11 +// forward decls for D3D11 (avoid pulling d3d11.h into every TU) +struct ID3D11Device; +struct ID3D11DeviceContext; +struct ID3D11RenderTargetView; +struct IDXGISwapChain; +#endif + namespace overlay { class Window; enum class OverlayRenderer { D3D9, +#ifdef SPICE_D3D11 + D3D11, +#endif SOFTWARE, }; @@ -62,6 +73,10 @@ namespace overlay { Window *window_log = nullptr; explicit SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device); +#ifdef SPICE_D3D11 + explicit SpiceOverlay(HWND hWnd, ID3D11Device *d3d11_device, + ID3D11DeviceContext *d3d11_context, IDXGISwapChain *d3d11_swapchain); +#endif explicit SpiceOverlay(HWND hWnd); ~SpiceOverlay(); @@ -92,6 +107,14 @@ namespace overlay { inline bool uses_device(IDirect3DDevice9 *other) { return this->device == other; } +#ifdef SPICE_D3D11 + inline bool uses_device(ID3D11Device *other) { + return this->d3d11_device == other; + } + inline bool uses_swapchain(IDXGISwapChain *other) { + return this->d3d11_swapchain == other; + } +#endif inline IDirect3DDevice9 *get_device() { return this->device; } @@ -124,6 +147,14 @@ namespace overlay { IDirect3D9 *d3d = nullptr; IDirect3DDevice9 *device = nullptr; +#ifdef SPICE_D3D11 + // D3D11 + ID3D11Device *d3d11_device = nullptr; + ID3D11DeviceContext *d3d11_context = nullptr; + IDXGISwapChain *d3d11_swapchain = nullptr; + ID3D11RenderTargetView *d3d11_rtv = nullptr; +#endif + // software std::vector pixel_data; size_t pixel_data_width = 0; @@ -156,6 +187,10 @@ namespace overlay { // synchronized helpers void create_d3d9(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device); +#ifdef SPICE_D3D11 + void create_d3d11(HWND hWnd, ID3D11Device *device, ID3D11DeviceContext *context, + IDXGISwapChain *swapchain); +#endif void create_software(HWND hWnd); void destroy(HWND hWnd = nullptr); }