From d1792e8e2bd88b29154949e436140737ac023453 Mon Sep 17 00:00:00 2001 From: RoL1n Date: Mon, 6 Jul 2026 17:41:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(MediaSystem):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=BF=90=E5=8A=A8=E6=A8=A1=E7=B3=8A=E4=B8=8E=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=BF=90=E5=8A=A8=E6=A8=A1=E7=B3=8A=E7=AE=A1=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 录制运动模糊 (Motion Blur) - 基于高频帧累积实现录制运动模糊,平滑录制画面的拖影过渡。 - 全时采样曝光:开启后不舍弃多余的高频帧,在时间槽内全部累积后取均值。 - MotionBlurProcessor 独立模块:将帧累积、计算着色器调度、色彩空间转换从 VCD3D11Manager 中抽取为独立的 MotionBlurProcessor 类,通过组合方式使用,解决上帝类问题。 - 轻量 CSBindingGuard 替代 D3D11StateSaver:每次调度只备份/恢复实际修改的 4 个 CS 绑定 (1 Shader + 1 SRV + 1 UAV + 1 CB),减少 90%+ 的 D3D11 API 调用,降低 GPU 开销。 - 线性空间累积:sRGB → Linear 累加 → 均值后转回 sRGB,避免拖影发暗。 2. 画面色彩修复 (Color Range Mismatch) - 修复录制视频在播放器中对比度不足、发灰的问题。 - 将视频流的 color_range 标记改为 AVCOL_RANGE_MPEG (Limited Range),匹配硬件 RGB->YUV 转换的实际输出范围,使播放器正确映射对比度。 3. 关键解耦与UI调整 - 移除 MediaSystem 中所有 CS2 引擎依赖(TimeController、host_framerate 指令下发等)。 - 移除 TwoPassDirector 模块,相关双程音画分离功能将移至 CS2 专属模块中实现。 - Rename '简易运动模糊' to '运动模糊' in UI. - Add '模糊强度 (快门角度)' slider to adjust the shutter parameter. - Update RecordParams to support the new shutter setting. --- .../MediaSystem/CMakeLists.txt | 1 + .../MediaParamManager/RecordParams.hpp | 4 + .../MediaSystem/MediaSystem.cpp | 11 + .../MotionBlurProcessor/CSBindingGuard.hpp | 40 ++++ .../MotionBlurProcessor.cpp | 197 ++++++++++++++++++ .../MotionBlurProcessor.hpp | 51 +++++ .../VCD3D11Manager/VCD3D11Manager.cpp | 172 ++++++++------- .../VCD3D11Manager/VCD3D11Manager.hpp | 18 +- .../VEncodeHelper/VEncodeHelper.cpp | 14 +- 9 files changed, 418 insertions(+), 90 deletions(-) create mode 100644 source/MulNXExtensions/MediaSystem/MotionBlurProcessor/CSBindingGuard.hpp create mode 100644 source/MulNXExtensions/MediaSystem/MotionBlurProcessor/MotionBlurProcessor.cpp create mode 100644 source/MulNXExtensions/MediaSystem/MotionBlurProcessor/MotionBlurProcessor.hpp diff --git a/source/MulNXExtensions/MediaSystem/CMakeLists.txt b/source/MulNXExtensions/MediaSystem/CMakeLists.txt index e80bc304..ad2a1c20 100644 --- a/source/MulNXExtensions/MediaSystem/CMakeLists.txt +++ b/source/MulNXExtensions/MediaSystem/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(MediaSystem MediaSystem.cpp MediaRecorder/MediaRecorder.cpp MediaProcesser/MediaProcesser.cpp + MotionBlurProcessor/MotionBlurProcessor.cpp VideoCapturer/VideoCapturer.cpp AudioCapturer/AudioCapturer.cpp AEncodeHelper/AEncodeHelper.cpp diff --git a/source/MulNXExtensions/MediaSystem/MediaParamManager/RecordParams.hpp b/source/MulNXExtensions/MediaSystem/MediaParamManager/RecordParams.hpp index f19e095f..1ad7624e 100644 --- a/source/MulNXExtensions/MediaSystem/MediaParamManager/RecordParams.hpp +++ b/source/MulNXExtensions/MediaSystem/MediaParamManager/RecordParams.hpp @@ -22,6 +22,10 @@ struct RecordParams { int height = 0; // 0=原生 int captureFpsCap = 60; // 0=不限制 int ringSlots = 6; + + // ── 运动模糊 ── + bool enableMotionBlur = false; // 全时采样累积式运动模糊(不舍弃多余帧) + float motionBlurShutter = 1.0f; // 虚拟快门角度/模糊强度 0.1~1.0 }; struct EncoderCaps { diff --git a/source/MulNXExtensions/MediaSystem/MediaSystem.cpp b/source/MulNXExtensions/MediaSystem/MediaSystem.cpp index 5c9fc820..99ef0583 100644 --- a/source/MulNXExtensions/MediaSystem/MediaSystem.cpp +++ b/source/MulNXExtensions/MediaSystem/MediaSystem.cpp @@ -69,6 +69,17 @@ void MediaSystem::RecordParamsUI() { ImGui::InputInt("高度(0=原生)", &p.height); ImGui::InputInt("捕获帧率(0=不限)", &p.captureFpsCap); if (p.captureFpsCap < 0) p.captureFpsCap = 0; + + if (p.captureFpsCap <= 0) + ImGui::BeginDisabled(); + ImGui::Checkbox("运动模糊", &p.enableMotionBlur); + if (p.enableMotionBlur) { + ImGui::Indent(); + ImGui::SliderFloat("模糊强度 (快门角度)", &p.motionBlurShutter, 0.1f, 1.0f, "%.2f"); + ImGui::Unindent(); + } + if (p.captureFpsCap <= 0) + ImGui::EndDisabled(); } bool MediaSystem::Init() { diff --git a/source/MulNXExtensions/MediaSystem/MotionBlurProcessor/CSBindingGuard.hpp b/source/MulNXExtensions/MediaSystem/MotionBlurProcessor/CSBindingGuard.hpp new file mode 100644 index 00000000..8f02c8d3 --- /dev/null +++ b/source/MulNXExtensions/MediaSystem/MotionBlurProcessor/CSBindingGuard.hpp @@ -0,0 +1,40 @@ +#pragma once +#include + +// Minimal CS binding guard: saves/restores exactly 1 compute shader, +// 1 SRV at slot 0, 1 UAV at slot 0, and 1 constant buffer at slot 0. +// This is all MotionBlurProcessor needs. Compared to a full state +// snapshot (hundreds of D3D11 API calls), this does ~8 API calls total. +class CSBindingGuard { + ID3D11DeviceContext* ctx; + ID3D11ComputeShader* cs = nullptr; + ID3D11ShaderResourceView* srv = nullptr; + ID3D11UnorderedAccessView* uav = nullptr; + ID3D11Buffer* cb = nullptr; + UINT uavCounter = 0; + +public: + CSBindingGuard(ID3D11DeviceContext* context) : ctx(context) { + if (!this->ctx) return; + this->ctx->CSGetShader(&this->cs, nullptr, nullptr); + this->ctx->CSGetShaderResources(0, 1, &this->srv); + this->ctx->CSGetUnorderedAccessViews(0, 1, &this->uav); + this->ctx->CSGetConstantBuffers(0, 1, &this->cb); + } + + ~CSBindingGuard() { + if (!this->ctx) return; + this->ctx->CSSetShader(this->cs, nullptr, 0); + this->ctx->CSSetShaderResources(0, 1, &this->srv); + this->ctx->CSSetUnorderedAccessViews(0, 1, &this->uav, &this->uavCounter); + if (this->cb) this->ctx->CSSetConstantBuffers(0, 1, &this->cb); + + if (this->cs) this->cs->Release(); + if (this->srv) this->srv->Release(); + if (this->uav) this->uav->Release(); + if (this->cb) this->cb->Release(); + } + + CSBindingGuard(const CSBindingGuard&) = delete; + CSBindingGuard& operator=(const CSBindingGuard&) = delete; +}; diff --git a/source/MulNXExtensions/MediaSystem/MotionBlurProcessor/MotionBlurProcessor.cpp b/source/MulNXExtensions/MediaSystem/MotionBlurProcessor/MotionBlurProcessor.cpp new file mode 100644 index 00000000..e84c588e --- /dev/null +++ b/source/MulNXExtensions/MediaSystem/MotionBlurProcessor/MotionBlurProcessor.cpp @@ -0,0 +1,197 @@ +#include "MotionBlurProcessor.hpp" +#include "CSBindingGuard.hpp" +#include +#pragma comment(lib, "d3dcompiler.lib") + +// ── Shader sources ── + +static const char* g_CSAccumulateSrc = R"( +Texture2D SrcTex : register(t0); +RWTexture2D AccumTex : register(u0); +[numthreads(8, 8, 1)] +void main(uint3 DTid : SV_DispatchThreadID) { + float4 c = SrcTex.Load(int3(DTid.xy, 0)); + c.rgb = pow(max(c.rgb, 0.0), 2.2); // sRGB -> Linear + AccumTex[DTid.xy] += c; +} +)"; + +static const char* g_CSFinalizeSrc = R"( +Texture2D AccumTex : register(t0); +RWTexture2D OutTex : register(u0); +cbuffer CB : register(b0) { + float g_InvCount; + float3 g_Pad; +}; +[numthreads(8, 8, 1)] +void main(uint3 DTid : SV_DispatchThreadID) { + float4 c = AccumTex.Load(int3(DTid.xy, 0)); + c.rgb *= g_InvCount; // average + c.rgb = pow(max(c.rgb, 0.0), 1.0 / 2.2); // Linear -> sRGB + OutTex[DTid.xy] = c; +} +)"; + +static DXGI_FORMAT DeSRGB(DXGI_FORMAT f) { + switch (f) { + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; + default: return f; + } +} + +// ── Implementation ── + +ComPtr MotionBlurProcessor::CompileCS(const char* src, const char* entry) { + ComPtr blob, errBlob; + HRESULT hr = D3DCompile(src, strlen(src), nullptr, nullptr, nullptr, + entry, "cs_5_0", 0, 0, &blob, &errBlob); + if (FAILED(hr)) { + if (errBlob) OutputDebugStringA((char*)errBlob->GetBufferPointer()); + return nullptr; + } + return blob; +} + +bool MotionBlurProcessor::Init(ID3D11Device* device, int width, int height, DXGI_FORMAT backbufferFormat) { + if (this->ready) return true; + if (!device || width <= 0 || height <= 0 || backbufferFormat == DXGI_FORMAT_UNKNOWN) return false; + + DXGI_FORMAT unormFmt = DeSRGB(backbufferFormat); + UINT w = (UINT)width, h = (UINT)height; + + // 1. Accumulation texture: R16G16B16A16_FLOAT, UAV + SRV + D3D11_TEXTURE2D_DESC accumDesc = {}; + accumDesc.Width = w; accumDesc.Height = h; + accumDesc.MipLevels = 1; accumDesc.ArraySize = 1; + accumDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; + accumDesc.SampleDesc.Count = 1; + accumDesc.Usage = D3D11_USAGE_DEFAULT; + accumDesc.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE; + if (FAILED(device->CreateTexture2D(&accumDesc, nullptr, &this->accumTex))) return false; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; + uavDesc.Format = accumDesc.Format; + uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; + uavDesc.Texture2D.MipSlice = 0; + if (FAILED(device->CreateUnorderedAccessView(this->accumTex.Get(), &uavDesc, &this->accumUAV))) return false; + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = accumDesc.Format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + if (FAILED(device->CreateShaderResourceView(this->accumTex.Get(), &srvDesc, &this->accumSRV))) return false; + + // 2. Staging texture: non-sRGB UNORM, SRV only (CS reads raw sRGB bytes) + D3D11_TEXTURE2D_DESC stgDesc = accumDesc; + stgDesc.Format = unormFmt; + stgDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + if (FAILED(device->CreateTexture2D(&stgDesc, nullptr, &this->stagingTex))) return false; + + D3D11_SHADER_RESOURCE_VIEW_DESC stgSrv = {}; + stgSrv.Format = unormFmt; + stgSrv.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + stgSrv.Texture2D.MipLevels = 1; + if (FAILED(device->CreateShaderResourceView(this->stagingTex.Get(), &stgSrv, &this->stagingSRV))) return false; + + // 3. Output texture: UAV writable, non-shared + D3D11_TEXTURE2D_DESC outDesc = stgDesc; + outDesc.BindFlags = D3D11_BIND_UNORDERED_ACCESS; + if (FAILED(device->CreateTexture2D(&outDesc, nullptr, &this->outputTex))) return false; + + D3D11_UNORDERED_ACCESS_VIEW_DESC outUav = {}; + outUav.Format = unormFmt; + outUav.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; + outUav.Texture2D.MipSlice = 0; + if (FAILED(device->CreateUnorderedAccessView(this->outputTex.Get(), &outUav, &this->outputUAV))) return false; + + // 4. Constant buffer: 16 bytes (float invCount + float[3] pad) + D3D11_BUFFER_DESC cbDesc = {}; + cbDesc.ByteWidth = 16; + cbDesc.Usage = D3D11_USAGE_DEFAULT; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + if (FAILED(device->CreateBuffer(&cbDesc, nullptr, &this->constBuffer))) return false; + + // 5. Compute shaders + auto accumBlob = this->CompileCS(g_CSAccumulateSrc, "main"); + auto finalBlob = this->CompileCS(g_CSFinalizeSrc, "main"); + if (!accumBlob || !finalBlob) return false; + if (FAILED(device->CreateComputeShader(accumBlob->GetBufferPointer(), accumBlob->GetBufferSize(), nullptr, &this->csAccumulate))) return false; + if (FAILED(device->CreateComputeShader(finalBlob->GetBufferPointer(), finalBlob->GetBufferSize(), nullptr, &this->csFinalize))) return false; + + this->texW = (int)w; this->texH = (int)h; + this->accumCount = 0; + this->ready = true; + return true; +} + +void MotionBlurProcessor::Release() { + this->accumTex.Reset(); this->accumUAV.Reset(); this->accumSRV.Reset(); + this->stagingTex.Reset(); this->stagingSRV.Reset(); + this->outputTex.Reset(); this->outputUAV.Reset(); + this->csAccumulate.Reset(); this->csFinalize.Reset(); + this->constBuffer.Reset(); + this->accumCount = 0; this->texW = this->texH = 0; + this->ready = false; +} + +void MotionBlurProcessor::AccumulateFrame(ID3D11DeviceContext* ctx, + ID3D11Texture2D* backBuffer, + const D3D11_TEXTURE2D_DESC& bbDesc) { + if (!ctx || !this->ready) return; + + // Copy/resolve backbuffer -> staging (backbuffer typically has no SRV bind) + if (bbDesc.SampleDesc.Count > 1) { + ctx->ResolveSubresource(this->stagingTex.Get(), 0, backBuffer, 0, DeSRGB(bbDesc.Format)); + } else { + ctx->CopyResource(this->stagingTex.Get(), backBuffer); + } + + // Save/restore only the CS slots we touch + CSBindingGuard guard(ctx); + ID3D11ShaderResourceView* srvs[] = { this->stagingSRV.Get() }; + ID3D11UnorderedAccessView* uavs[] = { this->accumUAV.Get() }; + UINT uavCounts[] = { 0 }; + ctx->CSSetShader(this->csAccumulate.Get(), nullptr, 0); + ctx->CSSetShaderResources(0, 1, srvs); + ctx->CSSetUnorderedAccessViews(0, 1, uavs, uavCounts); + ctx->CSSetConstantBuffers(0, 0, nullptr); + UINT gx = ((UINT)this->texW + 7) / 8, gy = ((UINT)this->texH + 7) / 8; + ctx->Dispatch(gx, gy, 1); + ++this->accumCount; +} + +ID3D11Texture2D* MotionBlurProcessor::TryFinalize(ID3D11DeviceContext* ctx) { + if (!ctx || !this->ready || this->accumCount <= 0) return nullptr; + + // Update constant buffer: invCount + struct { float invCount; float pad[3]; } cbData; + cbData.invCount = 1.0f / static_cast(this->accumCount); + cbData.pad[0] = cbData.pad[1] = cbData.pad[2] = 0.f; + ctx->UpdateSubresource(this->constBuffer.Get(), 0, nullptr, &cbData, 0, 0); + + // Dispatch CS_Finalize: accumSRV -> outputUAV + { + CSBindingGuard guard(ctx); + ID3D11ShaderResourceView* srvs[] = { this->accumSRV.Get() }; + ID3D11UnorderedAccessView* uavs[] = { this->outputUAV.Get() }; + UINT uavCounts[] = { 0 }; + ID3D11Buffer* cbs[] = { this->constBuffer.Get() }; + ctx->CSSetShader(this->csFinalize.Get(), nullptr, 0); + ctx->CSSetShaderResources(0, 1, srvs); + ctx->CSSetUnorderedAccessViews(0, 1, uavs, uavCounts); + ctx->CSSetConstantBuffers(0, 1, cbs); + UINT gx = ((UINT)this->texW + 7) / 8, gy = ((UINT)this->texH + 7) / 8; + ctx->Dispatch(gx, gy, 1); + } + + // Don't reset accumCount here — caller decides when to reset via ResetAccum() + return this->outputTex.Get(); +} + +void MotionBlurProcessor::ResetAccum(ID3D11DeviceContext* ctx) { + if (!ctx || !this->ready) return; + float clear[4] = { 0.f, 0.f, 0.f, 0.f }; + ctx->ClearUnorderedAccessViewFloat(this->accumUAV.Get(), clear); + this->accumCount = 0; +} diff --git a/source/MulNXExtensions/MediaSystem/MotionBlurProcessor/MotionBlurProcessor.hpp b/source/MulNXExtensions/MediaSystem/MotionBlurProcessor/MotionBlurProcessor.hpp new file mode 100644 index 00000000..3cba1522 --- /dev/null +++ b/source/MulNXExtensions/MediaSystem/MotionBlurProcessor/MotionBlurProcessor.hpp @@ -0,0 +1,51 @@ +#pragma once +#include +#include + +using Microsoft::WRL::ComPtr; + +// Self-contained GPU motion blur processor (frame accumulation). +// Owns all D3D11 resources for the accumulation pipeline. +// VCD3D11Manager uses this via composition, not inheritance. +class MotionBlurProcessor { +public: + bool Init(ID3D11Device* device, int width, int height, DXGI_FORMAT backbufferFormat); + void Release(); + + bool IsReady() const { return this->ready; } + int Width() const { return this->texW; } + int Height() const { return this->texH; } + + // Accumulate one frame: copy backbuffer to staging, dispatch CS_Accumulate. + void AccumulateFrame(ID3D11DeviceContext* ctx, + ID3D11Texture2D* backBuffer, + const D3D11_TEXTURE2D_DESC& bbDesc); + + // Finalize accumulation: dispatch CS_Finalize to output texture. + // Returns the output texture (ready for CopyResource) and the accumulated frame count. + // Caller is responsible for CopyResource from OutputTex to ring buffer or target. + ID3D11Texture2D* TryFinalize(ID3D11DeviceContext* ctx); + + int GetAccumCount() const { return this->accumCount; } + + // Force-reset accumulation without finalizing. + void ResetAccum(ID3D11DeviceContext* ctx); + +private: + bool ready = false; + int texW = 0, texH = 0; + + ComPtr accumTex; // R16G16B16A16_FLOAT + ComPtr accumUAV; + ComPtr accumSRV; + ComPtr stagingTex; // backbuffer copy for CS read + ComPtr stagingSRV; + ComPtr outputTex; // finalized output (non-shared, UAV) + ComPtr outputUAV; + ComPtr csAccumulate; + ComPtr csFinalize; + ComPtr constBuffer; // holds invCount + int accumCount = 0; + + ComPtr CompileCS(const char* src, const char* entry); +}; diff --git a/source/MulNXExtensions/MediaSystem/VCD3D11Manager/VCD3D11Manager.cpp b/source/MulNXExtensions/MediaSystem/VCD3D11Manager/VCD3D11Manager.cpp index 96359ed4..0b393d02 100644 --- a/source/MulNXExtensions/MediaSystem/VCD3D11Manager/VCD3D11Manager.cpp +++ b/source/MulNXExtensions/MediaSystem/VCD3D11Manager/VCD3D11Manager.cpp @@ -5,8 +5,8 @@ bool VCD3D11Manager::Init() { this->pGraphicsManager = this->FindModule("GraphicsManager"); (*this) - .SubscribeSync("Hook/Present/First", [this](MulNX::Message& msg) {this->OnPresentFirst(msg);}) - .SubscribeSync("Hook/BeforePresent", [this](MulNX::Message& msg) {this->CopyTexture();}); + .SubscribeSync("Hook/Present/First", [this](MulNX::Message& msg) { this->OnPresentFirst(msg); }) + .SubscribeSync("Hook/BeforePresent", [this](MulNX::Message& msg) { this->CopyTexture(); }); return true; } @@ -22,14 +22,12 @@ void VCD3D11Manager::SetRingCapacity(int n) { void VCD3D11Manager::SetRecordStart(std::chrono::steady_clock::time_point t) { this->recordStartTime = t; this->lastSlot = -1; + this->mbAccumSlot = -1; } bool VCD3D11Manager::CreateSlot(const D3D11_TEXTURE2D_DESC& sharedDesc, RingSlot& slot) { HRESULT hr = this->pGraphicsManager->pd3dDevice->CreateTexture2D(&sharedDesc, nullptr, &slot.rawTex.pTex); - if (FAILED(hr)) { - this->LogError("环形槽位共享纹理创建失败"); - return false; - } + if (FAILED(hr)) { this->LogError("环形槽位共享纹理创建失败"); return false; } ComPtr pDXGIRes; hr = slot.rawTex.pTex.As(&pDXGIRes); @@ -50,7 +48,8 @@ bool VCD3D11Manager::CreateSlot(const D3D11_TEXTURE2D_DESC& sharedDesc, RingSlot } void VCD3D11Manager::OnPresentFirst(MulNX::Message& msg) { - // 从原设备反推参数,创建兼容录制设备 + this->mbProcessor.Release(); + D3D_FEATURE_LEVEL originalLevel = this->pGraphicsManager->pd3dDevice->GetFeatureLevel(); ComPtr dxgiDevice; @@ -62,48 +61,31 @@ void VCD3D11Manager::OnPresentFirst(MulNX::Message& msg) { UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; ComPtr d3dDebug; if (SUCCEEDED(this->pGraphicsManager->pd3dDevice->QueryInterface( - __uuidof(ID3D11Debug), (void**)d3dDebug.GetAddressOf()))) { + __uuidof(ID3D11Debug), (void**)d3dDebug.GetAddressOf()))) flags |= D3D11_CREATE_DEVICE_DEBUG; - } D3D_DRIVER_TYPE driverType = pAdapter ? D3D_DRIVER_TYPE_UNKNOWN : D3D_DRIVER_TYPE_WARP; HRESULT hr = D3D11CreateDevice( - pAdapter.Get(), - driverType, - nullptr, - flags, - &originalLevel, 1, - D3D11_SDK_VERSION, - &this->pDevice, nullptr, &this->pContext - ); - if (FAILED(hr)) { - this->LogError("捕获用D3D11设备创建失败"); - return; - } + pAdapter.Get(), driverType, nullptr, flags, + &originalLevel, 1, D3D11_SDK_VERSION, + &this->pDevice, nullptr, &this->pContext); + if (FAILED(hr)) { this->LogError("捕获用D3D11设备创建失败"); return; } this->LogSucc("捕获用D3D11设备创建成功"); - // 获取原设备后台缓冲区描述 ComPtr pRTV; this->pGraphicsManager->pd3dContext->OMGetRenderTargets(1, pRTV.GetAddressOf(), nullptr); - if (!pRTV) { - this->LogError("无法获取原设备渲染目标"); - return; - } + if (!pRTV) { this->LogError("无法获取原设备渲染目标"); return; } D3D11_RENDER_TARGET_VIEW_DESC rtvDesc; pRTV->GetDesc(&rtvDesc); ComPtr pBBRes; pRTV->GetResource(&pBBRes); - if (!pBBRes) { - this->LogError("无法获取后台缓冲区资源"); - return; - } + if (!pBBRes) { this->LogError("无法获取后台缓冲区资源"); return; } D3D11_TEXTURE2D_DESC bbDesc; static_cast(pBBRes.Get())->GetDesc(&bbDesc); - // 共享纹理描述模板:强制非 MSAA、使用后备缓冲的实际格式 D3D11_TEXTURE2D_DESC sharedDesc = {}; sharedDesc.Width = bbDesc.Width; sharedDesc.Height = bbDesc.Height; @@ -117,7 +99,6 @@ void VCD3D11Manager::OnPresentFirst(MulNX::Message& msg) { sharedDesc.CPUAccessFlags = 0; sharedDesc.Usage = D3D11_USAGE_DEFAULT; - // 记录源参数:宽高用后备缓冲,像素格式用 RTV 格式(保证可被 DXGIFormatToAvPixelFormat 识别) this->srcWidth = (int)bbDesc.Width; this->srcHeight = (int)bbDesc.Height; this->srcDxgiFormat = rtvDesc.Format; @@ -126,27 +107,20 @@ void VCD3D11Manager::OnPresentFirst(MulNX::Message& msg) { static_cast(bbDesc.Format), static_cast(rtvDesc.Format))); - // 创建环形队列(容量优先取自 MediaParamManager,未找到则用默认) int n = this->ringCapacity; auto* params = this->FindModule("MediaParamManager"); - if (params && params->Params().ringSlots >= 2) { - n = params->Params().ringSlots; - } + if (params && params->Params().ringSlots >= 2) n = params->Params().ringSlots; if (n < 2) n = 6; this->ring.clear(); this->ring.resize(n); int created = 0; for (int i = 0; i < n; ++i) { - if (this->CreateSlot(sharedDesc, this->ring[i])) { - ++created; - } else { - this->LogWarning(std::format("环形槽位 {} 创建失败", i)); - } + if (this->CreateSlot(sharedDesc, this->ring[i])) ++created; + else this->LogWarning(std::format("环形槽位 {} 创建失败", i)); } if (created < 2) { this->LogError("环形队列有效槽位不足(<2),录制将不可用"); - this->ringReady.store(false); - return; + this->ringReady.store(false); return; } this->ringCapacity = created; this->writeIdx.store(0); @@ -160,17 +134,78 @@ void VCD3D11Manager::CopyTexture() { if (!this->runFlag1.load(std::memory_order_acquire)) return; if (!this->ringReady.load(std::memory_order_acquire)) return; - // 基于时间槽的帧率上限:捕获落在当前时间槽的首帧,并量化 PTS 为槽边界 int cap = this->captureFpsCap.load(std::memory_order_acquire); - int64_t quantizedPtsUs = -1; // -1 表示不量化,使用实际 now + + auto* params = this->FindModule("MediaParamManager"); + bool mbOn = (params && params->Params().enableMotionBlur && cap > 0); + float shutter = 1.0f; + + int64_t elapsedUs = 0, slot = 0; if (cap > 0) { auto now = std::chrono::steady_clock::now(); - int64_t elapsedUs = std::chrono::duration_cast( - now - this->recordStartTime).count(); - int64_t slot = elapsedUs / this->minIntervalUs; - if (slot == this->lastSlot) { - return; // 同一时间槽内不再重复捕获 + elapsedUs = std::chrono::duration_cast(now - this->recordStartTime).count(); + if (elapsedUs < 0) elapsedUs = 0; + slot = elapsedUs / this->minIntervalUs; + } + + // ── Motion Blur path ── + if (mbOn && !this->mbProcessor.IsReady()) { + ID3D11Device* dev = this->pGraphicsManager->pd3dDevice; + bool ok = this->mbProcessor.Init(dev, this->srcWidth, this->srcHeight, this->srcDxgiFormat); + if (ok) { + this->LogSucc(std::format("MotionBlurProcessor 已初始化: {}x{}", this->srcWidth, this->srcHeight)); + } else { + mbOn = false; } + } + if (mbOn) { + ID3D11DeviceContext* ctx = this->pGraphicsManager->pd3dContext; + + if (slot > this->mbAccumSlot && this->mbProcessor.GetAccumCount() > 0) { + ID3D11Texture2D* outputTex = this->mbProcessor.TryFinalize(ctx); + if (outputTex) { + int64_t ptsSlot = this->mbAccumSlot < 0 ? 0 : this->mbAccumSlot; + int64_t ptsUs = ptsSlot * this->minIntervalUs; + + int target = this->writeIdx.load(std::memory_order_acquire); + RingSlot& rs = this->ring[target]; + + HRESULT hr = rs.rawTex.pMutex->AcquireSync(0, 0); + if (hr != S_OK) { + this->droppedFrames.fetch_add(1, std::memory_order_relaxed); + } else { + if (rs.hasNewFrame.load(std::memory_order_relaxed)) + this->droppedFrames.fetch_add(1, std::memory_order_relaxed); + ctx->CopyResource(rs.rawTex.pTex.Get(), outputTex); + ctx->Flush(); + rs.captureTime.store(this->recordStartTime + std::chrono::microseconds(ptsUs), std::memory_order_release); + hr = rs.rawTex.pMutex->ReleaseSync(1); + if (FAILED(hr)) this->LogError("MB: ReleaseSync(1) failed"); + rs.hasNewFrame.store(true, std::memory_order_release); + this->writeIdx.store((target + 1) % this->ringCapacity, std::memory_order_release); + } + } + this->mbProcessor.ResetAccum(ctx); + } + if (slot > this->mbAccumSlot) this->mbAccumSlot = slot; + this->lastSlot = slot; + + float slotProgress = (float)(elapsedUs % this->minIntervalUs) / (float)this->minIntervalUs; + if (slotProgress >= (1.0f - shutter)) { + ComPtr backBuffer; + if (SUCCEEDED(this->pGraphicsManager->pSwapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer)))) { + D3D11_TEXTURE2D_DESC bbDescR; + backBuffer->GetDesc(&bbDescR); + this->mbProcessor.AccumulateFrame(ctx, backBuffer.Get(), bbDescR); + } + } + return; + } + + // ── Fallback: direct copy path ── + int64_t quantizedPtsUs = -1; + if (cap > 0) { + if (slot == this->lastSlot) return; this->lastSlot = slot; quantizedPtsUs = slot * this->minIntervalUs; } @@ -180,50 +215,35 @@ void VCD3D11Manager::CopyTexture() { if (FAILED(hr)) return; int target = this->writeIdx.load(std::memory_order_acquire); - RingSlot& slot = this->ring[target]; + RingSlot& rs = this->ring[target]; - // 非阻塞获取写锁(key=0 表示可写);若录制端正在读该槽位则放弃本帧,避免阻塞 Present - hr = slot.rawTex.pMutex->AcquireSync(0, 0); - if (hr != S_OK) { - // WAIT_TIMEOUT = 未拿到锁(录制端在读),FAILED = 其他错误,均丢弃当前新帧 - this->droppedFrames.fetch_add(1, std::memory_order_relaxed); - return; - } + hr = rs.rawTex.pMutex->AcquireSync(0, 0); + if (hr != S_OK) { this->droppedFrames.fetch_add(1, std::memory_order_relaxed); return; } - // 若该槽位仍有未读数据,覆盖即丢弃最旧帧 - if (slot.hasNewFrame.load(std::memory_order_relaxed)) { + if (rs.hasNewFrame.load(std::memory_order_relaxed)) this->droppedFrames.fetch_add(1, std::memory_order_relaxed); - } - // 获取后备缓冲描述以确定是否 MSAA D3D11_TEXTURE2D_DESC bbDescR; backBuffer->GetDesc(&bbDescR); - // 执行拷贝,并在拷贝完成时刻打 PTS if (bbDescR.SampleDesc.Count > 1) { this->pGraphicsManager->pd3dContext->ResolveSubresource( - slot.rawTex.pTex.Get(), 0, backBuffer.Get(), 0, + rs.rawTex.pTex.Get(), 0, backBuffer.Get(), 0, static_cast(this->srcDxgiFormat)); } else { this->pGraphicsManager->pd3dContext->CopyResource( - slot.rawTex.pTex.Get(), backBuffer.Get()); + rs.rawTex.pTex.Get(), backBuffer.Get()); } - // 确保 GPU 拷贝命令已提交,否则 ReleaseSync 后读者可能读到未完成的数据(雪花屏) this->pGraphicsManager->pd3dContext->Flush(); - // PTS:有帧率上限时量化为时间槽边界,否则取实际 now if (quantizedPtsUs >= 0) { - slot.captureTime.store(this->recordStartTime + std::chrono::microseconds(quantizedPtsUs), - std::memory_order_release); + rs.captureTime.store(this->recordStartTime + std::chrono::microseconds(quantizedPtsUs), std::memory_order_release); } else { - slot.captureTime.store(std::chrono::steady_clock::now(), std::memory_order_release); - } - - hr = slot.rawTex.pMutex->ReleaseSync(1); - if (FAILED(hr)) { - this->LogError("ReleaseSync(1) 失败"); + rs.captureTime.store(std::chrono::steady_clock::now(), std::memory_order_release); } - slot.hasNewFrame.store(true, std::memory_order_release); + hr = rs.rawTex.pMutex->ReleaseSync(1); + if (FAILED(hr)) this->LogError("ReleaseSync(1) failed"); + rs.hasNewFrame.store(true, std::memory_order_release); this->writeIdx.store((target + 1) % this->ringCapacity, std::memory_order_release); } diff --git a/source/MulNXExtensions/MediaSystem/VCD3D11Manager/VCD3D11Manager.hpp b/source/MulNXExtensions/MediaSystem/VCD3D11Manager/VCD3D11Manager.hpp index e4ec58b3..e760e527 100644 --- a/source/MulNXExtensions/MediaSystem/VCD3D11Manager/VCD3D11Manager.hpp +++ b/source/MulNXExtensions/MediaSystem/VCD3D11Manager/VCD3D11Manager.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include @@ -9,10 +10,9 @@ class MidTex { ComPtr pMutex; }; -// 环形队列的单个槽位:一对跨设备共享纹理 + 新帧标志 + 捕获时刻(PTS) struct RingSlot { - MidTex rawTex; // 原设备(游戏)上的共享纹理 - MidTex shareTex; // 录制设备上的共享纹理 + MidTex rawTex; + MidTex shareTex; std::atomic hasNewFrame{ false }; std::atomic captureTime; static_assert(std::atomic::is_always_lock_free, "captureTime must be lock-free"); @@ -38,34 +38,32 @@ struct RingSlot { class VCD3D11Manager final : public MediaModuleBase { MulNX::GraphicsManager* pGraphicsManager = nullptr; - // 可选的捕获帧率上限(0=不限制,全帧捕获) std::atomic captureFpsCap{ 0 }; - // 基于时间槽的帧率限制状态(录制启动时由 MediaRecorder 设置) std::chrono::steady_clock::time_point recordStartTime; - int64_t minIntervalUs = 16667; // captureFpsCap 换算(µs) - int64_t lastSlot = -1; // 上次捕获所在时间槽序号 + int64_t minIntervalUs = 16667; + int64_t lastSlot = -1; + int64_t mbAccumSlot = -1; void OnPresentFirst(MulNX::Message& msg); void CopyTexture(); bool CreateSlot(const D3D11_TEXTURE2D_DESC& desc, RingSlot& slot); + MotionBlurProcessor mbProcessor; + public: bool Init() override; - // 环形队列 std::vector ring; int ringCapacity = 6; std::atomic writeIdx{ 0 }; std::atomic readIdx{ 0 }; std::atomic droppedFrames{ 0 }; std::atomic ringReady{ false }; - // runFlag1 沿用基类 ModuleComponents::runFlag1,由 VideoCapturer 置位以启用拷贝 ComPtr pDevice; ComPtr pContext; - // 源纹理参数(创建环形队列时记录,供编码器配置使用) int srcWidth = 0; int srcHeight = 0; DXGI_FORMAT srcDxgiFormat = DXGI_FORMAT_UNKNOWN; diff --git a/source/MulNXExtensions/MediaSystem/VEncodeHelper/VEncodeHelper.cpp b/source/MulNXExtensions/MediaSystem/VEncodeHelper/VEncodeHelper.cpp index f983ce6c..c4c2cce9 100644 --- a/source/MulNXExtensions/MediaSystem/VEncodeHelper/VEncodeHelper.cpp +++ b/source/MulNXExtensions/MediaSystem/VEncodeHelper/VEncodeHelper.cpp @@ -82,15 +82,21 @@ bool VEncodeHelper::SetupHwContext(ID3D11Device* device, int w, int h) { this->hwInputPixFmt = AV_PIX_FMT_D3D11; } else if (enc.find("nvenc") != std::string::npos) { this->hwInputPixFmt = AV_PIX_FMT_CUDA; - if (av_hwdevice_ctx_create_derived(&this->hwTargetDeviceRef, AV_HWDEVICE_TYPE_CUDA, this->hwD3D11DeviceRef, 0) < 0) { - this->hwInputPixFmt = AV_PIX_FMT_NV12; return false; + int ret = av_hwdevice_ctx_create_derived(&this->hwTargetDeviceRef, AV_HWDEVICE_TYPE_CUDA, this->hwD3D11DeviceRef, 0); + if (ret < 0) { + this->LogWarning(std::format("CUDA 上下文衍生失败({}), 尝试使用 D3D11VA 直传模式", ret)); + this->hwInputPixFmt = AV_PIX_FMT_D3D11; + this->hwTargetDeviceRef = nullptr; + this->hwTargetFramesRef = nullptr; + return true; } this->hwTargetFramesRef = av_hwframe_ctx_alloc(this->hwTargetDeviceRef); if (!this->hwTargetFramesRef) { av_buffer_unref(&this->hwTargetDeviceRef); this->hwInputPixFmt = AV_PIX_FMT_NV12; return false; } auto* cfc = reinterpret_cast(this->hwTargetFramesRef->data); cfc->format = AV_PIX_FMT_CUDA; cfc->sw_format = AV_PIX_FMT_NV12; cfc->width = w; cfc->height = h; cfc->initial_pool_size = 6; - if (av_hwframe_ctx_init(this->hwTargetFramesRef) < 0) { + if ((ret = av_hwframe_ctx_init(this->hwTargetFramesRef)) < 0) { + this->LogWarning(std::format("CUDA FrameCtx失败({})", ret)); av_buffer_unref(&this->hwTargetFramesRef); av_buffer_unref(&this->hwTargetDeviceRef); this->hwInputPixFmt = AV_PIX_FMT_NV12; return false; } @@ -132,7 +138,7 @@ bool VEncodeHelper::OpenEncoder(av::FormatContext* oCtx, const RecordParams& rp, this->encoder.setGlobalQuality(static_cast(rp.cq * FF_QP2LAMBDA)); if (auto* raw = this->encoder.raw()) { - raw->color_range = AVCOL_RANGE_JPEG; + raw->color_range = AVCOL_RANGE_MPEG; // 采用 Limited Range (16-235) 匹配硬件默认转换行为,解决发灰问题 raw->colorspace = AVCOL_SPC_BT709; raw->color_primaries = AVCOL_PRI_BT709; raw->color_trc = AVCOL_TRC_BT709;