Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ All notable changes to Q1View are documented here. Releases follow [semantic ver

## [Unreleased]

### Fixed
- Viewer: completed frames are now presented atomically through a DXGI flip-model swap chain, removing the double-DWM-wait bottleneck that limited 60 fps video to roughly 30 displayed frames per second; the verified GDI path remains available as a fallback, and unrotated OpenCV video decodes directly into the playback buffer to sustain real-time 59.94 fps presentation. (issue #90)

---

## [2.7.8] — 2026-08-18
Expand Down
218 changes: 218 additions & 0 deletions Viewer/DxgiPresenter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#include "stdafx.h"
#include "DxgiPresenter.h"

#include "QDebug.h"

#include <cstring>

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")

using Microsoft::WRL::ComPtr;

DxgiPresenter::DxgiPresenter()
: mHwnd(NULL)
, mWidth(0)
, mHeight(0)
, mDisabled(::GetEnvironmentVariableW(L"Q1VIEW_DISABLE_DXGI", NULL, 0) != 0)
{
if (mDisabled)
LOGINF("%s", "DXGI presenter disabled by Q1VIEW_DISABLE_DXGI");
}

DxgiPresenter::~DxgiPresenter()
{
Reset();
}

void DxgiPresenter::Reset()
{
if (mContext)
mContext->ClearState();

mBackBuffer.Reset();
mUploadTexture.Reset();
mSwapChain.Reset();
mContext.Reset();
mDevice.Reset();
mHwnd = NULL;
mWidth = 0;
mHeight = 0;
}

bool DxgiPresenter::Fail(const char *operation, HRESULT hr)
{
LOGWRN("DXGI presenter %s failed (0x%08lx); using GDI fallback",
operation, static_cast<unsigned long>(hr));
Reset();
mDisabled = true;
return false;
}

bool DxgiPresenter::CreateDevice()
{
static const D3D_FEATURE_LEVEL levels[] = {
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
};

D3D_FEATURE_LEVEL selectedLevel = D3D_FEATURE_LEVEL_10_0;
HRESULT hr = ::D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL,
D3D11_CREATE_DEVICE_BGRA_SUPPORT, levels, _countof(levels),
D3D11_SDK_VERSION, &mDevice, &selectedLevel, &mContext);
if (FAILED(hr)) {
hr = ::D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_WARP, NULL,
D3D11_CREATE_DEVICE_BGRA_SUPPORT, levels, _countof(levels),
D3D11_SDK_VERSION, &mDevice, &selectedLevel, &mContext);
}

return SUCCEEDED(hr) || Fail("device creation", hr);
}

bool DxgiPresenter::CreateSwapChain(HWND hwnd, UINT width, UINT height)
{
ComPtr<IDXGIDevice> dxgiDevice;
HRESULT hr = mDevice.As(&dxgiDevice);
if (FAILED(hr))
return Fail("device query", hr);

ComPtr<IDXGIAdapter> adapter;
hr = dxgiDevice->GetAdapter(&adapter);
if (FAILED(hr))
return Fail("adapter query", hr);

ComPtr<IDXGIFactory2> factory;
hr = adapter->GetParent(IID_PPV_ARGS(&factory));
if (FAILED(hr))
return Fail("factory query", hr);

DXGI_SWAP_CHAIN_DESC1 desc = {};
desc.Width = width;
desc.Height = height;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.BufferCount = 2;
desc.Scaling = DXGI_SCALING_STRETCH;
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
desc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;

hr = factory->CreateSwapChainForHwnd(mDevice.Get(), hwnd, &desc, NULL, NULL,
&mSwapChain);
if (FAILED(hr)) {
mSwapChain.Reset();
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
hr = factory->CreateSwapChainForHwnd(mDevice.Get(), hwnd, &desc, NULL, NULL,
&mSwapChain);
}
if (FAILED(hr))
return Fail("swap-chain creation", hr);

factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER);
mHwnd = hwnd;
return CreateSizeDependentResources(width, height);
}

bool DxgiPresenter::CreateSizeDependentResources(UINT width, UINT height)
{
HRESULT hr = mSwapChain->GetBuffer(0, IID_PPV_ARGS(&mBackBuffer));
if (FAILED(hr))
return Fail("back-buffer acquisition", hr);

D3D11_TEXTURE2D_DESC desc = {};
desc.Width = width;
desc.Height = height;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

hr = mDevice->CreateTexture2D(&desc, NULL, &mUploadTexture);
if (FAILED(hr))
return Fail("upload-texture creation", hr);

mWidth = width;
mHeight = height;
return true;
}

