#include "d3d9_swapchain.h" #include "d3d9_surface.h" #include "d3d9_monitor.h" #include "d3d9_hud.h" #include "d3d9_window.h" namespace dxvk { static uint16_t MapGammaControlPoint(float x) { if (x < 0.0f) x = 0.0f; if (x > 1.0f) x = 1.0f; return uint16_t(65535.0f * x); } struct D3D9PresentInfo { float scale[2]; float offset[2]; }; D3D9SwapChainEx::D3D9SwapChainEx( D3D9DeviceEx* pDevice, D3DPRESENT_PARAMETERS* pPresentParams, const D3DDISPLAYMODEEX* pFullscreenDisplayMode) : D3D9SwapChainExBase(pDevice) , m_device (pDevice->GetDXVKDevice()) , m_context (m_device->createContext(DxvkContextType::Supplementary)) , m_frameLatencyCap (pDevice->GetOptions()->maxFrameLatency) , m_dialog (pDevice->GetOptions()->enableDialogMode) , m_swapchainExt (this) { this->NormalizePresentParameters(pPresentParams); m_presentParams = *pPresentParams; m_window = m_presentParams.hDeviceWindow; UpdateWindowCtx(); UpdatePresentRegion(nullptr, nullptr); if (m_window) { CreatePresenter(); if (!pDevice->GetOptions()->deferSurfaceCreation) RecreateSwapChain(); } if (FAILED(CreateBackBuffers(m_presentParams.BackBufferCount))) throw DxvkError("D3D9: Failed to create swapchain backbuffers"); CreateBlitter(); CreateHud(); InitRamp(); // Apply initial window mode and fullscreen state if (!m_presentParams.Windowed && FAILED(EnterFullscreenMode(pPresentParams, pFullscreenDisplayMode))) throw DxvkError("D3D9: Failed to set initial fullscreen state"); } D3D9SwapChainEx::~D3D9SwapChainEx() { // Avoids hanging when in this state, see comment // in DxvkDevice::~DxvkDevice. if (this_thread::isInModuleDetachment()) return; DestroyBackBuffers(); ResetWindowProc(m_window); RestoreDisplayMode(m_monitor); m_device->waitForSubmission(&m_presentStatus); m_device->waitForIdle(); m_parent->DecrementLosableCounter(); } HRESULT STDMETHODCALLTYPE D3D9SwapChainEx::QueryInterface(REFIID riid, void** ppvObject) { if (ppvObject == nullptr) return E_POINTER; *ppvObject = nullptr; if (riid == __uuidof(IUnknown) || riid == __uuidof(IDirect3DSwapChain9) || (GetParent()->IsExtended() && riid == __uuidof(IDirect3DSwapChain9Ex))) { *ppvObject = ref(this); return S_OK; } if (riid == __uuidof(ID3D9VkExtSwapchain)) { *ppvObject = ref(&m_swapchainExt); return S_OK; } if (logQueryInterfaceError(__uuidof(IDirect3DSwapChain9), riid)) { Logger::warn("D3D9SwapChainEx::QueryInterface: Unknown interface query"); Logger::warn(str::format(riid)); } return E_NOINTERFACE; } HRESULT STDMETHODCALLTYPE D3D9SwapChainEx::Present( const RECT* pSourceRect, const RECT* pDestRect, HWND hDestWindowOverride, const RGNDATA* pDirtyRegion, DWORD dwFlags) { D3D9DeviceLock lock = m_parent->LockDevice(); if (unlikely(m_parent->IsDeviceLost())) return D3DERR_DEVICELOST; // If we have no backbuffers, error out. // This handles the case where a ::Reset failed due to OOM // or whatever. // I am not sure what the actual HRESULT returned here is // or should be, but it is better than crashing... probably! if (m_backBuffers.empty()) return D3D_OK; uint32_t presentInterval = m_presentParams.PresentationInterval; // This is not true directly in d3d9 to to timing differences that don't matter for us. // For our purposes... // D3DPRESENT_INTERVAL_DEFAULT (0) == D3DPRESENT_INTERVAL_ONE (1) which means VSYNC. presentInterval = std::max(presentInterval, 1u); if (presentInterval == D3DPRESENT_INTERVAL_IMMEDIATE || (dwFlags & D3DPRESENT_FORCEIMMEDIATE)) presentInterval = 0; auto options = m_parent->GetOptions(); if (options->presentInterval >= 0) presentInterval = options->presentInterval; m_window = m_presentParams.hDeviceWindow; if (hDestWindowOverride != nullptr) m_window = hDestWindowOverride; UpdateWindowCtx(); bool recreate = false; recreate |= m_wctx->presenter == nullptr; recreate |= m_dialog != m_lastDialog; if (options->deferSurfaceCreation) recreate |= m_parent->IsDeviceReset(); if (m_wctx->presenter != nullptr) { m_dirty |= m_wctx->presenter->setSyncInterval(presentInterval) != VK_SUCCESS; m_dirty |= !m_wctx->presenter->hasSwapChain(); } m_dirty |= UpdatePresentRegion(pSourceRect, pDestRect); m_dirty |= recreate; m_lastDialog = m_dialog; #ifdef _WIN32 const bool useGDIFallback = m_partialCopy && !HasFrontBuffer(); if (useGDIFallback) return PresentImageGDI(m_window); #endif try { if (recreate) CreatePresenter(); if (std::exchange(m_dirty, false)) RecreateSwapChain(); // We aren't going to device loss simply because // 99% of D3D9 games don't handle this properly and // just end up crashing (like with alt-tab loss) if (!m_wctx->presenter->hasSwapChain()) return D3D_OK; PresentImage(presentInterval); return D3D_OK; } catch (const DxvkError& e) { Logger::err(e.message()); #ifdef _WIN32 return PresentImageGDI(m_window); #else return D3DERR_DEVICEREMOVED; #endif } } #ifdef _WIN32 #define DCX_USESTYLE 0x00010000 HRESULT D3D9SwapChainEx::PresentImageGDI(HWND Window) { m_parent->EndFrame(); m_parent->Flush(); if (!std::exchange(m_warnedAboutGDIFallback, true)) Logger::warn("Using GDI for swapchain presentation. This will impact performance."); HDC hDC; HRESULT result = m_backBuffers[0]->GetDC(&hDC); if (result) { Logger::err("D3D9SwapChainEx::BlitGDI Surface GetDC failed"); return D3DERR_DEVICEREMOVED; } HDC dstDC = GetDCEx(Window, 0, DCX_CACHE | DCX_USESTYLE); if (!dstDC) { Logger::err("D3D9SwapChainEx::BlitGDI: GetDCEx failed"); m_backBuffers[0]->ReleaseDC(hDC); return D3DERR_DEVICEREMOVED; } bool success = StretchBlt(dstDC, m_dstRect.left, m_dstRect.top, m_dstRect.right - m_dstRect.left, m_dstRect.bottom - m_dstRect.top, hDC, m_srcRect.left, m_srcRect.top, m_srcRect.right - m_srcRect.left, m_srcRect.bottom - m_srcRect.top, SRCCOPY); m_backBuffers[0]->ReleaseDC(hDC); ReleaseDC(Window, dstDC); if (!success) { Logger::err("D3D9SwapChainEx::BlitGDI: StretchBlt failed"); return D3DERR_DEVICEREMOVED; } return S_OK; } #endif HRESULT STDMETHODCALLTYPE D3D9SwapChainEx::GetFrontBufferData(IDirect3DSurface9* pDestSurface) { D3D9DeviceLock lock = m_parent->LockDevice(); // This function can do absolutely everything! // Copies the front buffer between formats with an implicit resolve. // Oh, and the dest is systemmem... // This is a slow function anyway, it waits for the copy to finish. // so there's no reason to not just make and throwaway temp images. // If extent of dst > src, then we blit to a subrect of the size // of src onto a temp image of dst's extents, // then copy buffer back to dst (given dst is subresource) // For SWAPEFFECT_COPY and windowed SWAPEFFECT_DISCARD with 1 backbuffer, we just copy the backbuffer data instead. // We just copy from the backbuffer instead of the front buffer to avoid having to do another blit. // This mostly impacts windowed mode and our implementation was not accurate in that case anyway as Windows D3D9 // takes a screenshot of the entire screen. D3D9Surface* dst = static_cast(pDestSurface); if (unlikely(dst == nullptr)) return D3DERR_INVALIDCALL; D3D9CommonTexture* dstTexInfo = dst->GetCommonTexture(); D3D9CommonTexture* srcTexInfo = GetFrontBuffer()->GetCommonTexture(); if (unlikely(dstTexInfo->Desc()->Pool != D3DPOOL_SYSTEMMEM && dstTexInfo->Desc()->Pool != D3DPOOL_SCRATCH)) return D3DERR_INVALIDCALL; if (unlikely(m_parent->IsDeviceLost())) { return D3DERR_DEVICELOST; } VkExtent3D dstTexExtent = dstTexInfo->GetExtentMip(dst->GetMipLevel()); VkExtent3D srcTexExtent = srcTexInfo->GetExtentMip(0); const bool clearDst = dstTexInfo->Desc()->MipLevels > 1 || dstTexExtent.width > srcTexExtent.width || dstTexExtent.height > srcTexExtent.height; dstTexInfo->CreateBuffer(clearDst); DxvkBufferSlice dstBufferSlice = dstTexInfo->GetBufferSlice(dst->GetSubresource()); Rc srcImage = srcTexInfo->GetImage(); if (srcImage->info().sampleCount != VK_SAMPLE_COUNT_1_BIT) { DxvkImageCreateInfo resolveInfo; resolveInfo.type = VK_IMAGE_TYPE_2D; resolveInfo.format = srcImage->info().format; resolveInfo.flags = 0; resolveInfo.sampleCount = VK_SAMPLE_COUNT_1_BIT; resolveInfo.extent = srcImage->info().extent; resolveInfo.numLayers = 1; resolveInfo.mipLevels = 1; resolveInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; resolveInfo.stages = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT; resolveInfo.access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; resolveInfo.tiling = VK_IMAGE_TILING_OPTIMAL; resolveInfo.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; Rc resolvedSrc = m_device->createImage( resolveInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); m_parent->EmitCs([ cDstImage = resolvedSrc, cSrcImage = srcImage ] (DxvkContext* ctx) { VkImageSubresourceLayers resolveSubresource; resolveSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; resolveSubresource.mipLevel = 0; resolveSubresource.baseArrayLayer = 0; resolveSubresource.layerCount = 1; VkImageResolve resolveRegion; resolveRegion.srcSubresource = resolveSubresource; resolveRegion.srcOffset = VkOffset3D { 0, 0, 0 }; resolveRegion.dstSubresource = resolveSubresource; resolveRegion.dstOffset = VkOffset3D { 0, 0, 0 }; resolveRegion.extent = cSrcImage->info().extent; ctx->resolveImage( cDstImage, cSrcImage, resolveRegion, VK_FORMAT_UNDEFINED); }); srcImage = std::move(resolvedSrc); } D3D9Format srcFormat = srcTexInfo->Desc()->Format; D3D9Format dstFormat = dstTexInfo->Desc()->Format; bool similar = AreFormatsSimilar(srcFormat, dstFormat); if (!similar || srcImage->info().extent != dstTexInfo->GetExtent()) { DxvkImageCreateInfo blitCreateInfo; blitCreateInfo.type = VK_IMAGE_TYPE_2D; blitCreateInfo.format = dstTexInfo->GetFormatMapping().FormatColor; blitCreateInfo.flags = 0; blitCreateInfo.sampleCount = VK_SAMPLE_COUNT_1_BIT; blitCreateInfo.extent = dstTexInfo->GetExtent(); blitCreateInfo.numLayers = 1; blitCreateInfo.mipLevels = 1; blitCreateInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; blitCreateInfo.stages = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT; blitCreateInfo.access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; blitCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL; blitCreateInfo.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; Rc blittedSrc = m_device->createImage( blitCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); const DxvkFormatInfo* dstFormatInfo = lookupFormatInfo(blittedSrc->info().format); const DxvkFormatInfo* srcFormatInfo = lookupFormatInfo(srcImage->info().format); const VkImageSubresource dstSubresource = dstTexInfo->GetSubresourceFromIndex(dstFormatInfo->aspectMask, 0); const VkImageSubresource srcSubresource = srcTexInfo->GetSubresourceFromIndex(srcFormatInfo->aspectMask, 0); VkImageSubresourceLayers dstSubresourceLayers = { dstSubresource.aspectMask, dstSubresource.mipLevel, dstSubresource.arrayLayer, 1 }; VkImageSubresourceLayers srcSubresourceLayers = { srcSubresource.aspectMask, srcSubresource.mipLevel, srcSubresource.arrayLayer, 1 }; VkExtent3D srcExtent = srcImage->mipLevelExtent(srcSubresource.mipLevel); // Blit to a subrect of the src extents VkImageBlit blitInfo; blitInfo.dstSubresource = dstSubresourceLayers; blitInfo.srcSubresource = srcSubresourceLayers; blitInfo.dstOffsets[0] = VkOffset3D{ 0, 0, 0 }; blitInfo.dstOffsets[1] = VkOffset3D{ int32_t(srcExtent.width), int32_t(srcExtent.height), 1 }; blitInfo.srcOffsets[0] = VkOffset3D{ 0, 0, 0 }; blitInfo.srcOffsets[1] = VkOffset3D{ int32_t(srcExtent.width), int32_t(srcExtent.height), 1 }; #ifdef _WIN32 if (m_presentParams.Windowed) { // In windowed mode, GetFrontBufferData takes a screenshot of the entire screen. // So place the copy of the front buffer at the position of the window. POINT point = { 0, 0 }; if (ClientToScreen(m_window, &point) != 0) { blitInfo.dstOffsets[0].x = point.x; blitInfo.dstOffsets[0].y = point.y; blitInfo.dstOffsets[1].x += point.x; blitInfo.dstOffsets[1].y += point.y; } } #endif m_parent->EmitCs([ cDstImage = blittedSrc, cDstMap = dstTexInfo->GetMapping().Swizzle, cSrcImage = srcImage, cSrcMap = srcTexInfo->GetMapping().Swizzle, cBlitInfo = blitInfo ] (DxvkContext* ctx) { ctx->blitImage( cDstImage, cDstMap, cSrcImage, cSrcMap, cBlitInfo, VK_FILTER_NEAREST); }); srcImage = std::move(blittedSrc); } const DxvkFormatInfo* srcFormatInfo = lookupFormatInfo(srcImage->info().format); const VkImageSubresource srcSubresource = srcTexInfo->GetSubresourceFromIndex(srcFormatInfo->aspectMask, 0); VkImageSubresourceLayers srcSubresourceLayers = { srcSubresource.aspectMask, srcSubresource.mipLevel, srcSubresource.arrayLayer, 1 }; VkExtent3D srcExtent = srcImage->mipLevelExtent(srcSubresource.mipLevel); m_parent->EmitCs([ cBufferSlice = std::move(dstBufferSlice), cImage = std::move(srcImage), cSubresources = srcSubresourceLayers, cLevelExtent = srcExtent ] (DxvkContext* ctx) { ctx->copyImageToBuffer(cBufferSlice.buffer(), cBufferSlice.offset(), 4, 0, cImage, cSubresources, VkOffset3D { 0, 0, 0 }, cLevelExtent); }); dstTexInfo->SetNeedsReadback(dst->GetSubresource(), true); m_parent->TrackTextureMappingBufferSequenceNumber(dstTexInfo, dst->GetSubresource()); return D3D_OK; } HRESULT STDMETHODCALLTYPE D3D9SwapChainEx::GetBackBuffer( UINT iBackBuffer, D3DBACKBUFFER_TYPE Type, IDirect3DSurface9** ppBackBuffer) { // Could be doing a device reset... D3D9DeviceLock lock = m_parent->LockDevice(); if (unlikely(ppBackBuffer == nullptr)) return D3DERR_INVALIDCALL; if (unlikely(iBackBuffer >= m_presentParams.BackBufferCount)) { Logger::err(str::format("D3D9: GetBackBuffer: Invalid back buffer index: ", iBackBuffer)); return D3DERR_INVALIDCALL; } if (m_backBuffers.empty()) { // The backbuffers were destroyed and not recreated. // This can happen when a call to Reset fails. *ppBackBuffer = nullptr; return D3D_OK; } *ppBackBuffer = ref(m_backBuffers[iBackBuffer].ptr()); return D3D_OK; } HRESULT STDMETHODCALLTYPE D3D9SwapChainEx::GetRasterStatus(D3DRASTER_STATUS* pRasterStatus) { // We could use D3DKMTGetScanLine but Wine doesn't implement that. // So... we lie here and make some stuff up // enough that it makes games work. // Assume there's 20 lines in a vBlank. constexpr uint32_t vBlankLineCount = 20; if (pRasterStatus == nullptr) return D3DERR_INVALIDCALL; D3DDISPLAYMODEEX mode; mode.Size = sizeof(mode); if (FAILED(this->GetDisplayModeEx(&mode, nullptr))) return D3DERR_INVALIDCALL; uint32_t scanLineCount = mode.Height + vBlankLineCount; auto nowUs = std::chrono::time_point_cast( dxvk::high_resolution_clock::now()) .time_since_epoch(); auto frametimeUs = std::chrono::microseconds(1000000u / mode.RefreshRate); auto scanLineUs = frametimeUs / scanLineCount; pRasterStatus->ScanLine = (nowUs % frametimeUs) / scanLineUs; pRasterStatus->InVBlank = pRasterStatus->ScanLine >= mode.Height; if (pRasterStatus->InVBlank) pRasterStatus->ScanLine = 0; return D3D_OK; } HRESULT STDMETHODCALLTYPE D3D9SwapChainEx::GetDisplayMode(D3DDISPLAYMODE* pMode) { if (pMode == nullptr) return D3DERR_INVALIDCALL; *pMode = D3DDISPLAYMODE(); D3DDISPLAYMODEEX mode; mode.Size = sizeof(mode); HRESULT hr = this->GetDisplayModeEx(&mode, nullptr); if (FAILED(hr)) return hr; pMode->Width = mode.Width; pMode->Height = mode.Height; pMode->Format = mode.Format; pMode->RefreshRate = mode.RefreshRate; return D3D_OK; } HRESULT STDMETHODCALLTYPE D3D9SwapChainEx::GetPresentParameters(D3DPRESENT_PARAMETERS* pPresentationParameters) { if (pPresentationParameters == nullptr) return D3DERR_INVALIDCALL; *pPresentationParameters = m_presentParams; return D3D_OK; } HRESULT STDMETHODCALLTYPE D3D9SwapChainEx::GetLastPresentCount(UINT* pLastPresentCount) { Logger::warn("D3D9SwapChainEx::GetLastPresentCount: Stub"); return D3D_OK; } HRESULT STDMETHODCALLTYPE D3D9SwapChainEx::GetPresentStats(D3DPRESENTSTATS* pPresentationStatistics) { Logger::warn("D3D9SwapChainEx::GetPresentStats: Stub"); return D3D_OK; } HRESULT STDMETHODCALLTYPE D3D9SwapChainEx::GetDisplayModeEx(D3DDISPLAYMODEEX* pMode, D3DDISPLAYROTATION* pRotation) { if (pMode == nullptr && pRotation == nullptr) return D3DERR_INVALIDCALL; if (pRotation != nullptr) *pRotation = D3DDISPLAYROTATION_IDENTITY; if (pMode != nullptr) { wsi::WsiMode devMode = { }; if (!wsi::getCurrentDisplayMode(wsi::getDefaultMonitor(), &devMode)) { Logger::err("D3D9SwapChainEx::GetDisplayModeEx: Failed to enum display settings"); return D3DERR_INVALIDCALL; } *pMode = ConvertDisplayMode(devMode); } return D3D_OK; } HRESULT D3D9SwapChainEx::Reset( D3DPRESENT_PARAMETERS* pPresentParams, D3DDISPLAYMODEEX* pFullscreenDisplayMode) { D3D9DeviceLock lock = m_parent->LockDevice(); HRESULT hr = D3D_OK; this->SynchronizePresent(); this->NormalizePresentParameters(pPresentParams); m_dirty |= m_presentParams.BackBufferFormat != pPresentParams->BackBufferFormat || m_presentParams.BackBufferCount != pPresentParams->BackBufferCount; bool changeFullscreen = m_presentParams.Windowed != pPresentParams->Windowed; if (pPresentParams->Windowed) { if (changeFullscreen) this->LeaveFullscreenMode(); } else { m_parent->NotifyFullscreen(m_window, true); if (changeFullscreen) { hr = this->EnterFullscreenMode(pPresentParams, pFullscreenDisplayMode); if (FAILED(hr)) return hr; } D3D9WindowMessageFilter filter(m_window); if (!changeFullscreen) { hr = ChangeDisplayMode(pPresentParams, pFullscreenDisplayMode); if (FAILED(hr)) return hr; wsi::updateFullscreenWindow(m_monitor, m_window, true); } } m_presentParams = *pPresentParams; if (changeFullscreen) SetGammaRamp(0, &m_ramp); hr = CreateBackBuffers(m_presentParams.BackBufferCount); if (FAILED(hr)) return hr; return D3D_OK; } HRESULT D3D9SwapChainEx::WaitForVBlank() { static bool s_errorShown = false; if (!std::exchange(s_errorShown, true)) Logger::warn("D3D9SwapChainEx::WaitForVBlank: Stub"); return D3D_OK; } static bool validateGammaRamp(const WORD (&ramp)[256]) { if (ramp[0] >= ramp[std::size(ramp) - 1]) { Logger::err("validateGammaRamp: ramp inverted or flat"); return false; } for (size_t i = 1; i < std::size(ramp); i++) { if (ramp[i] < ramp[i - 1]) { Logger::err("validateGammaRamp: ramp not monotonically increasing"); return false; } if (ramp[i] - ramp[i - 1] >= UINT16_MAX / 2) { Logger::err("validateGammaRamp: huuuge jump"); return false; } } return true; } void D3D9SwapChainEx::SetGammaRamp( DWORD Flags, const D3DGAMMARAMP* pRamp) { D3D9DeviceLock lock = m_parent->LockDevice(); if (unlikely(pRamp == nullptr)) return; if (unlikely(!validateGammaRamp(pRamp->red) && !validateGammaRamp(pRamp->blue) && !validateGammaRamp(pRamp->green))) return; m_ramp = *pRamp; bool isIdentity = true; std::array cp; for (uint32_t i = 0; i < NumControlPoints; i++) { uint16_t identity = MapGammaControlPoint(float(i) / float(NumControlPoints - 1)); cp[i].r = pRamp->red[i]; cp[i].g = pRamp->green[i]; cp[i].b = pRamp->blue[i]; cp[i].a = 0; isIdentity &= cp[i].r == identity && cp[i].g == identity && cp[i].b == identity; } if (!isIdentity && !m_presentParams.Windowed) m_blitter->setGammaRamp(NumControlPoints, cp.data()); else m_blitter->setGammaRamp(0, nullptr); } void D3D9SwapChainEx::GetGammaRamp(D3DGAMMARAMP* pRamp) { D3D9DeviceLock lock = m_parent->LockDevice(); if (likely(pRamp != nullptr)) *pRamp = m_ramp; } void D3D9SwapChainEx::Invalidate(HWND hWindow) { if (hWindow == nullptr) hWindow = m_parent->GetWindow(); if (m_presenters.count(hWindow)) { if (m_wctx == &m_presenters[hWindow]) m_wctx = nullptr; m_presenters.erase(hWindow); m_device->waitForSubmission(&m_presentStatus); m_device->waitForIdle(); } } HRESULT D3D9SwapChainEx::SetDialogBoxMode(bool bEnableDialogs) { D3D9DeviceLock lock = m_parent->LockDevice(); // https://docs.microsoft.com/en-us/windows/win32/api/d3d9/nf-d3d9-idirect3ddevice9-setdialogboxmode // The MSDN documentation says this will error out under many weird conditions. // However it doesn't appear to error at all in any of my tests of these // cases described in the documentation. m_dialog = bEnableDialogs; return D3D_OK; } D3D9Surface* D3D9SwapChainEx::GetBackBuffer(UINT iBackBuffer) { if (iBackBuffer >= m_presentParams.BackBufferCount) return nullptr; return m_backBuffers[iBackBuffer].ptr(); } void D3D9SwapChainEx::NormalizePresentParameters(D3DPRESENT_PARAMETERS* pPresentParams) { if (pPresentParams->hDeviceWindow == nullptr) pPresentParams->hDeviceWindow = m_parent->GetWindow(); pPresentParams->BackBufferCount = std::max(pPresentParams->BackBufferCount, 1u); const int32_t forcedMSAA = m_parent->GetOptions()->forceSwapchainMSAA; if (forcedMSAA != -1) { pPresentParams->MultiSampleType = D3DMULTISAMPLE_TYPE(forcedMSAA); pPresentParams->MultiSampleQuality = 0; } if (pPresentParams->Windowed) { wsi::getWindowSize(pPresentParams->hDeviceWindow, pPresentParams->BackBufferWidth ? nullptr : &pPresentParams->BackBufferWidth, pPresentParams->BackBufferHeight ? nullptr : &pPresentParams->BackBufferHeight); } else { wsi::getMonitorClientSize(wsi::getDefaultMonitor(), pPresentParams->BackBufferWidth ? nullptr : &pPresentParams->BackBufferWidth, pPresentParams->BackBufferHeight ? nullptr : &pPresentParams->BackBufferHeight); } if (pPresentParams->BackBufferFormat == D3DFMT_UNKNOWN) pPresentParams->BackBufferFormat = D3DFMT_X8R8G8B8; if (env::getEnvVar("DXVK_FORCE_WINDOWED") == "1") pPresentParams->Windowed = TRUE; } void D3D9SwapChainEx::PresentImage(UINT SyncInterval) { m_parent->EndFrame(); m_parent->Flush(); // Retrieve the image and image view to present Rc swapImage = m_backBuffers[0]->GetCommonTexture()->GetImage(); Rc swapImageView = m_backBuffers[0]->GetImageView(false); for (uint32_t i = 0; i < SyncInterval || i < 1; i++) { SynchronizePresent(); // Presentation semaphores and WSI swap chain image PresenterInfo info = m_wctx->presenter->info(); PresenterSync sync; uint32_t imageIndex = 0; VkResult status = m_wctx->presenter->acquireNextImage(sync, imageIndex); while (status != VK_SUCCESS && status != VK_SUBOPTIMAL_KHR) { RecreateSwapChain(); info = m_wctx->presenter->info(); status = m_wctx->presenter->acquireNextImage(sync, imageIndex); } if (m_hdrMetadata && m_dirtyHdrMetadata) { m_wctx->presenter->setHdrMetadata(*m_hdrMetadata); m_dirtyHdrMetadata = false; } m_context->beginRecording( m_device->createCommandList()); VkRect2D srcRect = { { int32_t(m_srcRect.left), int32_t(m_srcRect.top) }, { uint32_t(m_srcRect.right - m_srcRect.left), uint32_t(m_srcRect.bottom - m_srcRect.top) } }; VkRect2D dstRect = { { int32_t(m_dstRect.left), int32_t(m_dstRect.top) }, { uint32_t(m_dstRect.right - m_dstRect.left), uint32_t(m_dstRect.bottom - m_dstRect.top) } }; m_blitter->presentImage(m_context.ptr(), m_wctx->imageViews.at(imageIndex), dstRect, swapImageView, srcRect); if (m_hud != nullptr) m_hud->render(m_context, info.format, info.imageExtent); SubmitPresent(sync, i); } SyncFrameLatency(); // Rotate swap chain buffers so that the back // buffer at index 0 becomes the front buffer. for (uint32_t i = 1; i < m_backBuffers.size(); i++) m_backBuffers[i]->Swap(m_backBuffers[i - 1].ptr()); m_parent->m_flags.set(D3D9DeviceFlag::DirtyFramebuffer); } void D3D9SwapChainEx::SubmitPresent(const PresenterSync& Sync, uint32_t Repeat) { // Bump frame ID if (!Repeat) m_wctx->frameId += 1; // Present from CS thread so that we don't // have to synchronize with it first. m_presentStatus.result = VK_NOT_READY; m_parent->EmitCs([this, cRepeat = Repeat, cSync = Sync, cHud = m_hud, cPresentMode = m_wctx->presenter->info().presentMode, cFrameId = m_wctx->frameId, cCommandList = m_context->endRecording() ] (DxvkContext* ctx) { cCommandList->setWsiSemaphores(cSync); m_device->submitCommandList(cCommandList, nullptr); if (cHud != nullptr && !cRepeat) cHud->update(); uint64_t frameId = cRepeat ? 0 : cFrameId; m_device->presentImage(m_wctx->presenter, cPresentMode, frameId, &m_presentStatus); }); m_parent->FlushCsChunk(); } void D3D9SwapChainEx::SynchronizePresent() { // Recreate swap chain if the previous present call failed VkResult status = m_device->waitForSubmission(&m_presentStatus); if (status != VK_SUCCESS) RecreateSwapChain(); } void D3D9SwapChainEx::RecreateSwapChain() { // Ensure that we can safely destroy the swap chain m_device->waitForSubmission(&m_presentStatus); m_device->waitForIdle(); m_presentStatus.result = VK_SUCCESS; PresenterDesc presenterDesc; presenterDesc.imageExtent = GetPresentExtent(); presenterDesc.imageCount = PickImageCount(m_presentParams.BackBufferCount + 1); presenterDesc.numFormats = PickFormats(EnumerateFormat(m_presentParams.BackBufferFormat), presenterDesc.formats); presenterDesc.fullScreenExclusive = PickFullscreenMode(); VkResult vr = m_wctx->presenter->recreateSwapChain(presenterDesc); if (vr == VK_ERROR_SURFACE_LOST_KHR) { vr = m_wctx->presenter->recreateSurface([this] (VkSurfaceKHR* surface) { return CreateSurface(surface); }); if (vr) throw DxvkError(str::format("D3D9SwapChainEx: Failed to recreate surface: ", vr)); vr = m_wctx->presenter->recreateSwapChain(presenterDesc); } if (vr) throw DxvkError(str::format("D3D9SwapChainEx: Failed to recreate swap chain: ", vr)); CreateRenderTargetViews(); } void D3D9SwapChainEx::CreatePresenter() { // Ensure that we can safely destroy the swap chain m_device->waitForSubmission(&m_presentStatus); m_device->waitForIdle(); m_presentStatus.result = VK_SUCCESS; PresenterDesc presenterDesc; presenterDesc.imageExtent = GetPresentExtent(); presenterDesc.imageCount = PickImageCount(m_presentParams.BackBufferCount + 1); presenterDesc.numFormats = PickFormats(EnumerateFormat(m_presentParams.BackBufferFormat), presenterDesc.formats); presenterDesc.fullScreenExclusive = PickFullscreenMode(); m_wctx->presenter = new Presenter(m_device, m_wctx->frameLatencySignal, presenterDesc); m_wctx->presenter->setFrameRateLimit(m_parent->GetOptions()->maxFrameRate); } VkResult D3D9SwapChainEx::CreateSurface(VkSurfaceKHR* pSurface) { auto vki = m_device->adapter()->vki(); return wsi::createSurface(m_window, vki->getLoaderProc(), vki->instance(), pSurface); } void D3D9SwapChainEx::CreateRenderTargetViews() { PresenterInfo info = m_wctx->presenter->info(); m_wctx->imageViews.clear(); m_wctx->imageViews.resize(info.imageCount); DxvkImageCreateInfo imageInfo; imageInfo.type = VK_IMAGE_TYPE_2D; imageInfo.format = info.format.format; imageInfo.flags = 0; imageInfo.sampleCount = VK_SAMPLE_COUNT_1_BIT; imageInfo.extent = { info.imageExtent.width, info.imageExtent.height, 1 }; imageInfo.numLayers = 1; imageInfo.mipLevels = 1; imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; imageInfo.stages = 0; imageInfo.access = 0; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; imageInfo.shared = VK_TRUE; DxvkImageViewCreateInfo viewInfo; viewInfo.type = VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = info.format.format; viewInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; viewInfo.aspect = VK_IMAGE_ASPECT_COLOR_BIT; viewInfo.minLevel = 0; viewInfo.numLevels = 1; viewInfo.minLayer = 0; viewInfo.numLayers = 1; for (uint32_t i = 0; i < info.imageCount; i++) { VkImage imageHandle = m_wctx->presenter->getImage(i).image; Rc image = new DxvkImage( m_device.ptr(), imageInfo, imageHandle, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); m_wctx->imageViews[i] = new DxvkImageView( m_device->vkd(), image, viewInfo); } } void D3D9SwapChainEx::DestroyBackBuffers() { for (auto& backBuffer : m_backBuffers) backBuffer->ClearContainer(); m_backBuffers.clear(); } void D3D9SwapChainEx::UpdateWindowCtx() { if (!m_presenters.count(m_window)) { auto res = m_presenters.emplace( std::piecewise_construct, std::forward_as_tuple(m_window), std::forward_as_tuple()); auto& wctx = res.first->second; wctx.frameLatencySignal = new sync::Fence(wctx.frameId); } m_wctx = &m_presenters[m_window]; } HRESULT D3D9SwapChainEx::CreateBackBuffers(uint32_t NumBackBuffers) { // Explicitly destroy current swap image before // creating a new one to free up resources DestroyBackBuffers(); int NumFrontBuffer = HasFrontBuffer() ? 1 : 0; const uint32_t NumBuffers = NumBackBuffers + NumFrontBuffer; m_backBuffers.reserve(NumBuffers); // Create new back buffer D3D9_COMMON_TEXTURE_DESC desc; desc.Width = std::max(m_presentParams.BackBufferWidth, 1u); desc.Height = std::max(m_presentParams.BackBufferHeight, 1u); desc.Depth = 1; desc.MipLevels = 1; desc.ArraySize = 1; desc.Format = EnumerateFormat(m_presentParams.BackBufferFormat); desc.MultiSample = m_presentParams.MultiSampleType; desc.MultisampleQuality = m_presentParams.MultiSampleQuality; desc.Pool = D3DPOOL_DEFAULT; desc.Usage = D3DUSAGE_RENDERTARGET; desc.Discard = FALSE; desc.IsBackBuffer = TRUE; desc.IsAttachmentOnly = FALSE; // Docs: Also note that - unlike textures - swap chain back buffers, render targets [..] can be locked desc.IsLockable = TRUE; for (uint32_t i = 0; i < NumBuffers; i++) { D3D9Surface* surface; try { surface = new D3D9Surface(m_parent, &desc, this, nullptr); m_parent->IncrementLosableCounter(); } catch (const DxvkError& e) { DestroyBackBuffers(); Logger::err(e.message()); return D3DERR_OUTOFVIDEOMEMORY; } m_backBuffers.emplace_back(surface); } auto swapImage = m_backBuffers[0]->GetCommonTexture()->GetImage(); // Initialize the image so that we can use it. Clearing // to black prevents garbled output for the first frame. VkImageSubresourceRange subresources; subresources.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; subresources.baseMipLevel = 0; subresources.levelCount = 1; subresources.baseArrayLayer = 0; subresources.layerCount = 1; m_context->beginRecording( m_device->createCommandList()); for (uint32_t i = 0; i < m_backBuffers.size(); i++) { m_context->initImage( m_backBuffers[i]->GetCommonTexture()->GetImage(), subresources, VK_IMAGE_LAYOUT_UNDEFINED); } m_device->submitCommandList( m_context->endRecording(), nullptr); return D3D_OK; } void D3D9SwapChainEx::CreateBlitter() { m_blitter = new DxvkSwapchainBlitter(m_device); } void D3D9SwapChainEx::CreateHud() { m_hud = hud::Hud::createHud(m_device); if (m_hud != nullptr) { m_hud->addItem("api", 1, GetApiName()); m_hud->addItem("samplers", -1, m_parent); #ifdef D3D9_ALLOW_UNMAPPING m_hud->addItem("memory", -1, m_parent); #endif } } void D3D9SwapChainEx::InitRamp() { for (uint32_t i = 0; i < NumControlPoints; i++) { DWORD identity = DWORD(MapGammaControlPoint(float(i) / float(NumControlPoints - 1))); m_ramp.red[i] = identity; m_ramp.green[i] = identity; m_ramp.blue[i] = identity; } } void D3D9SwapChainEx::SyncFrameLatency() { // Wait for the sync event so that we respect the maximum frame latency m_wctx->frameLatencySignal->wait(m_wctx->frameId - GetActualFrameLatency()); } void D3D9SwapChainEx::SetApiName(const char* name) { m_apiName = name; CreateHud(); } uint32_t D3D9SwapChainEx::GetActualFrameLatency() { uint32_t maxFrameLatency = m_parent->GetFrameLatency(); if (m_frameLatencyCap) maxFrameLatency = std::min(maxFrameLatency, m_frameLatencyCap); maxFrameLatency = std::min(maxFrameLatency, m_presentParams.BackBufferCount + 1); return maxFrameLatency; } uint32_t D3D9SwapChainEx::PickFormats( D3D9Format Format, VkSurfaceFormatKHR* pDstFormats) { uint32_t n = 0; switch (Format) { default: Logger::warn(str::format("D3D9SwapChainEx: Unexpected format: ", Format)); [[fallthrough]]; case D3D9Format::A8R8G8B8: case D3D9Format::X8R8G8B8: case D3D9Format::A8B8G8R8: case D3D9Format::X8B8G8R8: { pDstFormats[n++] = { VK_FORMAT_R8G8B8A8_UNORM, m_colorspace }; pDstFormats[n++] = { VK_FORMAT_B8G8R8A8_UNORM, m_colorspace }; } break; case D3D9Format::A2R10G10B10: case D3D9Format::A2B10G10R10: { pDstFormats[n++] = { VK_FORMAT_A2B10G10R10_UNORM_PACK32, m_colorspace }; pDstFormats[n++] = { VK_FORMAT_A2R10G10B10_UNORM_PACK32, m_colorspace }; } break; case D3D9Format::X1R5G5B5: case D3D9Format::A1R5G5B5: { pDstFormats[n++] = { VK_FORMAT_B5G5R5A1_UNORM_PACK16, m_colorspace }; pDstFormats[n++] = { VK_FORMAT_R5G5B5A1_UNORM_PACK16, m_colorspace }; pDstFormats[n++] = { VK_FORMAT_A1R5G5B5_UNORM_PACK16, m_colorspace }; } break; case D3D9Format::R5G6B5: { pDstFormats[n++] = { VK_FORMAT_B5G6R5_UNORM_PACK16, m_colorspace }; pDstFormats[n++] = { VK_FORMAT_R5G6B5_UNORM_PACK16, m_colorspace }; } break; case D3D9Format::A16B16G16R16F: { if (m_unlockAdditionalFormats) { pDstFormats[n++] = { VK_FORMAT_R16G16B16A16_SFLOAT, m_colorspace }; } else { Logger::warn(str::format("D3D9SwapChainEx: Unexpected format: ", Format)); } break; } } return n; } uint32_t D3D9SwapChainEx::PickImageCount( UINT Preferred) { int32_t option = m_parent->GetOptions()->numBackBuffers; return option > 0 ? uint32_t(option) : uint32_t(Preferred); } void D3D9SwapChainEx::NotifyDisplayRefreshRate( double RefreshRate) { m_displayRefreshRate = RefreshRate; } HRESULT D3D9SwapChainEx::EnterFullscreenMode( D3DPRESENT_PARAMETERS* pPresentParams, const D3DDISPLAYMODEEX* pFullscreenDisplayMode) { if (FAILED(ChangeDisplayMode(pPresentParams, pFullscreenDisplayMode))) { Logger::err("D3D9: EnterFullscreenMode: Failed to change display mode"); return D3DERR_INVALIDCALL; } // Testing shows we shouldn't hook WM_NCCALCSIZE but we shouldn't change // windows style either. // // Some games restore window styles after we have changed it, so hooking is // also required. Doing it will allow us to create fullscreen windows // regardless of their style and it also appears to work on Windows. HookWindowProc(m_window, this); D3D9WindowMessageFilter filter(m_window); m_monitor = wsi::getDefaultMonitor(); if (!wsi::enterFullscreenMode(m_monitor, m_window, &m_windowState, true)) { Logger::err("D3D9: EnterFullscreenMode: Failed to enter fullscreen mode"); return D3DERR_INVALIDCALL; } m_parent->NotifyFullscreen(m_window, true); return D3D_OK; } HRESULT D3D9SwapChainEx::LeaveFullscreenMode() { if (!wsi::isWindow(m_window)) return D3DERR_INVALIDCALL; if (FAILED(RestoreDisplayMode(m_monitor))) Logger::warn("D3D9: LeaveFullscreenMode: Failed to restore display mode"); m_monitor = nullptr; ResetWindowProc(m_window); if (!wsi::leaveFullscreenMode(m_window, &m_windowState, false)) { Logger::err("D3D9: LeaveFullscreenMode: Failed to exit fullscreen mode"); return D3DERR_NOTAVAILABLE; } m_parent->NotifyFullscreen(m_window, false); return D3D_OK; } HRESULT D3D9SwapChainEx::ChangeDisplayMode( D3DPRESENT_PARAMETERS* pPresentParams, const D3DDISPLAYMODEEX* pFullscreenDisplayMode) { D3DDISPLAYMODEEX mode; if (pFullscreenDisplayMode) { mode = *pFullscreenDisplayMode; } else { mode.Width = pPresentParams->BackBufferWidth; mode.Height = pPresentParams->BackBufferHeight; mode.Format = pPresentParams->BackBufferFormat; mode.RefreshRate = pPresentParams->FullScreen_RefreshRateInHz; mode.ScanLineOrdering = D3DSCANLINEORDERING_PROGRESSIVE; mode.Size = sizeof(D3DDISPLAYMODEEX); } wsi::WsiMode wsiMode = ConvertDisplayMode(mode); HMONITOR monitor = wsi::getDefaultMonitor(); if (!wsi::setWindowMode(monitor, m_window, wsiMode)) return D3DERR_NOTAVAILABLE; if (wsi::getCurrentDisplayMode(monitor, &wsiMode)) NotifyDisplayRefreshRate(double(wsiMode.refreshRate.numerator) / double(wsiMode.refreshRate.denominator)); else NotifyDisplayRefreshRate(0.0); return D3D_OK; } HRESULT D3D9SwapChainEx::RestoreDisplayMode(HMONITOR hMonitor) { if (hMonitor == nullptr) return D3DERR_INVALIDCALL; if (!wsi::restoreDisplayMode()) return D3DERR_NOTAVAILABLE; NotifyDisplayRefreshRate(0.0); return D3D_OK; } bool D3D9SwapChainEx::UpdatePresentRegion(const RECT* pSourceRect, const RECT* pDestRect) { const bool isWindowed = m_presentParams.Windowed; // Tests show that present regions are ignored in fullscreen if (pSourceRect == nullptr || !isWindowed) { m_srcRect.top = 0; m_srcRect.left = 0; m_srcRect.right = m_presentParams.BackBufferWidth; m_srcRect.bottom = m_presentParams.BackBufferHeight; } else m_srcRect = *pSourceRect; UINT width, height; wsi::getWindowSize(m_window, &width, &height); RECT dstRect; if (pDestRect == nullptr || !isWindowed) { // TODO: Should we hook WM_SIZE message for this? dstRect.top = 0; dstRect.left = 0; dstRect.right = LONG(width); dstRect.bottom = LONG(height); } else dstRect = *pDestRect; m_partialCopy = dstRect.left != 0 || dstRect.top != 0 || dstRect.right - dstRect.left != LONG(width) || dstRect.bottom - dstRect.top != LONG(height); bool recreate = m_wctx->presenter == nullptr || m_wctx->presenter->info().imageExtent.width != width || m_wctx->presenter->info().imageExtent.height != height; m_swapchainExtent = { width, height }; m_dstRect = dstRect; return recreate; } VkExtent2D D3D9SwapChainEx::GetPresentExtent() { return m_swapchainExtent; } VkFullScreenExclusiveEXT D3D9SwapChainEx::PickFullscreenMode() { return m_dialog ? VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT : VK_FULL_SCREEN_EXCLUSIVE_DEFAULT_EXT; } std::string D3D9SwapChainEx::GetApiName() { if (m_apiName == nullptr) { return this->GetParent()->IsExtended() ? "D3D9Ex" : "D3D9"; } else { return m_apiName; } } D3D9VkExtSwapchain::D3D9VkExtSwapchain(D3D9SwapChainEx *pSwapChain) : m_swapchain(pSwapChain) { } ULONG STDMETHODCALLTYPE D3D9VkExtSwapchain::AddRef() { return m_swapchain->AddRef(); } ULONG STDMETHODCALLTYPE D3D9VkExtSwapchain::Release() { return m_swapchain->Release(); } HRESULT STDMETHODCALLTYPE D3D9VkExtSwapchain::QueryInterface( REFIID riid, void** ppvObject) { return m_swapchain->QueryInterface(riid, ppvObject); } BOOL STDMETHODCALLTYPE D3D9VkExtSwapchain::CheckColorSpaceSupport( VkColorSpaceKHR ColorSpace) { return m_swapchain->m_wctx->presenter->supportsColorSpace(ColorSpace); } HRESULT STDMETHODCALLTYPE D3D9VkExtSwapchain::SetColorSpace( VkColorSpaceKHR ColorSpace) { if (!CheckColorSpaceSupport(ColorSpace)) return D3DERR_INVALIDCALL; m_swapchain->m_dirty |= ColorSpace != m_swapchain->m_colorspace; m_swapchain->m_colorspace = ColorSpace; return S_OK; } HRESULT STDMETHODCALLTYPE D3D9VkExtSwapchain::SetHDRMetaData( const VkHdrMetadataEXT *pHDRMetadata) { if (!pHDRMetadata) return D3DERR_INVALIDCALL; m_swapchain->m_hdrMetadata = *pHDRMetadata; m_swapchain->m_dirtyHdrMetadata = true; return S_OK; } HRESULT STDMETHODCALLTYPE D3D9VkExtSwapchain::GetCurrentOutputDesc( D3D9VkExtOutputMetadata *pOutputDesc) { HMONITOR monitor = m_swapchain->m_monitor; if (!monitor) monitor = wsi::getDefaultMonitor(); // ^ this should be the display we are mostly covering someday. wsi::WsiEdidData edidData = wsi::getMonitorEdid(monitor); wsi::WsiDisplayMetadata metadata = {}; { std::optional r_metadata = std::nullopt; if (!edidData.empty()) r_metadata = wsi::parseColorimetryInfo(edidData); if (r_metadata) metadata = *r_metadata; else Logger::err("D3D9: Failed to parse display metadata + colorimetry info, using blank."); } NormalizeDisplayMetadata(CheckColorSpaceSupport(VK_COLOR_SPACE_HDR10_ST2084_EXT), metadata); pOutputDesc->RedPrimary[0] = metadata.redPrimary[0]; pOutputDesc->RedPrimary[1] = metadata.redPrimary[1]; pOutputDesc->GreenPrimary[0] = metadata.greenPrimary[0]; pOutputDesc->GreenPrimary[1] = metadata.greenPrimary[1]; pOutputDesc->BluePrimary[0] = metadata.bluePrimary[0]; pOutputDesc->BluePrimary[1] = metadata.bluePrimary[1]; pOutputDesc->WhitePoint[0] = metadata.whitePoint[0]; pOutputDesc->WhitePoint[1] = metadata.whitePoint[1]; pOutputDesc->MinLuminance = metadata.minLuminance; pOutputDesc->MaxLuminance = metadata.maxLuminance; pOutputDesc->MaxFullFrameLuminance = metadata.maxFullFrameLuminance; return S_OK; } void STDMETHODCALLTYPE D3D9VkExtSwapchain::UnlockAdditionalFormats() { m_swapchain->m_unlockAdditionalFormats = true; } }