diff --git a/code/framework/CMakeLists.txt b/code/framework/CMakeLists.txt index 24eef1109..90e380f31 100644 --- a/code/framework/CMakeLists.txt +++ b/code/framework/CMakeLists.txt @@ -91,9 +91,11 @@ list(APPEND FRAMEWORK_CLIENT_SRC src/gui/view.cpp src/gui/sdk.cpp src/gui/backend/view_d3d11.cpp + src/gui/backend/view_d3d12.cpp src/gui/cef/app.cpp src/gui/cef/renderer_app.cpp src/gui/cef/client.cpp + src/gui/cef/disk_resource_handler.cpp src/gui/cef/render_handler.cpp src/gui/cef/life_span_handler.cpp src/gui/cef/load_handler.cpp diff --git a/code/framework/src/graphics/backend/d3d12.cpp b/code/framework/src/graphics/backend/d3d12.cpp index 743abdfe1..0ee9862c1 100644 --- a/code/framework/src/graphics/backend/d3d12.cpp +++ b/code/framework/src/graphics/backend/d3d12.cpp @@ -9,18 +9,25 @@ #include "d3d12.h" #include +#include + +#include namespace Framework::Graphics { bool D3D12Backend::Init(const Framework::Graphics::RendererConfiguration &opts) { const auto swapChain = opts.d3d12.swapchain; const auto commandQueue = opts.d3d12.commandQueue; - _device = opts.d3d12.device; _context = opts.d3d12.deviceContext; - // #1 get device from swapchain (maybe different device) + // #1 get device from swapchain — and use it as THE device everywhere: + // descriptor heaps/RTVs below are created from it, so SRVs, textures + // and fences created later through GetDevice() must match or D3D12 + // rejects the mixed-device usage. opts.d3d12.device is ignored if it + // differs. ID3D12Device *pD3DDevice; if (FAILED(swapChain->GetDevice(__uuidof(ID3D12Device), (void **)&pD3DDevice))) { return false; } + _device = pD3DDevice; _swapChain = swapChain; _commandQueue = commandQueue; @@ -39,12 +46,21 @@ namespace Framework::Graphics { { D3D12_DESCRIPTOR_HEAP_DESC desc {}; desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; - desc.NumDescriptors = _frameBufferCount; + desc.NumDescriptors = _frameBufferCount + kExtraSrvSlots; desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; if (pD3DDevice->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&_srvHeap)) != S_OK) { return false; } + + _srvDescriptorSize = pD3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + + // Slots [0, _frameBufferCount) stay reserved for ImGui (its font SRV + // sits at the heap start); the rest are up for allocation + _freeSrvSlots.clear(); + for (UINT i = 0; i < kExtraSrvSlots; i++) { + _freeSrvSlots.push_back(_frameBufferCount + i); + } } // #3 create rtv heap @@ -62,10 +78,11 @@ namespace Framework::Graphics { const auto rtvDescriptorSize = pD3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = _rtvHeap->GetCPUDescriptorHandleForHeapStart(); + // Reserve one RTV descriptor slot per frame index; the back-buffer + // resource (and its RTV) is bound fresh each frame in Begin(), so we + // don't grab the buffers here. for (UINT i = 0; i < _frameBufferCount; i++) { _frameContext[i]._mainRenderTargetDescriptor = rtvHandle; - swapChain->GetBuffer(i, IID_PPV_ARGS(&_frameContext[i]._mainRenderTargetResource)); - pD3DDevice->CreateRenderTargetView(_frameContext[i]._mainRenderTargetResource, nullptr, rtvHandle); rtvHandle.ptr += rtvDescriptorSize; } } @@ -93,22 +110,37 @@ namespace Framework::Graphics { void D3D12Backend::Shutdown() { // release objects + if (_currentBackBuffer) { + _currentBackBuffer->Release(); + _currentBackBuffer = nullptr; + } _rtvHeap->Release(); _srvHeap->Release(); _commandList->Release(); for (const auto &frameContext : _frameContext) { frameContext._commandAllocator->Release(); - frameContext._mainRenderTargetResource->Release(); } } void D3D12Backend::Begin() { - const auto ¤tFrameContext = _frameContext[_swapChain->GetCurrentBackBufferIndex()]; + const UINT idx = _swapChain->GetCurrentBackBufferIndex(); + const auto ¤tFrameContext = _frameContext[idx]; currentFrameContext._commandAllocator->Reset(); + // Acquire the current back buffer fresh (AddRef'd) and bind its RTV. It is + // released in End() — we never hold it across frames. The swapchain owns + // the buffer, so dropping our reference doesn't free it; the GPU keeps + // reading it for this frame via the swapchain's own hold. + _currentBackBuffer = nullptr; + if (FAILED(_swapChain->GetBuffer(idx, IID_PPV_ARGS(&_currentBackBuffer)))) { + _currentBackBuffer = nullptr; + return; + } + _device->CreateRenderTargetView(_currentBackBuffer, nullptr, currentFrameContext._mainRenderTargetDescriptor); + _barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; _barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; - _barrier.Transition.pResource = currentFrameContext._mainRenderTargetResource; + _barrier.Transition.pResource = _currentBackBuffer; _barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; _barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; _barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; @@ -120,11 +152,20 @@ namespace Framework::Graphics { } void D3D12Backend::End() { + if (!_currentBackBuffer) { + return; // Begin() failed to acquire this frame + } + _barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; _barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; _commandList->ResourceBarrier(1, &_barrier); _commandList->Close(); _commandQueue->ExecuteCommandLists(1, (ID3D12CommandList **)&_commandList); + + // Drop our per-frame reference now that the work is submitted. Safe: the + // swapchain still owns the buffer, so it stays alive for the GPU. + _currentBackBuffer->Release(); + _currentBackBuffer = nullptr; } void D3D12Backend::Update() {} @@ -132,4 +173,95 @@ namespace Framework::Graphics { int D3D12Backend::NumFramesInFlight() const { return _frameBufferCount; } + + int D3D12Backend::AllocateSRVSlot() { + if (!_srvHeap) { + Framework::Logging::GetLogger(FRAMEWORK_INNER_GRAPHICS)->error("D3D12Backend::AllocateSRVSlot, no descriptor heap"); + return -1; + } + if (_freeSrvSlots.empty()) { + Framework::Logging::GetLogger(FRAMEWORK_INNER_GRAPHICS)->error("D3D12Backend::AllocateSRVSlot, heap exhausted"); + return -1; + } + const auto slot = _freeSrvSlots.back(); + _freeSrvSlots.pop_back(); + return static_cast(slot); + } + + void D3D12Backend::FreeSRVSlot(int slot) { + if (slot < 0) { + return; + } + _freeSrvSlots.push_back(static_cast(slot)); + } + + D3D12_CPU_DESCRIPTOR_HANDLE D3D12Backend::GetSRVSlotCPUHandle(int slot) const { + if (!_srvHeap) { + return {}; + } + auto handle = _srvHeap->GetCPUDescriptorHandleForHeapStart(); + handle.ptr += static_cast(slot) * _srvDescriptorSize; + return handle; + } + + D3D12_GPU_DESCRIPTOR_HANDLE D3D12Backend::GetSRVSlotGPUHandle(int slot) const { + if (!_srvHeap) { + return {}; + } + auto handle = _srvHeap->GetGPUDescriptorHandleForHeapStart(); + handle.ptr += static_cast(slot) * _srvDescriptorSize; + return handle; + } + + bool D3D12Backend::WaitForGpu() { + if (!_commandQueue || !_device) { + return true; // nothing to drain + } + + // During process teardown the game has already destroyed its device and + // queue; touching them is UB and the fence would never signal anyway. + // Freeing without the drain is safe here - in-flight work died with + // the queue's threads + if (Framework::Utils::IsProcessShutdownInProgress()) { + return true; + } + + Microsoft::WRL::ComPtr fence; + if (FAILED(_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)))) { + Framework::Logging::GetLogger(FRAMEWORK_INNER_GRAPHICS)->error("D3D12Backend::WaitForGpu, CreateFence failed"); + return false; + } + + const HANDLE event = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!event) { + Framework::Logging::GetLogger(FRAMEWORK_INNER_GRAPHICS)->error("D3D12Backend::WaitForGpu, CreateEvent failed"); + return false; + } + + bool drained = false; + if (FAILED(_commandQueue->Signal(fence.Get(), 1))) { + // Generally means the device was removed; in-flight work is dead, + // but we can't prove it, so report failure and let callers decide + Framework::Logging::GetLogger(FRAMEWORK_INNER_GRAPHICS)->error("D3D12Backend::WaitForGpu, queue Signal failed (device removed?)"); + } + else if (fence->GetCompletedValue() >= 1) { + drained = true; + } + else if (SUCCEEDED(fence->SetEventOnCompletion(1, event))) { + // A live GPU signals in single-digit ms; the timeout only bites + // when the queue is wedged - which is precisely when freeing + // GPU-referenced resources is unsafe, hence the failure return + if (WaitForSingleObject(event, 500) == WAIT_OBJECT_0 || fence->GetCompletedValue() >= 1) { + drained = true; + } + else { + Framework::Logging::GetLogger(FRAMEWORK_INNER_GRAPHICS)->error("D3D12Backend::WaitForGpu, fence wait timed out"); + } + } + else { + Framework::Logging::GetLogger(FRAMEWORK_INNER_GRAPHICS)->error("D3D12Backend::WaitForGpu, SetEventOnCompletion failed"); + } + CloseHandle(event); + return drained; + } } // namespace Framework::Graphics diff --git a/code/framework/src/graphics/backend/d3d12.h b/code/framework/src/graphics/backend/d3d12.h index a885fd3e4..ba8906a07 100644 --- a/code/framework/src/graphics/backend/d3d12.h +++ b/code/framework/src/graphics/backend/d3d12.h @@ -15,6 +15,11 @@ namespace Framework::Graphics { class D3D12Backend: public Backend { + public: + // Extra shader-visible SRV slots reserved after the ImGui font slot(s), + // handed out to web views via AllocateSRVSlot + static constexpr UINT kExtraSrvSlots = 64; + private: IDXGISwapChain3 *_swapChain = nullptr; UINT _frameBufferCount = 0; @@ -23,15 +28,23 @@ namespace Framework::Graphics { ID3D12GraphicsCommandList *_commandList = nullptr; ID3D12CommandQueue *_commandQueue = nullptr; + UINT _srvDescriptorSize = 0; + std::vector _freeSrvSlots; + struct FrameContext { ID3D12CommandAllocator *_commandAllocator = nullptr; - ID3D12Resource *_mainRenderTargetResource = nullptr; D3D12_CPU_DESCRIPTOR_HANDLE _mainRenderTargetDescriptor; }; std::vector _frameContext; D3D12_RESOURCE_BARRIER _barrier {}; + // The current frame's back buffer, acquired fresh in Begin() and released + // in End(). We deliberately never hold a swapchain reference across + // frames so the game can resize/recreate the swapchain (which happens + // between presents) without our reference blocking DXGI's ResizeBuffers. + ID3D12Resource *_currentBackBuffer = nullptr; + public: bool Init(const Framework::Graphics::RendererConfiguration &opts) override; void Shutdown() override; @@ -40,6 +53,16 @@ namespace Framework::Graphics { void End(); int NumFramesInFlight() const; + // Since we no longer cache the back buffers, a swapchain resize or even a + // full swapchain recreation needs nothing from us beyond pointing at the + // current swapchain — Begin() re-acquires the back buffer each frame. + IDXGISwapChain3 *GetSwapChain() const { + return _swapChain; + } + void SetSwapChain(IDXGISwapChain3 *swapChain) { + _swapChain = swapChain; + } + // TODO: Backend not implemented yet void BeginDrawing() {} void EndDrawing() {} @@ -66,5 +89,29 @@ namespace Framework::Graphics { ID3D12GraphicsCommandList *GetGraphicsCommandList() const { return _commandList; } + + UINT GetCurrentFrameIndex() const { + return _swapChain ? _swapChain->GetCurrentBackBufferIndex() : 0; + } + + // Allocates a shader-visible SRV slot from the backend heap (the one ImGui + // is initialized against, so handles are usable as ImTextureID). Returns -1 + // when exhausted. + int AllocateSRVSlot(); + void FreeSRVSlot(int slot); + D3D12_CPU_DESCRIPTOR_HANDLE GetSRVSlotCPUHandle(int slot) const; + D3D12_GPU_DESCRIPTOR_HANDLE GetSRVSlotGPUHandle(int slot) const; + + // Blocks until the GPU has drained the queue. Used before destroying + // resources that in-flight command lists may still reference. Returns + // false when the drain could not be confirmed (fence timeout/failure) + // so callers can refuse to free still-referenced resources; returns + // true during process teardown, where waiting is impossible and + // freeing is safe (the queue's threads are already gone). + [[nodiscard]] bool WaitForGpu(); + + size_t GetFreeSRVSlotCount() const { + return _freeSrvSlots.size(); + } }; } // namespace Framework::Graphics diff --git a/code/framework/src/graphics/renderer.cpp b/code/framework/src/graphics/renderer.cpp index 8228ddeef..adbd60f7e 100644 --- a/code/framework/src/graphics/renderer.cpp +++ b/code/framework/src/graphics/renderer.cpp @@ -21,7 +21,8 @@ namespace Framework::Graphics { return RendererError::RENDERER_ALREADY_INITIALIZED; } - _config = config; + _config = config; + _backend = config.backend; if (_config.backend == RendererBackend::BACKEND_D3D_11) { _d3d11Backend = std::make_unique(); diff --git a/code/framework/src/gui/backend/view_d3d11.cpp b/code/framework/src/gui/backend/view_d3d11.cpp index 3d9e793df..2624f8360 100644 --- a/code/framework/src/gui/backend/view_d3d11.cpp +++ b/code/framework/src/gui/backend/view_d3d11.cpp @@ -175,25 +175,31 @@ namespace Framework::GUI { else { // CPU path: use pixel data from CEF's OnPaint auto *renderHandler = GetRenderHandler(); - if (renderHandler && !renderHandler->GetPixelData().empty()) { - Graphics::Bitmap bmp; - bmp.format = Graphics::BitmapFormat::BGRA8; - bmp.width = _width; - bmp.height = _height; - bmp.pitch = _width * 4; - bmp.size = static_cast(renderHandler->GetPixelData().size()); - bmp.pixels = const_cast(renderHandler->GetPixelData().data()); - - if (_cpuTextureID == 0) { - _cpuTextureID = backend->NextTextureId(); - backend->CreateTexture(_cpuTextureID, bmp); - } - else if (renderHandler->IsPixelDataDirty()) { - backend->UpdateTexture(_cpuTextureID, bmp); - renderHandler->ClearPixelDataDirty(); - } + if (renderHandler) { + // Held for the whole read — OnPaint reallocates the buffer on + // resize + const auto pixelLock = renderHandler->LockPixels(); + + if (!renderHandler->GetPixelData().empty()) { + Graphics::Bitmap bmp; + bmp.format = Graphics::BitmapFormat::BGRA8; + bmp.width = _width; + bmp.height = _height; + bmp.pitch = _width * 4; + bmp.size = static_cast(renderHandler->GetPixelData().size()); + bmp.pixels = const_cast(renderHandler->GetPixelData().data()); + + if (_cpuTextureID == 0) { + _cpuTextureID = backend->NextTextureId(); + backend->CreateTexture(_cpuTextureID, bmp); + } + else if (renderHandler->IsPixelDataDirty()) { + backend->UpdateTexture(_cpuTextureID, bmp); + renderHandler->ClearPixelDataDirty(); + } - gpuState.texture_1_id = _cpuTextureID; + gpuState.texture_1_id = _cpuTextureID; + } } } diff --git a/code/framework/src/gui/backend/view_d3d12.cpp b/code/framework/src/gui/backend/view_d3d12.cpp new file mode 100644 index 000000000..e0b7c0d48 --- /dev/null +++ b/code/framework/src/gui/backend/view_d3d12.cpp @@ -0,0 +1,277 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2024, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#include "view_d3d12.h" +#include "logging/logger.h" + +#include "graphics/backend/d3d12.h" + +#include +#include + +namespace Framework::GUI { + ViewD3D12::ViewD3D12(int id, Graphics::Renderer *graphicsRenderer, Manager *manager): View(id, graphicsRenderer, manager) {} + + ViewD3D12::~ViewD3D12() { + ReleaseResources(); + } + + GUIError ViewD3D12::Init(const std::string &url, int width, int height, int offsetX, int offsetY, bool gpuAccelerated) { + if (gpuAccelerated) { + // CEF shares D3D11 textures on the accelerated path; consuming them + // here would require D3D11-on-12 interop, so fall back to OnPaint + Framework::Logging::GetLogger("Web")->warn("ViewD3D12: GPU-accelerated OSR not supported, falling back to CPU path"); + } + return View::Init(url, width, height, offsetX, offsetY, false); + } + + void ViewD3D12::Update() { + if (!_browser || !_shouldDisplay) { + return; + } + + std::scoped_lock lock(_renderMutex); + View::Update(); + } + + bool ViewD3D12::CreateResources() { + auto *backend = _graphicsRenderer ? _graphicsRenderer->GetD3D12Backend() : nullptr; + auto *device = backend ? backend->GetDevice() : nullptr; + if (!device) { + return false; + } + + // Keep the SRV slot across resizes; the descriptor is rewritten in place + if (_srvSlot < 0) { + _srvSlot = backend->AllocateSRVSlot(); + if (_srvSlot < 0) { + return false; + } + } + + // Resize path: the previous texture/upload buffers (and the SRV being + // rewritten below) may still be referenced by the in-flight command + // list — the render thread submits one frame ahead. Drain first; if + // the drain can't be confirmed, keep the old resources and retry on a + // later frame rather than freeing something the GPU still reads. + if (_texture && !backend->WaitForGpu()) { + Framework::Logging::GetLogger("Web")->error("ViewD3D12: GPU drain failed, deferring resource recreation"); + return false; + } + + _texture.Reset(); + _uploadBuffers.clear(); + + D3D12_HEAP_PROPERTIES defaultHeap {}; + defaultHeap.Type = D3D12_HEAP_TYPE_DEFAULT; + + D3D12_RESOURCE_DESC texDesc {}; + texDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + texDesc.Width = static_cast(_width); + texDesc.Height = static_cast(_height); + texDesc.DepthOrArraySize = 1; + texDesc.MipLevels = 1; + texDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + texDesc.SampleDesc.Count = 1; + texDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + + if (FAILED(device->CreateCommittedResource(&defaultHeap, D3D12_HEAP_FLAG_NONE, &texDesc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&_texture)))) { + Framework::Logging::GetLogger("Web")->error("ViewD3D12: failed to create texture {}x{}", _width, _height); + return false; + } + _textureState = D3D12_RESOURCE_STATE_COPY_DEST; + + _uploadPitch = (static_cast(_width) * 4 + D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1) & ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1); + + D3D12_HEAP_PROPERTIES uploadHeap {}; + uploadHeap.Type = D3D12_HEAP_TYPE_UPLOAD; + + D3D12_RESOURCE_DESC bufDesc {}; + bufDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + bufDesc.Width = static_cast(_uploadPitch) * _height; + bufDesc.Height = 1; + bufDesc.DepthOrArraySize = 1; + bufDesc.MipLevels = 1; + bufDesc.SampleDesc.Count = 1; + bufDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + + // Clamped: a zero would leave _uploadBuffers empty and UploadPixels + // does a modulo by its size + const auto framesInFlight = static_cast(std::max(1, backend->NumFramesInFlight())); + _uploadBuffers.resize(framesInFlight); + + for (auto &upload : _uploadBuffers) { + if (FAILED(device->CreateCommittedResource(&uploadHeap, D3D12_HEAP_FLAG_NONE, &bufDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&upload.resource)))) { + Framework::Logging::GetLogger("Web")->error("ViewD3D12: failed to create upload buffer"); + ReleaseResources(); + return false; + } + + const D3D12_RANGE noRead {0, 0}; + if (FAILED(upload.resource->Map(0, &noRead, reinterpret_cast(&upload.mapped)))) { + Framework::Logging::GetLogger("Web")->error("ViewD3D12: failed to map upload buffer"); + ReleaseResources(); + return false; + } + } + + D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc {}; + srvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srvDesc.Texture2D.MipLevels = 1; + device->CreateShaderResourceView(_texture.Get(), &srvDesc, backend->GetSRVSlotCPUHandle(_srvSlot)); + _srvGpuHandle = backend->GetSRVSlotGPUHandle(_srvSlot); + + _texWidth = _width; + _texHeight = _height; + _textureReady = false; + + Framework::Logging::GetLogger("Web")->debug("ViewD3D12: resources created {}x{}, srv slot {}", _width, _height, _srvSlot); + return true; + } + + void ViewD3D12::ReleaseResources() { + // In-flight command lists may still reference the texture/upload buffers + // (the render thread submits one frame ahead) — drain the GPU first. + // This is the destructor path, so an unconfirmed drain can only be + // logged: the resources are freed regardless + if (_texture) { + if (auto *backend = _graphicsRenderer ? _graphicsRenderer->GetD3D12Backend() : nullptr) { + if (!backend->WaitForGpu()) { + Framework::Logging::GetLogger("Web")->warn("ViewD3D12: GPU drain unconfirmed, freeing resources anyway (teardown)"); + } + } + } + + _uploadBuffers.clear(); + _texture.Reset(); + _textureReady = false; + + if (_srvSlot >= 0) { + if (auto *backend = _graphicsRenderer ? _graphicsRenderer->GetD3D12Backend() : nullptr) { + backend->FreeSRVSlot(_srvSlot); + } + _srvSlot = -1; + } + } + + void ViewD3D12::UploadPixels(const std::vector &pixels) { + auto *backend = _graphicsRenderer ? _graphicsRenderer->GetD3D12Backend() : nullptr; + auto *cmdList = backend ? backend->GetGraphicsCommandList() : nullptr; + if (!cmdList || !_texture || _uploadBuffers.empty()) { + return; + } + + const auto frameIdx = backend->GetCurrentFrameIndex() % _uploadBuffers.size(); + uint8_t *dst = _uploadBuffers[frameIdx].mapped; + + const uint32_t srcPitch = static_cast(_width) * 4; + for (int y = 0; y < _height; y++) { + memcpy(dst + static_cast(y) * _uploadPitch, pixels.data() + static_cast(y) * srcPitch, srcPitch); + } + + D3D12_RESOURCE_BARRIER barrier {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Transition.pResource = _texture.Get(); + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + + if (_textureState != D3D12_RESOURCE_STATE_COPY_DEST) { + barrier.Transition.StateBefore = _textureState; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST; + cmdList->ResourceBarrier(1, &barrier); + } + + D3D12_TEXTURE_COPY_LOCATION dstLoc {}; + dstLoc.pResource = _texture.Get(); + dstLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dstLoc.SubresourceIndex = 0; + + D3D12_TEXTURE_COPY_LOCATION srcLoc {}; + srcLoc.pResource = _uploadBuffers[frameIdx].resource.Get(); + srcLoc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + srcLoc.PlacedFootprint.Footprint.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + srcLoc.PlacedFootprint.Footprint.Width = static_cast(_width); + srcLoc.PlacedFootprint.Footprint.Height = static_cast(_height); + srcLoc.PlacedFootprint.Footprint.Depth = 1; + srcLoc.PlacedFootprint.Footprint.RowPitch = _uploadPitch; + + cmdList->CopyTextureRegion(&dstLoc, 0, 0, 0, &srcLoc, nullptr); + + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + cmdList->ResourceBarrier(1, &barrier); + _textureState = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + + _textureReady = true; + } + + void ViewD3D12::Render() { + if (!_browser || !_shouldDisplay) { + return; + } + + std::scoped_lock lock(_renderMutex); + + auto *backend = _graphicsRenderer ? _graphicsRenderer->GetD3D12Backend() : nullptr; + if (!backend) { + return; + } + + auto *renderHandler = GetRenderHandler(); + if (!renderHandler) { + return; + } + + // Held for the whole read: OnPaint (game thread, inside the CEF pump) + // reallocates the buffer on resize + const auto pixelLock = renderHandler->LockPixels(); + + const auto &pixels = renderHandler->GetPixelData(); + if (pixels.empty()) { + return; + } + + if (!_texture || _texWidth != _width || _texHeight != _height) { + if (!CreateResources()) { + return; + } + } + + if (renderHandler->IsPixelDataDirty() || !_textureReady) { + // The pixel buffer can briefly lag the view size during a resize + if (pixels.size() >= static_cast(_width) * _height * 4) { + UploadPixels(pixels); + renderHandler->ClearPixelDataDirty(); + } + } + } + + void ViewD3D12::SubmitImGuiDraw() { + std::scoped_lock lock(_renderMutex); + + if (!_shouldDisplay || !_textureReady || _srvSlot < 0) { + return; + } + + auto *drawList = ImGui::GetBackgroundDrawList(); + drawList->AddImage(static_cast(_srvGpuHandle.ptr), ImVec2(static_cast(_x), static_cast(_y)), + ImVec2(static_cast(_x + _width), static_cast(_y + _height))); + } + + std::string ViewD3D12::GetDebugString() const { + auto *renderHandler = _renderHandler.get(); + size_t pixelBytes = 0; + if (renderHandler) { + const auto pixelLock = renderHandler->LockPixels(); + pixelBytes = renderHandler->GetPixelData().size(); + } + return fmt::format("d3d12 display={} texReady={} srv={} tex={}x{} pixels={} dirty={}", _shouldDisplay, _textureReady, _srvSlot, _texWidth, _texHeight, pixelBytes, + renderHandler ? renderHandler->IsPixelDataDirty() : false); + } +} // namespace Framework::GUI diff --git a/code/framework/src/gui/backend/view_d3d12.h b/code/framework/src/gui/backend/view_d3d12.h new file mode 100644 index 000000000..a570da822 --- /dev/null +++ b/code/framework/src/gui/backend/view_d3d12.h @@ -0,0 +1,66 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2024, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include "graphics/renderer.h" +#include "gui/view.h" + +namespace Framework::GUI { + // CPU-path D3D12 view: uploads CEF's OnPaint pixel buffer into a texture on + // the backend command list (render thread, between D3D12Backend::Begin and + // the ImGui draw) and blits it via the ImGui background draw list (game + // thread, inside the ImGui frame). The GPU-accelerated CEF path is not + // supported here — CEF shares D3D11 textures, which would require interop. + class ViewD3D12 final: public View { + private: + Microsoft::WRL::ComPtr _texture; + + // One upload buffer per frame in flight so we never write a buffer the + // GPU may still be copying from + struct UploadBuffer { + Microsoft::WRL::ComPtr resource; + uint8_t *mapped = nullptr; + }; + std::vector _uploadBuffers; + + int _texWidth = 0; + int _texHeight = 0; + uint32_t _uploadPitch = 0; + D3D12_RESOURCE_STATES _textureState = D3D12_RESOURCE_STATE_COPY_DEST; + + int _srvSlot = -1; + D3D12_GPU_DESCRIPTOR_HANDLE _srvGpuHandle {}; + + // Set once the texture holds at least one full frame of pixels + bool _textureReady = false; + + public: + ViewD3D12(int id, Graphics::Renderer *graphicsRenderer, Manager *manager); + ~ViewD3D12() override; + + [[nodiscard]] GUIError Init(const std::string &url, int width, int height, int offsetX, int offsetY, bool gpuAccelerated = false) override; + + void Update() override; + void Render() override; + void SubmitImGuiDraw() override; + std::string GetDebugString() const override; + + private: + bool CreateResources(); + void ReleaseResources(); + void UploadPixels(const std::vector &pixels); + }; +} // namespace Framework::GUI diff --git a/code/framework/src/gui/cef/app.cpp b/code/framework/src/gui/cef/app.cpp index fd943f494..37be564b2 100644 --- a/code/framework/src/gui/cef/app.cpp +++ b/code/framework/src/gui/cef/app.cpp @@ -22,11 +22,18 @@ namespace Framework::GUI::CEF { std::string scheme = CefString(&urlParts.scheme).ToString(); std::string domain = CefString(&urlParts.host).ToString(); - auto it = _handlers.find({scheme, domain}); - if (it == _handlers.end()) - return nullptr; + SchemaHandlerFactoryCallback handler; + { + std::scoped_lock lock(_handlersMutex); + auto it = _handlers.find({scheme, domain}); + if (it == _handlers.end()) + return nullptr; + handler = it->second; + } - return it->second(browser, frame, scheme_name, request); + // Invoked on a copy outside the lock: the callback may be arbitrarily + // slow and a re-registration must neither wait on it nor race it + return handler(browser, frame, scheme_name, request); } void App::OnBeforeCommandLineProcessing(const CefString &processType, CefRefPtr commandLine) { @@ -53,6 +60,7 @@ namespace Framework::GUI::CEF { } void App::RegisterSchemeHandlerFactory(const std::string &scheme, const std::string &domain, SchemaHandlerFactoryCallback callback) { - _handlers[{scheme, domain}] = callback; + std::scoped_lock lock(_handlersMutex); + _handlers[{scheme, domain}] = std::move(callback); } } // namespace Framework::GUI::CEF diff --git a/code/framework/src/gui/cef/app.h b/code/framework/src/gui/cef/app.h index d19c6d87a..095df95e1 100644 --- a/code/framework/src/gui/cef/app.h +++ b/code/framework/src/gui/cef/app.h @@ -14,6 +14,7 @@ #include "include/cef_browser_process_handler.h" #include +#include #include namespace Framework::GUI::CEF { @@ -68,6 +69,9 @@ namespace Framework::GUI::CEF { IMPLEMENT_REFCOUNTING(App); private: + // Create() runs on the CEF IO thread per request; registration happens + // on the game thread + std::mutex _handlersMutex; std::unordered_map _handlers; }; } // namespace Framework::GUI::CEF diff --git a/code/framework/src/gui/cef/disk_resource_handler.cpp b/code/framework/src/gui/cef/disk_resource_handler.cpp new file mode 100644 index 000000000..1a85642e6 --- /dev/null +++ b/code/framework/src/gui/cef/disk_resource_handler.cpp @@ -0,0 +1,67 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2024, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#include "disk_resource_handler.h" + +#include "include/cef_parser.h" +#include "include/cef_stream.h" +#include "include/wrapper/cef_stream_resource_handler.h" + +#include + +#include + +namespace Framework::GUI::CEF { + namespace { + CefRefPtr NotFound() { + static const char kBody[] = "Not Found"; + // CreateForData does not copy — the static literal outlives the handler + return new CefStreamResourceHandler(404, "Not Found", "text/plain", {}, CefStreamReader::CreateForData(const_cast(kBody), sizeof(kBody) - 1)); + } + } // namespace + + CefRefPtr CreateDiskResourceHandler(const std::filesystem::path &root, const CefRefPtr &request) { + CefURLParts urlParts; + if (!request || !CefParseURL(request->GetURL(), urlParts)) { + return NotFound(); + } + + const std::string urlPath = CefURIDecode(CefString(&urlParts.path), false, UU_SPACES).ToString(); + + // Empty = rejected: the path escaped the root (e.g. via "..") + const auto file = Framework::Utils::ResolvePathUnderRoot(root, urlPath); + if (file.empty()) { + return NotFound(); + } + + // wstring(): path::string() narrows to the ANSI codepage on Windows but + // CefString decodes std::string as UTF-8 — non-ASCII paths would 404 + CefRefPtr stream = CefStreamReader::CreateForFile(file.wstring()); + if (!stream) { + return NotFound(); + } + + std::string ext = file.extension().string(); + if (!ext.empty() && ext.front() == '.') { + ext.erase(0, 1); + } + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + std::string mime = CefGetMimeType(ext).ToString(); + if (mime.empty()) { + mime = "application/octet-stream"; + } + + // Local files change while iterating on UIs — never serve a stale copy + CefResponse::HeaderMap headers; + headers.emplace("Cache-Control", "no-store"); + + return new CefStreamResourceHandler(200, "OK", mime, headers, stream); + } +} // namespace Framework::GUI::CEF diff --git a/code/framework/src/gui/cef/disk_resource_handler.h b/code/framework/src/gui/cef/disk_resource_handler.h new file mode 100644 index 000000000..bb69eedc2 --- /dev/null +++ b/code/framework/src/gui/cef/disk_resource_handler.h @@ -0,0 +1,23 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2024, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "include/cef_request.h" +#include "include/cef_resource_handler.h" + +#include + +namespace Framework::GUI::CEF { + // Resolves the request URL against a directory tree ("/" maps to + // index.html) and returns a streaming handler for the file, for use with + // RegisterSchemeHandlerFactory. Anything missing or escaping the root + // yields a 404 handler (never nullptr — that would fall through to the + // network stack for standard schemes). + CefRefPtr CreateDiskResourceHandler(const std::filesystem::path &root, const CefRefPtr &request); +} // namespace Framework::GUI::CEF diff --git a/code/framework/src/gui/cef/life_span_handler.cpp b/code/framework/src/gui/cef/life_span_handler.cpp index 0553c32ff..2c8696e69 100644 --- a/code/framework/src/gui/cef/life_span_handler.cpp +++ b/code/framework/src/gui/cef/life_span_handler.cpp @@ -10,8 +10,15 @@ #include "include/cef_parser.h" namespace Framework::GUI::CEF { + std::atomic LifeSpanHandler::s_liveBrowserCount {0}; + void LifeSpanHandler::OnAfterCreated(CefRefPtr browser) { _browser = browser; + ++s_liveBrowserCount; + + if (_onAfterCreated) { + _onAfterCreated(browser); + } } bool LifeSpanHandler::OnBeforePopup(CefRefPtr browser, CefRefPtr frame, int popupId, const CefString &targetUrl, const CefString &targetFrameName, CefLifeSpanHandler::WindowOpenDisposition targetDisposition, bool userGesture, const CefPopupFeatures &popupFeatures, CefWindowInfo &windowInfo, CefRefPtr &client, CefBrowserSettings &settings, CefRefPtr &extraInfo, bool *noJavascriptAccess) { @@ -21,6 +28,7 @@ namespace Framework::GUI::CEF { void LifeSpanHandler::OnBeforeClose(CefRefPtr browser) { _browser = nullptr; + --s_liveBrowserCount; } bool LifeSpanHandler::OnBeforeBrowse(CefRefPtr browser, CefRefPtr frame, CefRefPtr request, bool userGesture, bool isRedirect) { diff --git a/code/framework/src/gui/cef/life_span_handler.h b/code/framework/src/gui/cef/life_span_handler.h index f7745e2cd..6b1689f88 100644 --- a/code/framework/src/gui/cef/life_span_handler.h +++ b/code/framework/src/gui/cef/life_span_handler.h @@ -10,6 +10,7 @@ #include "include/cef_life_span_handler.h" #include "include/cef_request_handler.h" +#include #include namespace Framework::GUI::CEF { @@ -24,7 +25,16 @@ namespace Framework::GUI::CEF { std::function)> _onAfterCreated; OnBeforeBrowseCallback _onBeforeBrowse; + // Browsers created but not yet through OnBeforeClose, across all views. + // Manager::Shutdown pumps the message loop until this drains before + // calling CefShutdown + static std::atomic s_liveBrowserCount; + public: + static int GetLiveBrowserCount() { + return s_liveBrowserCount.load(); + } + void SetOnAfterCreatedCallback(std::function)> cb) { _onAfterCreated = std::move(cb); } diff --git a/code/framework/src/gui/cef/render_handler.cpp b/code/framework/src/gui/cef/render_handler.cpp index 005582a7c..f2d781795 100644 --- a/code/framework/src/gui/cef/render_handler.cpp +++ b/code/framework/src/gui/cef/render_handler.cpp @@ -43,6 +43,10 @@ namespace Framework::GUI::CEF { return; } + // Views read _pixelData from their render path; the resize below can + // reallocate the buffer out from under them without this + std::lock_guard lock(_pixelMutex); + const size_t size = width * height * 4; if (_pixelData.size() != size) { _pixelData.resize(size); diff --git a/code/framework/src/gui/cef/render_handler.h b/code/framework/src/gui/cef/render_handler.h index 21a6b4dd3..7135411ee 100644 --- a/code/framework/src/gui/cef/render_handler.h +++ b/code/framework/src/gui/cef/render_handler.h @@ -27,7 +27,10 @@ namespace Framework::GUI::CEF { Microsoft::WRL::ComPtr _sharedTexture; HANDLE _sharedHandle = nullptr; - // CPU fallback + // CPU fallback. OnPaint writes from the thread pumping CEF (the game + // thread); views read from the render thread — hold LockPixels() for + // the whole read, GetPixelData() returns a reference + std::mutex _pixelMutex; std::vector _pixelData; bool _pixelDataDirty = false; @@ -51,6 +54,10 @@ namespace Framework::GUI::CEF { return std::lock_guard(_textureMutex); } + [[nodiscard]] std::lock_guard LockPixels() { + return std::lock_guard(_pixelMutex); + } + ID3D11Texture2D *GetSharedTexture() const { return _sharedTexture.Get(); } diff --git a/code/framework/src/gui/manager.cpp b/code/framework/src/gui/manager.cpp index 36d02292c..94ba35a31 100644 --- a/code/framework/src/gui/manager.cpp +++ b/code/framework/src/gui/manager.cpp @@ -9,12 +9,34 @@ #include "manager.h" #include +#include #include "gui/backend/view_d3d11.h" +#include "gui/backend/view_d3d12.h" +#include "include/cef_scheme.h" + +#include #include namespace Framework::GUI { + namespace { + // Unbuffered shutdown trace: the spdlog file sink is unreliable + // (per-logger sink duplication overwrites the file) and shutdown is + // exactly where the in-game console can no longer be read. Open/append/ + // close per line so every entry survives even a hard process kill. + void ShutdownTrace(const std::string &rootDir, const std::string &msg) { + if (rootDir.empty()) { + return; + } + FILE *f = nullptr; + if (fopen_s(&f, (rootDir + "/logs/web_shutdown_trace.log").c_str(), "a") != 0 || !f) { + return; + } + fprintf(f, "[%llu ms] %s\n", static_cast(GetTickCount64()), msg.c_str()); + fclose(f); + } + } // namespace Manager::Manager() { _clipboard = std::make_unique(); } @@ -26,22 +48,63 @@ namespace Framework::GUI { } void Manager::Shutdown() { - for (auto &view : _views) { - view.reset(); + { + std::scoped_lock lock(_renderMutex); + ShutdownTrace(_rootDir, fmt::format("Shutdown begin (views={}, dying={}, processTeardown={})", _views.size(), _dyingViews.size(), Framework::Utils::IsProcessShutdownInProgress())); + for (auto &view : _views) { + view.reset(); + } + _views.clear(); + _dyingViews.clear(); } - _views.clear(); + ShutdownTrace(_rootDir, "views released"); if (_cefInitialized) { - CefShutdown(); + if (Framework::Utils::IsProcessShutdownInProgress()) { + // Too late for a clean CefShutdown: its threads are gone and it + // would deadlock. Let the OS reap; subprocesses watch the parent. + Framework::Logging::GetLogger("Web")->debug("Process teardown in progress, skipping CefShutdown"); + ShutdownTrace(_rootDir, "process teardown in progress, skipping CefShutdown"); + } + else { + // The global request context holds references to registered + // scheme handler factories (our CefApp); outstanding references + // at CefShutdown time can stall it + CefClearSchemeHandlerFactories(); + ShutdownTrace(_rootDir, "scheme handler factories cleared"); + + // CloseBrowser is async — pump until every browser has been + // through OnBeforeClose (hard cap so a wedged renderer can't + // hold game exit hostage), then a settle pass for the teardown + // tasks queued behind the close. The count can't miss a + // browser: views create via CreateBrowserSync, so + // OnAfterCreated has fired before a view can exist + int drainedMs = 0; + while (CEF::LifeSpanHandler::GetLiveBrowserCount() > 0 && drainedMs < 3000) { + CefDoMessageLoopWork(); + Sleep(10); + drainedMs += 10; + } + for (int i = 0; i < 50; i++) { + CefDoMessageLoopWork(); + Sleep(10); + } + ShutdownTrace(_rootDir, fmt::format("message loop drained (close wait {} ms, {} browsers left)", drainedMs, CEF::LifeSpanHandler::GetLiveBrowserCount())); + + CefShutdown(); + ShutdownTrace(_rootDir, "CefShutdown complete"); + } _cefInitialized = false; } Lifecycle::Shutdown(); + ShutdownTrace(_rootDir, "Shutdown end"); } GUIError Manager::Init(const std::string &rootDir, ViewportConfiguration initialViewport, Graphics::Renderer *renderer, bool gpuAccelerated) { _graphicsRenderer = renderer; _gpuAccelerated = gpuAccelerated; + _rootDir = rootDir; SetViewportConfiguration(initialViewport); @@ -62,9 +125,20 @@ namespace Framework::GUI { CefString(&settings.cache_path) = cacheRoot.wstring(); CefString(&settings.log_file) = rootDir + "/logs/cef.log"; - // CEF requires an absolute path for the subprocess executable + // CEF requires an absolute path for the subprocess executable. Resolve it + // relative to the module containing this code rather than the process exe: + // when the framework lives in a DLL injected into a game, the game's + // install dir doesn't (and shouldn't) carry our CEF binaries. + static const int s_moduleAnchor = 0; + HMODULE selfModule = nullptr; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(&s_moduleAnchor), &selfModule) || !selfModule) { + // Without it GetModuleFileNameW(nullptr) would silently point the + // subprocess path at the game EXE's dir, which doesn't carry CEF + Framework::Logging::GetLogger("Web")->error("Failed to resolve owning module for the CEF subprocess path"); + return GUIError::GUI_CEF_INIT_FAILED; + } wchar_t exePath[MAX_PATH] = {}; - GetModuleFileNameW(nullptr, exePath, MAX_PATH); + GetModuleFileNameW(selfModule, exePath, MAX_PATH); std::filesystem::path subprocessPath = std::filesystem::path(exePath).parent_path() / "cef_subprocess.exe"; CefString(&settings.browser_subprocess_path) = subprocessPath.wstring(); @@ -89,46 +163,78 @@ namespace Framework::GUI { return; } - std::scoped_lock lock(_renderMutex); - - // Pump the CEF message loop + // Pump OUTSIDE _renderMutex. The pump dispatches the game window's + // messages mid-tick; a handler that blocks on the render thread (the + // exit flow does an RHI flush) deadlocks against Manager::Render, + // which takes this mutex on the render thread every frame. Seen as a + // minutes-long exit stall crawling forward on the game's wait + // timeouts. The pump itself never touches _views. CefDoMessageLoopWork(); + std::scoped_lock lock(_renderMutex); + // Update the views for (auto &view : _views) { view->Update(); } - } - void Manager::Render() { - if (!_cefInitialized) { - return; + // Destroy retired views once any in-flight frames referencing their + // textures have long drained + constexpr int kDyingViewTicks = 8; + for (auto it = _dyingViews.begin(); it != _dyingViews.end();) { + if (++it->second > kDyingViewTicks) { + it = _dyingViews.erase(it); + } + else { + ++it; + } } + } - // Sort views by z-index + std::vector Manager::GetViewsByZIndex() const { std::vector views; - for (auto &view : _views) { + for (const auto &view : _views) { views.push_back(view.get()); } std::sort(views.begin(), views.end(), [](GUI::View *a, GUI::View *b) { return a->GetZIndex() < b->GetZIndex(); }); + return views; + } + + void Manager::SubmitImGuiDraws() { + if (!_cefInitialized) { + return; + } + + std::scoped_lock lock(_renderMutex); + + for (auto *view : GetViewsByZIndex()) { + view->SubmitImGuiDraw(); + } + } + + void Manager::Render() { + if (!_cefInitialized) { + return; + } std::scoped_lock lock(_renderMutex); - // Render the views - for (auto &view : views) { + for (auto *view : GetViewsByZIndex()) { view->Render(); } } void Manager::ProcessMouseEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) const { + std::scoped_lock lock(_renderMutex); for (auto &view : _views) { view->ProcessMouseEvent(hWnd, msg, wParam, lParam); } } void Manager::ProcessKeyboardEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) const { + std::scoped_lock lock(_renderMutex); for (auto &view : _views) { view->ProcessKeyboardEvent(hWnd, msg, wParam, lParam); } @@ -140,6 +246,10 @@ namespace Framework::GUI { return -1; } + // A 0x0 request means "fill the viewport" — such views follow the + // viewport across resizes (see Manager::Resize) + const bool autoResize = (width == 0 && height == 0); + if (width == 0) { width = _viewportConfiguration.width; } @@ -154,6 +264,9 @@ namespace Framework::GUI { case Graphics::RendererBackend::BACKEND_D3D_11: view = std::make_unique(++_id, _graphicsRenderer, this); break; + case Graphics::RendererBackend::BACKEND_D3D_12: + view = std::make_unique(++_id, _graphicsRenderer, this); + break; default: Framework::Logging::GetLogger("Web")->error("Failed to create view: Unsupported renderer backend"); return -1; @@ -168,43 +281,70 @@ namespace Framework::GUI { return -1; } - _views.push_back(std::move(view)); + view->SetAutoResize(autoResize); + + { + std::scoped_lock lock(_renderMutex); + _views.push_back(std::move(view)); + } Framework::Logging::GetLogger("Web")->debug("Created view with id {}", _id); return _id; } + void Manager::Resize(int width, int height) { + if (width <= 0 || height <= 0) { + return; + } + + std::scoped_lock lock(_renderMutex); + _viewportConfiguration.width = width; + _viewportConfiguration.height = height; + + for (auto &view : _views) { + if (view && view->IsAutoResize()) { + view->Resize(width, height); + } + } + } + + void Manager::RetireView(std::unique_ptr view) { + // Caller must hold _renderMutex. Hide the view so no further draws are + // submitted, then let it age out (destroyed in Update once in-flight + // frames that may reference its texture have drained). + view->Display(false); + view->Focus(false); + _dyingViews.emplace_back(std::move(view), 0); + } + bool Manager::DestroyView(int id) { if (!_cefInitialized) { Framework::Logging::GetLogger("Web")->error("Failed to destroy view: CEF is not initialized"); return false; } - int index = -1; - int i = 0; + std::scoped_lock lock(_renderMutex); - for (auto it = _views.begin(); it != _views.end(); ++it, ++i) { + for (auto it = _views.begin(); it != _views.end(); ++it) { if ((*it)->GetId() == id) { - index = i; - break; - } - } + RetireView(std::move(*it)); + _views.erase(it); - if (index == -1) { - Framework::Logging::GetLogger("Web")->error("Failed to destroy view: View does not exist"); - return false; + Framework::Logging::GetLogger("Web")->debug("Destroyed view with id {}", id); + return true; + } } - _views[index].reset(); - _views.erase(_views.begin() + index); - - Framework::Logging::GetLogger("Web")->debug("Destroyed view with id {}", id); - return true; + Framework::Logging::GetLogger("Web")->error("Failed to destroy view: View does not exist"); + return false; } void Manager::CleanupViews() { + std::scoped_lock lock(_renderMutex); + for (auto it = _views.begin(); it != _views.end();) { if ((*it)->IsGarbageCollected()) { + RetireView(std::move(*it)); it = _views.erase(it); } else { @@ -214,6 +354,7 @@ namespace Framework::GUI { } bool Manager::IsAnyViewFocused() const { + std::scoped_lock lock(_renderMutex); for (const auto &view : _views) { if (view->HasFocus()) { return true; @@ -223,6 +364,7 @@ namespace Framework::GUI { } bool Manager::IsAnyGCViewFocused() const { + std::scoped_lock lock(_renderMutex); for (const auto &view : _views) { if (view->HasFocus() && view->IsGarbageCollected()) { return true; @@ -232,6 +374,7 @@ namespace Framework::GUI { } std::vector Manager::GetAllViews() const { + std::scoped_lock lock(_renderMutex); std::vector views; for (const auto &view : _views) { views.push_back(view.get()); @@ -240,6 +383,7 @@ namespace Framework::GUI { } std::vector Manager::GetGCViews() const { + std::scoped_lock lock(_renderMutex); std::vector views; for (const auto &view : _views) { if (view->IsGarbageCollected()) { @@ -250,6 +394,7 @@ namespace Framework::GUI { } View *Manager::GetView(int id) const { + std::scoped_lock lock(_renderMutex); for (auto it = _views.begin(); it != _views.end(); ++it) { if ((*it)->GetId() == id) { return it->get(); diff --git a/code/framework/src/gui/manager.h b/code/framework/src/gui/manager.h index 46e97ebb5..f7df660d9 100644 --- a/code/framework/src/gui/manager.h +++ b/code/framework/src/gui/manager.h @@ -36,14 +36,27 @@ namespace Framework::GUI { CefRefPtr _cefApp; bool _cefInitialized = false; - std::recursive_mutex _renderMutex; + // Guards _views/_dyingViews across the game thread, the render thread + // and reentrant message dispatch; mutable so const readers can lock + mutable std::recursive_mutex _renderMutex; std::vector> _views; + + // Destroyed views are parked here for a few ticks before their GPU + // resources are actually freed: draw data built on the game thread can + // reference a view's texture for a couple of frames after removal + std::vector, int>> _dyingViews; + std::unique_ptr _clipboard; + std::string _rootDir; Graphics::Renderer *_graphicsRenderer {}; bool _gpuAccelerated = false; int _id = 0; + // Callers must hold _renderMutex + std::vector GetViewsByZIndex() const; + void RetireView(std::unique_ptr view); + public: Manager(); ~Manager(); @@ -61,9 +74,21 @@ namespace Framework::GUI { std::vector GetAllViews() const; std::vector GetGCViews() const; + size_t GetDyingViewCount() const { + return _dyingViews.size(); + } + void Update() override; void Render(); + // Match the viewport (and every fullscreen view) to a new client size, + // e.g. after a swapchain resize / fullscreen toggle. + void Resize(int width, int height); + + // Game-thread companion to Render(): must be called inside an active + // ImGui frame so D3D12 views can blit through the background draw list + void SubmitImGuiDraws(); + void ProcessMouseEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) const; void ProcessKeyboardEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) const; diff --git a/code/framework/src/gui/view.cpp b/code/framework/src/gui/view.cpp index 9e9d2cbae..ac2821467 100644 --- a/code/framework/src/gui/view.cpp +++ b/code/framework/src/gui/view.cpp @@ -109,6 +109,27 @@ namespace Framework::GUI { // Nothing to update at the base level for CEF; message loop is driven by Manager } + void View::Resize(int width, int height) { + if (width <= 0 || height <= 0) { + return; + } + + // Synchronize with the render thread (ViewD3D12::Render reads _width to + // decide whether to recreate its texture, also under _renderMutex) + std::scoped_lock lock(_renderMutex); + _width = width; + _height = height; + if (_renderHandler) { + _renderHandler->SetDimensions(width, height); + } + if (_browser) { + // Make CEF re-query GetViewRect and repaint at the new size; the + // next OnPaint delivers a full-size buffer and the backend recreates + // its texture (ViewD3D12 detects _texWidth/_texHeight != _width) + _browser->GetHost()->WasResized(); + } + } + void View::ProcessMouseEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (!_browser || !_shouldDisplay || !_hasFocus) { return; diff --git a/code/framework/src/gui/view.h b/code/framework/src/gui/view.h index 07ada0c65..524b2cb8d 100644 --- a/code/framework/src/gui/view.h +++ b/code/framework/src/gui/view.h @@ -71,6 +71,10 @@ namespace Framework::GUI { bool _shouldDisplay = false; bool _garbageCollected = false; + // Fullscreen overlays (created at 0x0 = viewport size) track the + // viewport across swapchain resizes; explicitly-sized views don't. + bool _autoResize = false; + std::recursive_mutex _renderMutex; glm::vec2 _cursorPos {}; bool _isMouseDown = false; @@ -85,6 +89,15 @@ namespace Framework::GUI { virtual void Update(); virtual void Render() = 0; + // Called on the game thread inside an active ImGui frame; backends that + // draw through ImGui (D3D12) submit their textured quad here + virtual void SubmitImGuiDraw() {} + + // One-line backend state dump for debug overlays + virtual std::string GetDebugString() const { + return ""; + } + void ProcessMouseEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); void ProcessKeyboardEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); @@ -116,6 +129,18 @@ namespace Framework::GUI { _y = y; } + // Resize the view (and the underlying CEF surface) to new dimensions. + // The backend recreates its GPU texture lazily on the next Render. + void Resize(int width, int height); + + void SetAutoResize(bool enable) { + _autoResize = enable; + } + + bool IsAutoResize() const { + return _autoResize; + } + glm::vec2 GetPosition() const { return {_x, _y}; } diff --git a/code/framework/src/utils/path.cpp b/code/framework/src/utils/path.cpp index f5a9eba6d..2348acde9 100644 --- a/code/framework/src/utils/path.cpp +++ b/code/framework/src/utils/path.cpp @@ -192,4 +192,31 @@ namespace Framework::Utils { return std::string(path.substr(pos)); // Include the dot in the extension } + + std::filesystem::path ResolvePathUnderRoot(const std::filesystem::path &root, std::string urlPath) { + if (urlPath.empty() || urlPath == "/") { + urlPath = "/index.html"; + } + if (urlPath.front() == '/') { + urlPath.erase(0, 1); + } + + std::error_code ec; + const auto file = std::filesystem::weakly_canonical(root / urlPath, ec); + if (ec) { + return {}; + } + const auto rootCanonical = std::filesystem::weakly_canonical(root, ec); + if (ec) { + return {}; + } + + // lexically_relative is empty when the paths have different roots + // (drive injection) and ".."-prefixed when the input escaped upward + const auto relative = file.lexically_relative(rootCanonical); + if (relative.empty() || *relative.begin() == "..") { + return {}; + } + return file; + } } // namespace Framework::Utils diff --git a/code/framework/src/utils/path.h b/code/framework/src/utils/path.h index 48fd52a73..544f3c361 100644 --- a/code/framework/src/utils/path.h +++ b/code/framework/src/utils/path.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include @@ -18,4 +19,10 @@ namespace Framework::Utils { std::string GetAppDataPathA(); std::wstring GetFileExtensionW(const std::wstring &path); std::string GetFileExtensionA(std::string_view path); + + // Resolves a URL-style path ("" and "/" map to "/index.html") against a + // root directory, for safely serving files from a directory tree. Returns + // an empty path when the input escapes the root (".." traversal, drive or + // absolute-path injection). Purely lexical — existence is not checked. + std::filesystem::path ResolvePathUnderRoot(const std::filesystem::path &root, std::string urlPath); } // namespace Framework::Utils diff --git a/code/framework/src/utils/process_shutdown.h b/code/framework/src/utils/process_shutdown.h new file mode 100644 index 000000000..126e01cc3 --- /dev/null +++ b/code/framework/src/utils/process_shutdown.h @@ -0,0 +1,27 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2024, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "safe_win32.h" + +namespace Framework::Utils { + // True once the OS has begun tearing the process down (CRT/DLL detach). + // Blocking work (thread joins, GPU fences, CefShutdown) must be skipped + // then: the threads it would wait on are already dead and the wait only + // stalls process exit. + inline bool IsProcessShutdownInProgress() { +#ifdef WIN32 + using RtlDllShutdownInProgress_t = BOOLEAN(NTAPI *)(); + static const auto fn = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlDllShutdownInProgress")); + return fn && fn(); +#else + return false; +#endif + } +} // namespace Framework::Utils diff --git a/code/tests/framework_ut.cpp b/code/tests/framework_ut.cpp index 8830aa7a7..fac0afd97 100644 --- a/code/tests/framework_ut.cpp +++ b/code/tests/framework_ut.cpp @@ -12,6 +12,7 @@ /* TEST CATEGORIES */ #include "modules/interpolator_ut.h" +#include "modules/path_resolve_ut.h" #include "modules/state_machine_ut.h" // Scripting tests @@ -27,6 +28,7 @@ int main() { Framework::Logging::GetInstance()->PauseLogging(true); UNIT_MODULE(interpolator); + UNIT_MODULE(path_resolve); UNIT_MODULE(state_machine); // Scripting tests diff --git a/code/tests/modules/path_resolve_ut.h b/code/tests/modules/path_resolve_ut.h new file mode 100644 index 000000000..fe9724e5c --- /dev/null +++ b/code/tests/modules/path_resolve_ut.h @@ -0,0 +1,66 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2024, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "utils/path.h" + +#include + +// Backslash separators and drive prefixes are only meaningful on Windows; on +// POSIX they are ordinary filename characters and the same inputs resolve to +// (harmless) files inside the root +#ifdef _WIN32 +inline constexpr bool kPathSepIsWindows = true; +#else +inline constexpr bool kPathSepIsWindows = false; +#endif + +MODULE(path_resolve, { + using Framework::Utils::ResolvePathUnderRoot; + namespace fs = std::filesystem; + + const fs::path root = fs::temp_directory_path() / "fw_path_resolve_ut" / "ui"; + fs::create_directories(root / "sub"); + const fs::path rootCanonical = fs::weakly_canonical(root); + + IT("maps empty and / to index.html", { + EQUALS(ResolvePathUnderRoot(root, "/") == rootCanonical / "index.html", true); + EQUALS(ResolvePathUnderRoot(root, "") == rootCanonical / "index.html", true); + }); + + IT("resolves nested paths under the root", { + EQUALS(ResolvePathUnderRoot(root, "/sub/style.css") == rootCanonical / "sub" / "style.css", true); + }); + + IT("resolves lexically without requiring the file to exist", { + EQUALS(ResolvePathUnderRoot(root, "/missing/deep/file.js") == rootCanonical / "missing" / "deep" / "file.js", true); + }); + + IT("allows dot-dot that stays inside the root", { + EQUALS(ResolvePathUnderRoot(root, "/sub/../index.html") == rootCanonical / "index.html", true); + }); + + IT("rejects dot-dot traversal escaping the root", { + EQUALS(ResolvePathUnderRoot(root, "/../secret.txt").empty(), true); + EQUALS(ResolvePathUnderRoot(root, "/sub/../../secret.txt").empty(), true); + EQUALS(ResolvePathUnderRoot(root, "/sub/../../../../../etc/passwd").empty(), true); + }); + + IT("rejects backslash traversal where backslash separates", { + EQUALS(ResolvePathUnderRoot(root, "/..\\secret.txt").empty(), kPathSepIsWindows); + EQUALS(ResolvePathUnderRoot(root, "/sub\\..\\..\\secret.txt").empty(), kPathSepIsWindows); + }); + + IT("rejects drive injection where drives exist", { + EQUALS(ResolvePathUnderRoot(root, "/C:/Windows/win.ini").empty(), kPathSepIsWindows); + EQUALS(ResolvePathUnderRoot(root, "/C:\\Windows\\win.ini").empty(), kPathSepIsWindows); + }); + + fs::remove_all(fs::temp_directory_path() / "fw_path_resolve_ut"); +});