bool DxgiPresenter::Resize(UINT width, UINT height)
{
mContext->ClearState();
mBackBuffer.Reset();
mUploadTexture.Reset();

HRESULT hr = mSwapChain->ResizeBuffers(0, width, height,
DXGI_FORMAT_UNKNOWN, 0);
if (FAILED(hr))
return Fail("swap-chain resize", hr);

return CreateSizeDependentResources(width, height);
}

bool DxgiPresenter::EnsureResources(HWND hwnd, UINT width, UINT height)
{
if (mDisabled || hwnd == NULL || width == 0 || height == 0)
return false;

if (!mDevice && !CreateDevice())
return false;

if (!mSwapChain || mHwnd != hwnd) {
if (mSwapChain)
Reset();
if (!mDevice && !CreateDevice())
return false;
if (!CreateSwapChain(hwnd, width, height))
return false;
LOGINF("DXGI flip-model presenter active (%ux%u)", width, height);
return true;
}

if (mWidth != width || mHeight != height)
return Resize(width, height);

return true;
}

bool DxgiPresenter::Upload(const void *pixels, UINT rowPitch)
{
D3D11_MAPPED_SUBRESOURCE mapped = {};
HRESULT hr = mContext->Map(mUploadTexture.Get(), 0,
D3D11_MAP_WRITE_DISCARD, 0, &mapped);
if (FAILED(hr))
return Fail("texture mapping", hr);

const BYTE *src = static_cast<const BYTE *>(pixels);
BYTE *dst = static_cast<BYTE *>(mapped.pData);
const UINT copyBytes = mWidth * 4;
for (UINT y = 0; y < mHeight; ++y)
std::memcpy(dst + y * mapped.RowPitch, src + y * rowPitch, copyBytes);

mContext->Unmap(mUploadTexture.Get(), 0);
mContext->CopyResource(mBackBuffer.Get(), mUploadTexture.Get());
return true;
}

bool DxgiPresenter::Present(HWND hwnd, const void *pixels, UINT width,
UINT height, UINT rowPitch)
{
if (pixels == NULL || rowPitch < width * 4 ||
!EnsureResources(hwnd, width, height)) {
return false;
}

if (!Upload(pixels, rowPitch))
return false;

HRESULT hr = mSwapChain->Present(1, 0);
if (FAILED(hr))
return Fail("present", hr);

return true;
}
37 changes: 37 additions & 0 deletions Viewer/DxgiPresenter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma once

#include <d3d11.h>
#include <dxgi1_2.h>
#include <wrl/client.h>

// Presents a completed CPU-rendered BGRA frame through an HWND flip-model
// swap chain. All methods must be called on the UI thread.
class DxgiPresenter
{
public:
DxgiPresenter();
~DxgiPresenter();

bool Present(HWND hwnd, const void *pixels, UINT width, UINT height, UINT rowPitch);
void Reset();
bool IsActive() const { return mSwapChain != nullptr; }

private:
bool EnsureResources(HWND hwnd, UINT width, UINT height);
bool CreateDevice();
bool CreateSwapChain(HWND hwnd, UINT width, UINT height);
bool Resize(UINT width, UINT height);
bool CreateSizeDependentResources(UINT width, UINT height);
bool Upload(const void *pixels, UINT rowPitch);
bool Fail(const char *operation, HRESULT hr);

Microsoft::WRL::ComPtr<ID3D11Device> mDevice;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> mContext;
Microsoft::WRL::ComPtr<IDXGISwapChain1> mSwapChain;
Microsoft::WRL::ComPtr<ID3D11Texture2D> mUploadTexture;
Microsoft::WRL::ComPtr<ID3D11Texture2D> mBackBuffer;
HWND mHwnd;
UINT mWidth;
UINT mHeight;
bool mDisabled;
};
43 changes: 40 additions & 3 deletions Viewer/FrmProvideThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,60 @@
#include "ViewerCmn.h"
#include "QOcv.h"

void FrmProvideThread::LogPlaybackTrace() const
{
if (!mTraceEnabled || mTraceFrameCount == 0) {
return;
}

LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
double tickToMs = 1000.0 / static_cast<double>(frequency.QuadPart);
LOGWRN("worker summary: frames=%ld read_avg=%.3fms post_avg=%.3fms",
mTraceFrameCount, mTraceLoadTicks * tickToMs / mTraceFrameCount,
mTracePostProcessTicks * tickToMs / mTraceFrameCount);
}

