前情提要

前篇:https://www.cnblogs.com/judgeou/p/14728617.html

上一集我们攻略了 Direct3D 11 渲染,充分发挥现代 GPU 的性能。这一集比较轻松,主要是完善剩下需要的功能。

利用垂直同步控制播放速度

正确控制播放速度其实有非常多的方式,比较常见的是将视频和音频同步,或者与外部时钟同步。但这里我要介绍一种比较少见的方式,可以在没有音频的时候使用,就是利用显示屏的垂直同步信号来同步视频画面。

当调用 IDXGISwapChain::Present 并且第一个参数为 1 时,会阻塞线程,直到屏幕完成一帧画面的显示,发送垂直同步信号,才会返回继续执行,利用这一特性,来完成播放速度的正确处理。

假设我们的屏幕刷新率是 60Hz,视频是 30fps,那么处理起来很简单,每 2 个呈现周期,更新一次视频画面即可,可以保证每一帧画面的出现,时机都恰到好处。但如果视频是 24fps,就需要每 2.5 个呈现周期更新一次画面,导致你的视频画面几乎在绝大多数时候会与正确的播放时机错开,你能做的,只能是这帧慢了,下一帧就快点,这一帧快了,下一帧就慢点。

// 获取视频帧率
double GetFrameFreq(const DecoderParam& param) {
	auto avg_frame_rate = param.fmtCtx->streams[param.videoStreamIndex]->avg_frame_rate;
	auto framerate = param.vcodecCtx->framerate;

	if (avg_frame_rate.num > 0) {
		return (double)avg_frame_rate.num / avg_frame_rate.den;
	}
	else if (framerate.num > 0) {
		return (double)framerate.num / framerate.den;
	}
}
// ...

DEVMODE devMode = {};
devMode.dmSize = sizeof(devMode);
EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &devMode);
// 屏幕刷新率
auto displayFreq = devMode.dmDisplayFrequency;

// 记录屏幕呈现了多少帧
int displayCount = 1;
// 记录视频播放了多少帧
int frameCount = 1;

MSG msg;
while (1) {
	// ...
	if (hasMsg) {
		// ...
	}
	else {
		double frameFreq = GetFrameFreq(decoderParam);
		double freqRatio = displayFreq / frameFreq;
		double countRatio = (double)displayCount / frameCount;

		if (freqRatio < countRatio) {
			auto frame = RequestFrame(decoderParam);
			UpdateVideoTexture(frame, scenceParam, decoderParam);
			frameCount++;
			av_frame_free(&frame);
		}

		Draw(d3ddeivce.Get(), d3ddeviceCtx.Get(), swapChain.Get(), scenceParam);

		swapChain->Present(1, 0);
		displayCount++;
	}
}

用 displayCount 和 frameCount 分别记录渲染的帧数和播放的帧数,这两个数字的比值(countRatio)应当与 屏幕刷新率 和 视频帧率 的比值(freqRatio )尽可能接近,所以判断一旦 freqRatio < countRatio 就解码下一帧视频,否则就继续渲染上一次的画面。经过这个改动后,低于或等于屏幕刷新率的视频就可以正常播放了。

但是如果是高刷新率的视频,比如120fps的视频,此时你的屏幕是60帧,那么就要放弃渲染一些帧。

// ...

double frameFreq = GetFrameFreq(decoderParam);
double freqRatio = displayFreq / frameFreq;
double countRatio = (double)displayCount / frameCount;

while (freqRatio < countRatio) {
	auto frame = RequestFrame(decoderParam);
	frameCount++;
	countRatio = (double)displayCount / frameCount;

	if (freqRatio >= countRatio) {
		UpdateVideoTexture(frame, scenceParam, decoderParam);
	}
	av_frame_free(&frame);
}

把原来的 if (freqRatio < countRatio) 改为 while (freqRatio < countRatio),这样视频解码一帧后会再触发判断,如果是120fps视频则继续解码下一帧并跳过 UpdateVideoTexture。

这样不管是什么帧率的视频,在什么刷新率的屏幕上都可以以正确的速度播放了。

注意:通过 EnumDisplaySettings 获取的屏幕刷新率其实是不太精确的,实际刷新率通常不是整数,而是带小数点,这里就不深究了,有兴趣的看 DwmGetCompositionTimingInfo

保持画面比例

先把windows窗体样式改回 WS_OVERLAPPEDWINDOW,方便我们对窗口进行任意缩放。

auto window = CreateWindow(className, L"Hello World 标题", WS_OVERLAPPEDWINDOW, 100, 100, clientWidth, clientHeight, NULL, NULL, hInstance, NULL);

想要保持画面比例,就要根据当前窗口的 width height 对四边形进行缩放调整,要么变胖变瘦,要么变高变矮,这些都属于缩放变换,那么四边形每一个顶点要如何变化呢?答案就是把每一个顶点坐标,乘以相对应的缩放矩阵即可。其他的诸如平移、旋转等也是通过与矩阵相乘实现的:

image

当物体的顶点数量十分庞大时,在CPU做矩阵变换太耗费时间了,GPU就非常适合干这个活儿。尽管我们只有4个点,但这里还是使用业界标准做法,把矩阵传送到图形管线,在着色器里面对各个顶点进行矩阵乘法。

这里要用上微软提供的库:DirectXMath,已经包含在 Windows SDK 里了,先引入必要的头文件:

#include <DirectXMath.h>
namespace dx = DirectX;

相关函数是在命名空间 DirectX 下的,为了写起来方便,就用 dx 别名代替。

为了把矩阵放进管线,需要一个新的 ID3D11Buffer。

struct ScenceParam {
// ...
	ComPtr<ID3D11Buffer> pConstantBuffer;
// ...
	int viewWidth;
	int viewHeight;
};

在结构体 ScenceParam 添加 ComPtr<ID3D11Buffer> pConstantBuffer,并且添加两个属性 viewWidth viewHeight,保存当前窗口大小。

修改 InitScence 函数,添加创建常量缓冲区的代码:

void InitScence(ID3D11Device* device, ScenceParam& param, const DecoderParam& decoderParam) {
// ...

	// 常量缓冲区
	auto constant = dx::XMMatrixScaling(1, 1, 1);
	constant = dx::XMMatrixTranspose(constant);
	D3D11_BUFFER_DESC cbd = {};
	cbd.Usage = D3D11_USAGE_DYNAMIC;
	cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
	cbd.ByteWidth = sizeof(constant);
	cbd.StructureByteStride = 0;
	D3D11_SUBRESOURCE_DATA csd = {};
	csd.pSysMem = &constant;
	
	device->CreateBuffer(&cbd, &csd, &param.pConstantBuffer);
// ...
}

因为需要每一帧都更新 pConstantBuffer 的内容,所以 Usage 必须要是 D3D11_USAGE_DYNAMIC,CPUAccessFlags 必须是 D3D11_CPU_ACCESS_WRITE。初始的时候,先给一个 缩放(1, 1, 1) 矩阵,其实就相当于啥也没变,这里注意 XMMatrixTranspose 函数,他把矩阵的行和列置换了,为什么要干这个呢,因为GPU看待矩阵行列的形式反了过来,CPU他是一行一行的读,GPU是一列一列的读。所以传送到GPU前需要处理一下。不过,缩放矩阵就算你不置换,结果都是正常的

版权声明:本文为judgeou原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/judgeou/p/14746051.html