bool FrmProvideThread::threadLoop()
{
BYTE *RGB = mBufferPool->checkout();
if (RGB == NULL)
return false;

long frameID = GetNextFrameID();
bool ok = loadOrigBuf(frameID, mOrigBuf);
const bool directRgb = mColorSpace == QIMAGE_CS_BGR888 &&
mBgr888Processor == NULL && mRot == QROT_000 &&
supportsDirectRgbLoad();
LARGE_INTEGER loadStart = {};
LARGE_INTEGER loadEnd = {};
if (mTraceEnabled)
QueryPerformanceCounter(&loadStart);
bool ok = directRgb ? loadRgbBuf(frameID, RGB) :
loadOrigBuf(frameID, mOrigBuf);
if (mTraceEnabled)
QueryPerformanceCounter(&loadEnd);
if (!ok) {
cancelFrameReservation(frameID);
sendQuitMsg(frameID);
mBufferPool->turn_back(RGB);
return false;
}

BufferInfo bi = PostProcess(mColorSpace, mBgr888Processor, mW, mH,
mOrigBuf, RGB, mBufOffset2, mBufOffset3, mRot, mCsc2Rgb888, frameID);
LARGE_INTEGER postEnd = {};
BufferInfo bi;
if (directRgb) {
bi.ID = frameID;
bi.addr = RGB;
} else {
bi = PostProcess(mColorSpace, mBgr888Processor, mW, mH,
mOrigBuf, RGB, mBufOffset2, mBufOffset3, mRot, mCsc2Rgb888, frameID);
}
if (mTraceEnabled) {
QueryPerformanceCounter(&postEnd);
mTraceFrameCount++;
mTraceLoadTicks += loadEnd.QuadPart - loadStart.QuadPart;
mTracePostProcessTicks += postEnd.QuadPart - loadEnd.QuadPart;
}

mBufferQueue->ordered_push(bi, frameID);

Expand Down
17 changes: 17 additions & 0 deletions Viewer/FrmProvideThread.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ class FrmProvideThread : public SThread
, mBufOffset3(0)
, mBgr888Processor(pBgr888Processor)
, mRot(QROT_000)
, mTraceEnabled(false)
, mTraceFrameCount(0)
, mTraceLoadTicks(0)
, mTracePostProcessTicks(0)
{}

virtual ~FrmProvideThread(void)
Expand All @@ -34,13 +38,20 @@ class FrmProvideThread : public SThread
delete [] mOrigBuf;
}

void LogPlaybackTrace() const;

inline long GetNextFrameID()
{
return qcmn_atomic_inc(mPlayFrameIdPtr);
}

inline bool setup(CViewerDoc *pDoc)
{
mTraceEnabled =
::GetEnvironmentVariableW(L"Q1VIEW_TRACE_PLAYBACK", NULL, 0) != 0;
mTraceFrameCount = 0;
mTraceLoadTicks = 0;
mTracePostProcessTicks = 0;
mW = pDoc->mW;
mH = pDoc->mH;
mRot = pDoc->mRot;
Expand Down Expand Up @@ -70,6 +81,8 @@ class FrmProvideThread : public SThread
virtual bool setupDetail(CViewerDoc *pDoc) = 0;
virtual void sendQuitMsg(long frameID) = 0;
virtual bool loadOrigBuf(long frameID, BYTE *buf) = 0;
virtual bool supportsDirectRgbLoad() const { return false; }
virtual bool loadRgbBuf(long frameID, BYTE *buf) { return false; }
virtual void cancelFrameReservation(long) {}

SSafeCQ<BufferInfo> *mBufferQueue;
Expand All @@ -88,4 +101,8 @@ class FrmProvideThread : public SThread
QIMAGE_CSC_FN mCsc2Rgb888;
q1::ImageProcessor *mBgr888Processor;
QROTATION mRot;
bool mTraceEnabled;
long mTraceFrameCount;
LONGLONG mTraceLoadTicks;
LONGLONG mTracePostProcessTicks;
};
1 change: 1 addition & 0 deletions Viewer/VidCapFrmSrc.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ class VidCapFrmSrc : public FrmSrc {
virtual inline void Stop()
{
mVidCapThread->requestExitAndWait();
mVidCapThread->LogPlaybackTrace();
}

virtual inline void Release()
Expand Down
Loading
Loading