一、简介
现代科技网络日益发达,视频已经成为人们生活中不可或缺的一部分。随着互联网和移动设备的普及,视频内容在传播和分享方面发挥着越来越重要的作用。从社交媒体到在线教育,从数字广告到远程工作,视频已经成为人们获取信息、娱乐和交流的主要方式之一。在这样一个视频日益普及的超视频时代,开发一套跨语言、跨设备、跨系统的多媒体处理框架显得尤为重要,这样的框架可以为开发人员提供统一的解决方案,帮助他们在不同的平台上快速、高效地处理多媒体内容,从而提供一致的用户体验和功能,是迎接未来的必然趋势。
在当今数字化的世界中,Windows 平台的重要性和关键性无可置疑。作为普通用户的首要选择,Windows 提供了广泛的硬件和软件支持,为用户提供了丰富多彩的体验。特别是在多媒体处理领域,Windows 平台凭借其强大的生态系统和稳定的性能,基本是普通用户的首选。Windows 平台拥有庞大而完善的 DirectX 能力体系,这使得在Windows 环境下可以很方便地实现通过 GPU 加速图像视频处理的性能,这种强大的图形处理能力可以更高效地处理和渲染视频、音频等多媒体内容。特别是对于游戏主播、视频编辑等相关领域的从业者,Windows 平台提供了一个稳定而强大的开发环境,为他们的创作和工作带来了极大的便利和效率。因此,开发一套兼容 Windows 平台的多媒体处理框架具有重要的意义。这不仅可以满足普通用户对于多媒体内容的需求,还可以为专业从业者提供强大的工具和支持。无论是游戏行业、直播行业还是视频编辑领域,都可以受益于这样一套高效、稳定的多媒体处理框架,为用户带来更优质的体验和服务。
基于以上两个前提,2023 年 8 月 22 日,火山引擎视频云与NVIDIA正式开源多媒体处理框架 Babit Multimedia Framework (以下统称 BMF 框架),BMF 在 Windows 侧对齐 Linux,目前已经打通了框架的编译、构建、同时支持模块自定义开发,在字节跳动内部,BMF 在 Win 侧已集成多种使用 CPU/GPU 的图像处理算法,服务于抖音直播伴侣业务,目前已有 5 个算法已被成功集成,BMF 框架作为搭建算法与业务的桥梁,通过自定义模块实现算法逻辑与业务的完全解耦,其内部可以很方便地在 Win 侧集成不同图像处理算法。本文将沿三个步骤,全面介绍 BMF 框架在 Windows 端的能力建设与技术实践,首先介绍 BMF 框架在 Windows 环境下如何配置与编译,其次介绍如何在 Windows 环境配置 BMF 开发环境,并展示一个简单 Python 模块的运行过程,最后展示一个基于 DirectX 的全链路图像缩放模块的开发与部署案例,展示 BMF 在 Windows 端友好的兼容性和强大的功能适配能力,助您在 Windows 下玩转BMF!
二、编译与构建
编译 BMF 框架需要依赖以下环境:
- MSYS2。提供了一个基于开源软件的本机构建环境,可以在 Windows 上用 Linux 的方式使用多种不同的环境和工具来执行不同的任务。
- CMake。管理框架构建过程,推荐版本 3.27。
- Visual Studio 2013 - 2022。BMF 在 Win 端选用兼容性较好的 msvc 编译工具链,目前支持版本 2013 - 2022。
以上三个依赖是必须项,下面还有 2 个依赖可供选择是否打开相关的框架能力:
- Python 3.7 - 3.10。用于编译 BMF Python SDK,如果不提供,则框架无法编译 Python 相关的调用能力
- FFmpeg 4.4 - 5.1。用于编译 BMF built-in modules(ffmpeg_decoder、ffmpeg_encoder、ffmpeg_filter),如果不提供,默认取消编译相关产物
本文在介绍编译过程时,默认会打开以上两个选项,实现一次全链路的编译构建,下面用图文方式介绍 BMF 框架编译过程:
- 打开 Visual Studio 的命令提示环境,建议以管理员方式
- 找到 MSYS2 的安装目录,执行命令: .\msys2_shell.cmd -use-full-path,携带宿主机环境进入 msys2(注意:这里需要保证 CMake 工具已经成功配置进系统环境变量,本文的实验环境默认配置了 CMake 3.27、FFmpeg 4.4、Python 3.7.9 的环境变量)。
- 克隆 BMF 项目,在根目录运行 build_win_lite 脚本,项目设有以下编译控制选项:
Build Options:
--msvc 设定 msvc 版本,包括[2013, 2015, 2017, 2019, 2022]
bmf_ffmpeg 控制是否集成 FFmpeg 相关能力并编译 built-in module
--preset 编译配置,包括[x86-Debug, x86-Release, x64-Debug, x64-Release]
注意:如果您需要编译 FFmpeg 相关的能力,且您本地的 CMake 版本高于等于 3.28,你还需要设置 ffmpeg 的 include 目录,命令如下:
export INCLUDE="$INCLUDE;C:\path\to\include_for_ffmpeg"
以 msvc 2022 编译 x64-Debug 版本为例,脚本执行命令:
./build_win_lite.sh --msvc=2022 --preset=x64-Debug bmf_ffmpeg
执行后会在项目目录的 build_win_lite/x64-Debug 目录生成 BMF.sln 解决方案
之后,您可以双击 sln 文件使用 Visual Studio 友好的界面进行项目构建和编译,您也可以使用以下 CMake 命令直接通过命令行进行项目构建:
cmake --build build_win_lite/x64-Debug --config Debug --target ALL_BUILD
至此,您便可以在 Windows 环境中完成对 BMF 框架的编译与构建,在 Visual Studio 的编译过程如图所示
三、开发环境配置与模块运行
本章介绍如何搭建 BMF 开发环境,继上文所述,当我们完成 BMF 框架的编译构建后,会生成 output 文件夹,我们需要将 bin 目录与 lib 目录配置进系统环境变量,与此同时,BMF 开发环境还需依赖一个 Win 相关的依赖集合 win_rootfs(https://github.com/BabitMF/bmf/releases/download/files/win_rootfs.tar.gz),需配置环境变量,如图所示:
配置完 BMF 环境变量后,我们需要重启 msys2 环境,目的是让 BMF 的环境变量生效,下面展示如何运行一个 Python Module 的测试程序:test_customize_module。首先,需要将 msys2 当前目录切换至编译产物的上级目录,设置一些 msys2 环境的环境变量,配置 BMF 框架的 Python 运行环境
export PYTHONHOME="$(dirname "$(which python)")"
export PYTHONPATH=$(pwd)/output/bmf/lib:$(pwd)/output
配置完毕后,进入 python 环境应该可以正常 import bmf 框架,如图所示
我们要运行的 customize_module 文件在 output/test/customize_module 目录下的 my_module.py 文件,模块的实现代码如下:
from bmf import Module, Log, LogLevel, InputType, ProcessResult, Packet, Timestamp, scale_av_pts, av_time_base, \
BmfCallBackType, VideoFrame, AudioFrame
class my_module(Module):
def __init__(self, node, optinotallow=None):
self.node_ = node
self.option_ = option
pass
def process(self, task):
for (input_id, input_packets) in task.get_inputs().items():
## output queue
output_packets = task.get_outputs()[input_id]
while not input_packets.empty():
pkt = input_packets.get()
## process EOS
if pkt.timestamp == Timestamp.EOF:
Log.log_node(LogLevel.DEBUG, task.get_node(),
"Receive EOF")
output_packets.put(Packet.generate_eof_packet())
task.timestamp = Timestamp.DONE
return ProcessResult.OK
## copy input packet to output
if pkt.defined() and pkt.timestamp != Timestamp.UNSET:
output_packets.put(pkt)
return ProcessResult.OK
可以看到,模块仅仅将帧从输入队列取出,不做任何处理,直接传递至输出队列,我们将要执行的测试程序 test_customize_module.py 的实现如下:
import sys
import time
import unittest
sys.path.append("../../..")
sys.path.append("../../c_module_sdk/build/bin/lib")
import bmf
import os
if os.name == 'nt':
## We redefine timeout_decorator on windows
class timeout_decorator:
@staticmethod
def timeout(*args, **kwargs):
return lambda f: f ## return a no-op decorator
else:
import timeout_decorator
sys.path.append("../../test/")
from base_test.base_test_case import BaseTestCase
from base_test.media_info import MediaInfo
class TestCustomizeModule(BaseTestCase):
@timeout_decorator.timeout(secnotallow=120)
def test_customize_module(self):
input_video_path = "../../files/big_bunny_10s_30fps.mp4"
output_path = "./output.mp4"
expect_result = '|1080|1920|10.0|MOV,MP4,M4A,3GP,3G2,MJ2|1783292|2229115|h264|' \
'{"fps": "30.0662251656"}'
self.remove_result_data(output_path)
(bmf.graph().decode({'input_path': input_video_path
})['video'].module('my_module').encode(
None, {
"output_path": output_path
}).run())
self.check_video_diff(output_path, expect_result)
if __name__ == '__main__':
unittest.main()
在测试程序的 33 - 37 行中,我们构建了一个 BMF Graph,首先对输入视频进行解码,随后调起事先我们写好的 Python 模块 my_module 对解码后的视频帧进行一次处理,最后调用 encode 模块对视频帧进行编码,产出输出文件,剩余部分是框架使用 Google Test 框架所进行的一些转码指标的验证,这里不深入追溯,程序使用的输入视频是 BMF 框架为集成测试预先准备好的一组测试资源包,您可以通过https://github.com/BabitMF/bmf/releases/download/files/files.tar.gz 进行下载和使用,本文将使用这个资源包,下载命令如下
(cd output && wget https://github.com/BabitMF/bmf/releases/download/files/files.tar.gz && tar xvf files.tar.gz && rm -rf files.tar.gz)
至此,所有执行该程序的前置依赖均已准备完毕,切换到 customize_module 目录执行程序
cd test/customize_module
python test_customize_module.py
程序执行结果如图所示,可以看到在本地成功产出了 output.mp4 文件
四、实践案例
本章将从 0 到 1 带你实现一个 RGBA 图像的 GPU 缩放功能,基于 DirectX Compute Shader 机制完成算法能力建设,并使用 BMF 框架的模块机制将算法能力封装进 BMF 模块中,同时实现一套 Host 端的 BMF 调用测试程序,实现对 GPU 图像缩放模块的调用,并提供构建脚本的实现,整体链路将算法层与调用层解耦,充分发挥并展示 BMF 框架在 Windows 端的良好的兼容性、适配性与易用性,本节流程主要分为三个部分:1. GPU 图像缩放算法模块的实现 2. 调用程序的实现。3. 构建脚本的实现
图像缩放模块
与其他可编程着色器(例如顶点和几何着色器)一样,计算着色器(Compute Shader)是使用 HLSL 设计和实现的,但相似之处仅此而已。计算着色器提供高速通用计算,并利用图形处理单元 (GPU) 上的大量并行处理器。计算着色器提供内存共享和线程同步功能,这些特性让 Win 端用户具备轻易调用跨平台框架 DirectX 调用 GPU 高效处理音视频图像领域的诸多计算任务。
一个完整的 DirectX Compute Shader 调用过程分为 Host 端和 Device 端,下面简要阐述 Host 端的调用步骤:
当使用 DirectX 11 或更高版本执行计算着色器时,通常需要以下步骤:
- 创建设备和设备上下文:
a. 创建 DirectX 设备对象,通常通过调用D3D11CreateDevice()
函数。
b. 为了执行计算着色器,设备需要支持 DirectCompute 功能,因此需要检查设备是否支持 DirectCompute。可以通过检查设备属性来实现。 - 创建计算着色器:
a. 创建计算着色器对象,通常通过编译 HLSL(High-Level Shading Language)代码而获得。可以使用 HLSL 编译器将计算着色器代码编译为字节码形式。
b. 使用ID3D11Device::CreateComputeShader()
函数创建计算着色器对象。 - 创建常量缓冲区和资源:
a. 如果计算着色器需要常量或者其他资源作为输入,则需要创建对应的常量缓冲区或者资源。
b. 常量缓冲区通常通过ID3D11Device::CreateBuffer()
函数创建,然后通过ID3D11DeviceContext::CSSetConstantBuffers()
函数将常量缓冲区绑定到计算着色器上下文。
c. 其他资源,如纹理、UAV、SRV 视图、常亮缓冲区等,可以通过相应的创建函数创建,并通过ID3D11DeviceContext::CSSetShaderResources()
函数将其绑定到计算着色器上下文。 - 设置执行参数:
a. 在执行计算着色器之前,需要设置执行参数,包括计算着色器的线程组数等。
b. 使用ID3D11DeviceContext::Dispatch()
函数设置计算着色器执行的线程组数。 - 执行计算着色器:
a. 调用ID3D11DeviceContext::CSSetShader()
函数将计算着色器绑定到设备上下文。
b. 调用ID3D11DeviceContext::Dispatch()
函数执行计算着色器。 - 等待执行完成:可以通过插入事件或者查询设备上下文的执行状态来等待计算着色器的执行完成。
- 清理资源:在完成计算着色器的使用后,需要释放相关资源,包括计算着色器对象、常量缓冲区、资源等。
基于以上流程,本节将构建一个 BMF 模块,命名为 d3dresizemodule ,d3dresizemodule 拥有两个输入流(InputStream),编号 0、1,0 号输入流负责接收 Device、Devicecontext 等基础资源管理对象,并控制 DirectX 侧的初始化流程,在初始化流程中需要完成纹理、SRV/UAV 视图、着色器、采样器等资源的创建和初始化,因此 0 号输入流也被命名为“配置流”(config_stream)。1 号流负责在 DirectX 资源成功被初始化后,接收外界调用方传入的输入纹理数据,并职责,因此也被命名为“数据流”(data_stream),模块整体架构如下图所示:
image.png
d3dresizemodule 模块的声明文件实现如下所示:
#ifndef ROI_Module_H
#define ROI_Module_H
#include <bmf/sdk/module.h>
#include <bmf/sdk/task.h>
#include <d3d11_common.h>
USE_BMF_SDK_NS
class D3DResizeModule : public Module {
public:
D3DResizeModule(int node_id, JsonParam option);
int32_t init();
// DirectX 侧初始化函数,需通过配置流成功配置 device_、device_context_ 后触发
int32_t unsafe_init();
int32_t init_d3d11();
// 模块处理函数,由 BMF 框架驱动调用
int32_t process(Task &task);
int32_t unsafe_process(Task &task);
int32_t close();
// UAV 功能检测函数
bool checkUAVFeature();
~D3DResizeModule();
JsonParam option_;
bool inited = false;
int width_ = 0;
int height_ = 0;
int inputWidth_ = 0;
int inputHeight_ = 0;
BMFComPtr<ID3D11ComputeShader> processShader = nullptr;
BMFComPtr<ID3D11Buffer> outputSizebuf = nullptr;
BMFComPtr<ID3D11SamplerState> sampleState = nullptr;
ID3D11Device *device_ = nullptr;
ID3D11DeviceContext *device_context_ = nullptr;
};
#endif
其中,d3d11_common.h 是一组基于 Windows DirectX 的调用能力,用户可以调用这些接口轻易地实现 DirectX 相关资源的创建和管理,文件内部主要封装了一些与 DirectX 11 相关的常用操作和数据结构。让我们逐一解析:
1.包含头文件:
包含了一些与 DirectX 11 相关的头文件,如 <d3d11.h>
, <wrl/client.h>
, <d3dcompiler.h>
等。这些头文件包含了 DirectX 11 中定义的接口和数据结构。
2.定义了一些结构体:
a.D3D11DeviceWrapper
结构体用于封装了一个 DirectX 11 设备对象的指针。
b.D3D11DeviceContextWrapper
结构体用于封装了一个 DirectX 11 设备上下文对象的指针。
c.D3D11TextureWrapper
结构体用于封装了一个 DirectX 11 纹理对象的指针。
d.InputSizeBuffer
结构体用于定义输入尺寸缓冲区的数据结构,主要用于 DirectX 常量缓冲区。
e.OutputSizeBuffer
结构体用于定义输出尺寸缓冲区的数据结构,主要用于 DirectX 常量缓冲区。
3.定义了一个模板别名:
a.BMFComPtr
是一个模板别名,用于简化使用 Microsoft::WRL::ComPtr类型的代码。
4.声明了一些外部函数:
a.CreateTexture
函数用于创建一个 DirectX 11 纹理对象。
b.CreateUAV
函数用于创建一个 DirectX 11 无序访问视图对象。
c.CreateSRV
函数用于创建一个 DirectX 11 着色器资源视图对象。
d.createComputeShader
函数用于创建一个 DirectX 11 计算着色器对象。
e.CreateStagingTexture
函数用于创建一个用于数据传输的临时纹理对象。
f.CreateSampleState
函数用于创建一个 DirectX 11 采样器状态对象
总的来说,这个头文件封装了一些常用的 DirectX 11 操作函数和数据结构,提供了一种简化 DirectX 11 编程的方式,使得开发者可以更方便地使用 DirectX 11 相关功能,下面是 d3d11_common.h 文件的实现:
#ifndef D3D11_COMMON__H
#define D3D11_COMMON__H
#include <d3d11.h>
#include <wrl/client.h>
#include <d3dcompiler.h>
#include <string>
#include <vector>
struct D3D11DeviceWrapper {
ID3D11Device *device;
};
struct D3D11DeviceContextWrapper {
ID3D11DeviceContext *device_context;
};
struct D3D11TextureWrapper {
ID3D11Texture2D *texture;
};
struct InputSizeBuffer
{
uint32_t inWidth;
uint32_t inHeight;
float padding[2];
};
struct OutputSizeBuffer
{
uint32_t outWidth;
uint32_t outHeight;
float padding[2];
};
template <class T>
using BMFComPtr = Microsoft::WRL::ComPtr<T>;
extern bool CreateTexture(ID3D11Texture2D** texture, ID3D11Device* d3dDevice, int width, int height, DXGI_FORMAT format, const void* initData, D3D11_BIND_FLAG bindflag, int pixelbit, int bitSize);
extern bool CreateUAV(ID3D11UnorderedAccessView** uav, ID3D11Device* d3dDevice, D3D11_UNORDERED_ACCESS_VIEW_DESC* desc, ID3D11Texture2D* texture);
extern bool CreateSRV(ID3D11ShaderResourceView** srv, ID3D11Device* d3dDevice, D3D11_SHADER_RESOURCE_VIEW_DESC* desc, ID3D11Texture2D* texture);
extern bool createComputeShader(ID3D11ComputeShader** shader_, const std::string& shader, ID3D11Device* device);
extern bool CreateStagingTexture(ID3D11Texture2D** stagingTexture, ID3D11Device* device, int width, int height, DXGI_FORMAT format);
extern bool CreateSampleState(ID3D11SamplerState** state, ID3D11Device* device);
// ...
#endif
关于 Shader 的编译,默认的方式是将 shader 代码写在 HLSL 文件中,在程序初始化时调用编译程序读取文件进行编译,这种方式会要求强制暴露 HLSL 代码实现,外界调用方才可以通过 BMF 框架正确加载模块,不利于 Shader 代码的封装,Demo 中使用 map 封装每个 Shader 的字符二进制,并分类管理,这样的好处是在模块编译出 dll 时,相关 hlsl 代码已经成功被封装进模块内部,无需额外附上 hlsl 代码文件,下面关于是 gpuresize Shader 的实现:
static std::map<std::string, std::string> hlslMap = {
{ "gpuresize", R"(
// Define a linear sampler state
SamplerState LinearSampler : register(s0);
Texture2D RGBATexture : register(t0);
cbuffer OutputSize : register(b0)
{
uint outWidth;
uint outHeight;
};
RWTexture2D<float4> RGBOutput : register(u0);
// bgra -> scale -> rgba
[numthreads(16, 16, 1)]
void CSMain(uint3 dtid : SV_DispatchThreadID)
{
if (dtid.x >= outWidth || dtid.y >= outHeight)
return;
float2 samplepoint = (float2(dtid.xy) + float2(0.5, 0.5)) / float2(outWidth, outHeight);
float4 rgba = RGBATexture.SampleLevel(LinearSampler, samplepoint, 0);
RGBOutput[dtid.xy] = rgba;
}
)" }
};
首先,Shader 定义了采样器、输入输出纹理、常量缓冲区等资源,在计算主逻辑中,首先判断当前线程是否处于输出图像范围内,若超出范围则直接返回。然后,根据当前线程的索引计算对应的采样点坐标,并使用 SampleLevel
方法从输入纹理中进行线性采样,获取采样到的 RGBA 像素值,并将其写入输出纹理中。
关于模块的具体实现,这里重点分析三个函数:init_d3d11、process、unsafe_process,首先来看 process 函数:
int32_t D3DResizeModule::process(Task &task) {
try {
int32_t res = unsafe_process(task);
return res;
} catch (std::exception &e) {
BMFLOG(BMF_INFO) << "ROI module process throws std::exception: "
<< e.what();
throw e;
return -1;
}
}
模块的 process 函数由 BMF 框架层调用,本模块将函数执行逻辑主体封装在 unsafe_process 函数中,process 函数使用 try catch 捕获异常,起到兜底作用,unsafe_process 的实现如下:
int32_t D3DResizeModule::unsafe_process(Task& task) {
if (!inited) {
bmf_sdk::Packet d3d11_packet;
while (task.pop_packet_from_input_queue(0, d3d11_packet)) {
if (d3d11_packet.timestamp() == bmf_sdk::BMF_EOF) {
task.set_timestamp(bmf_sdk::DONE);
task.fill_output_packet(0, bmf_sdk::Packet::generate_eof_packet());
break;
}
if (d3d11_packet.is<D3D11DeviceWrapper>()) {
device_ = d3d11_packet.get<D3D11DeviceWrapper>().device;
}
else if (d3d11_packet.is<D3D11DeviceContextWrapper>()) {
device_context_ = d3d11_packet.get<D3D11DeviceContextWrapper>().device_context;
}
else {
BMFLOG_NODE(BMF_WARNING, node_id_) << "get unexpected data:" << d3d11_packet.type_info().name << std::endl;
}
}
if (device_ && device_context_) {
init_d3d11();
}
}
bmf_sdk::Packet frame_packet;
int textureStyle = -1;
while (task.pop_packet_from_input_queue(1, frame_packet)) {
if (frame_packet.timestamp() == bmf_sdk::BMF_EOF) {
task.set_timestamp(bmf_sdk::DONE);
task.fill_output_packet(0, bmf_sdk::Packet::generate_eof_packet());
break;
}
if (!frame_packet.is<D3D11TextureWrapper>()) {
BMFLOG_NODE(BMF_ERROR, node_id_) << "get unexpected data:" << frame_packet.type_info().name << std::endl;
}
D3D11TextureWrapper inputPkt = frame_packet.get<D3D11TextureWrapper>();
ID3D11Texture2D* input_texture = inputPkt.texture;
if (!input_texture) {
throw std::exception("null texture input!");
}
ID3D11Texture2D *outputTexture = nullptr;
CreateTexture(&outputTexture, device_, width_, height_, DXGI_FORMAT_R8G8B8A8_UNORM, nullptr, (D3D11_BIND_FLAG)(D3D11_BIND_UNORDERED_ACCESS), 4, sizeof(uint8_t));
BMFComPtr<ID3D11ShaderResourceView> input_srv;
BMFComPtr<ID3D11UnorderedAccessView> output_texture_uav;
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
ZeroMemory(&srvDesc, sizeof(srvDesc));
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = 1;
if (!CreateSRV(input_srv.GetAddressOf(), device_, &srvDesc, input_texture)) {
throw std::exception("input_srv create failed");
}
if (!CreateUAV(output_texture_uav.GetAddressOf(), device_, nullptr, outputTexture)) {
throw std::exception("output_texture_uav create failed!");
}
// flush the DirectX resource
ID3D11ShaderResourceView* null_srv = nullptr;
ID3D11UnorderedAccessView* null_uav = nullptr;
ID3D11Buffer* null_buf = nullptr;
ID3D11SamplerState* null_sample = nullptr;
ID3D11ComputeShader* null_shader = nullptr;
device_context_->CSSetShaderResources(0, 1, &null_srv);
device_context_->CSSetUnorderedAccessViews(0, 1, &null_uav, nullptr);
device_context_->CSSetConstantBuffers(0, 1, &null_buf);
device_context_->CSSetSamplers(0, 1, &null_sample);
device_context_->CSSetShader(null_shader, nullptr, 0);
// execute the resize shader
device_context_->CSSetShaderResources(0, 1, input_srv.GetAddressOf());
device_context_->CSSetUnorderedAccessViews(0, 1, output_texture_uav.GetAddressOf(), nullptr);
device_context_->CSSetShader(processShader.Get(), nullptr, 0);
device_context_->CSSetConstantBuffers(0, 1, outputSizebuf.GetAddressOf());
device_context_->CSSetSamplers(0, 1, sampleState.GetAddressOf());
device_context_->Dispatch((width_ - 1 + 16) / 16, (height_ - 1 + 16) / 16, 1);
D3D11TextureWrapper outputTextureWrapper;
outputTextureWrapper.texture = outputTexture;
bmf_sdk::Packet output_packet(outputTextureWrapper);
task.fill_output_packet(0, output_packet);
}
return 0;
}
这段代码实现的主要功能如下:
1.初始化阶段(未初始化时):
通过从模块的配置流中获取数据包,初始化 D3D11 设备和设备上下文对象。
如果获取到了设备和设备上下文对象,则调用 init_d3d11
函数进行 DirectX 11 的初始化。
2.处理阶段:
- 清空之前的着色器资源视图和无序访问视图。
- 设置输入纹理的着色器资源视图和输出纹理的无序访问视图。
- 设置计算着色器,并执行计算着色器的调度,实现了一个 D3D11 RGBA 纹理的 resize 功能
a .通过循环从模块的数据流获取数据包,直到获取到结束标志(BMF_EOF
)为止。
b.如果获取到的数据包类型为 D3D11TextureWrapper
,则表示获取到了 D3D11 纹理对象。
c.根据获取到的输入纹理,创建一个新的输出纹理对象。
d.调用上文描述的接口,创建输入纹理的着色器资源视图(SRV)和输出纹理的无序访问视图(UAV)。
e.执行一系列的 DirectX 11 操作:
f.封装输出纹理对象为数据包,并 push 到模块的输出队列中,供外界调用代码获取
init_d3d11 函数的实现如下:
int32_t D3DResizeModule::init_d3d11() {
if (!inited) {
if (!device_ || !device_context_) {
throw std::exception("d3d11 device or context is not inited!");
}
D3D_FEATURE_LEVEL featureLevel = device_->GetFeatureLevel();
if (featureLevel < D3D_FEATURE_LEVEL_11_0) {
throw std::exception("local d3d11 feature level < 11! computeshader cannot work!");
}
if (!checkUAVFeature()) {
throw std::exception("UAV load is not supported in this hardware");
}
if (!createComputeShader(processShader.GetAddressOf(), "gpuresize", device_)) {
throw std::exception("shader compile failed!");
}
if (!CreateSampleState(sampleState.GetAddressOf(), device_)) {
throw std::exception("sample state create failed!");
}
OutputSizeBuffer sizeBuffer;
sizeBuffer.outWidth = width_;
sizeBuffer.outHeight = height_;
D3D11_BUFFER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.ByteWidth = sizeof(OutputSizeBuffer);
desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
desc.MiscFlags = 0;
desc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA initData;
initData.pSysMem = &sizeBuffer;
initData.SysMemPitch = 0;
initData.SysMemSlicePitch = 0;
HRESULT hr = device_->CreateBuffer(&desc, &initData, outputSizebuf.GetAddressOf());
if (FAILED(hr)) {
throw std::exception("constant buffer create failed!");
}
inited = true;
}
}
代码主要实现的逻辑如下:
- 检查设备和设备上下文对象是否已经初始化,如果没有则抛出异常。
- 获取当前设备的特性级别(Feature Level),并检查是否支持 D3D11 特性级别 11.0 以上,如果不支持则抛出异常。
- 检查硬件是否支持 UAV(Unordered Access View)加载特性,如果不支持则抛出异常。
- 编译创建计算着色器,用于图像的 resize 操作。
- 创建采样器状态(Sampler State),用于计算着色器中的纹理采样。
- 创建输出大小的常量缓冲区(Constant Buffer),用于传递输出图像的宽度和高度信息给计算着色器。
- 将输出大小的缓冲区数据初始化,并创建 D3D11 缓冲区对象。
- 设置标志位
inited
为 true,表示 D3D11 初始化完成。
从上文 unsafe_process 函数的逻辑可以获知:init_d3d11 函数的调用时机是当模块成功接收了外界传入的 Device 和 DeviceContext 之后。
以上便是基于 DirectX 的图像缩放模块 D3DResizeModule 的设计与实现,通过集成 BMF 开发环境,可以编译出对应的 BMF 模块
调用程序
本节将实现一个测试 demo 程序,用于测试和调用上文中所构建的图像缩放模块,测试程序实现如下:
#include <bmf/sdk/log.h>
#include <bmf/sdk/video_frame.h>
#include <nlohmann/json.hpp>
#include <builder.hpp>
#include <chrono>
#include <d3d11_common.h>
#include <fstream>
using json = nlohmann::json;
using namespace std::chrono;
namespace fs = std::filesystem;
static const int TESTINPUTWIDTH = 720;
static const int TESTINPUTHEIGHT = 1280;
static const int TESTOUTPUTWIDTH = 1080;
static const int TESTOUTPUTHEIGHT = 1920;
static void readRGBAFile(std::vector<uint8_t>& data, const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
if (!file) {
throw std::runtime_error("cannot open yuv file!");
}
file.seekg(0, std::ios::end);
std::streampos fileSize = file.tellg();
file.seekg(0, std::ios::beg);
if (fileSize != (TESTINPUTWIDTH * TESTINPUTHEIGHT * 4)) {
throw std::runtime_error("file size not compared with TESTINPUTWIDTH and TESTINPUTHEIGHT! it should be a RGBA data");
}
file.read(reinterpret_cast<char*>(data.data()), fileSize);
file.close();
}
int main(int argc, char const *argv[])
{
static const int profile_time = 1;
HRESULT hr = S_OK;
BMFComPtr<ID3D11Device> device = nullptr;
BMFComPtr<ID3D11DeviceContext> context = nullptr;
BMFComPtr<ID3D11ComputeShader> computeShader = nullptr;
BMFComPtr<ID3D11Texture2D> input_rgba = nullptr;
// Initialize device and context
hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
D3D11_SDK_VERSION, &device, nullptr, &context);
if (FAILED(hr)) {
std::cerr << "Failed to create D3D11 device" << std::endl;
exit(EXIT_FAILURE);
}
std::vector<uint8_t> filedata(TESTINPUTWIDTH * TESTINPUTHEIGHT * 4, 0);
readRGBAFile(filedata, "../../../files/test_opencv_lenna_720x1280.rgb");
ID3DBlob* pBlob = nullptr;
D3D11_SUBRESOURCE_DATA sd;
sd.pSysMem = filedata.data();
sd.SysMemPitch = TESTINPUTWIDTH * sizeof(uint8_t) * 4;
sd.SysMemSlicePitch = TESTINPUTWIDTH * TESTINPUTHEIGHT * sizeof(uint8_t) * 4;
CD3D11_TEXTURE2D_DESC texDesC(DXGI_FORMAT_R8G8B8A8_UNORM, TESTINPUTWIDTH, TESTINPUTHEIGHT, 1, 1, (D3D11_BIND_FLAG)(D3D11_BIND_SHADER_RESOURCE), D3D11_USAGE_DEFAULT, 0, 1, 0, D3D11_RESOURCE_MISC_SHARED);
texDesC.CPUAccessFlags = 0;
texDesC.MipLevels = 1;
texDesC.ArraySize = 1;
hr = device->CreateTexture2D(&texDesC, &sd, &input_rgba);
if (FAILED(hr)) {
std::cerr << "Failed to CreateTexture2D" << std::endl;
exit(EXIT_FAILURE);
}
{
try {
bmf::builder::Graph graph = bmf::builder::Graph(bmf::builder::GeneratorMode);
auto input_stream0 = graph.InputStream(
"config_stream", "stream0", "");
auto input_stream1 = graph.InputStream(
"data_stream", "stream1", "");
json roi_option = {
{"width", TESTOUTPUTWIDTH},
{"height", TESTOUTPUTHEIGHT}
};
std::string moduleName;
#ifdef _DEBUG
moduleName = "d3dresizemoduled.dll";
#else
moduleName = "d3dresizemodule.dll";
#endif
auto resize_module = graph.CppModule({ input_stream0, input_stream1 }, "d3dresizemodule", bmf_sdk::JsonParam(roi_option),
"", moduleName, "d3dresizemodule:D3DResizeModule");
auto resize_output_stream = resize_module.Stream(0);
std::vector<bmf::builder::Stream> enhanceGenerateStreams;
enhanceGenerateStreams.emplace_back(resize_output_stream);
graph.Start(enhanceGenerateStreams);
D3D11DeviceWrapper device_wrapper;
D3D11DeviceContextWrapper device_context_wrapper;
device_wrapper.device = device.Get();
device_context_wrapper.device_context = context.Get();
graph.FillPacket(input_stream0.GetName(), bmf_sdk::Packet(device_wrapper));
graph.FillPacket(input_stream0.GetName(), bmf_sdk::Packet(device_context_wrapper));
D3D11TextureWrapper input;
input.texture = input_rgba.Get();
graph.FillPacket(input_stream1.GetName(), bmf_sdk::Packet(input));
Packet output_pkt = graph.Generate(resize_output_stream.GetName());
ID3D11Texture2D* out_texture = nullptr;
D3D11TextureWrapper output_ = output_pkt.get<D3D11TextureWrapper>();
out_texture = output_.texture;
// Read Output and write file local
std::vector<uint8_t> outdata(TESTOUTPUTWIDTH * TESTOUTPUTHEIGHT * 4, 1);
ReadDataFromTexture(device.Get(), context.Get(), out_texture, TESTOUTPUTWIDTH, TESTOUTPUTHEIGHT, DXGI_FORMAT_R8G8B8A8_UNORM, outdata, 4);
std::ofstream outputFile("output.rgb", std::ios::binary);
if (!outputFile.is_open()) {
printf("open outputfile error!\n");
}
outputFile.write((const char*)outdata.data(), sizeof(uint8_t) * TESTOUTPUTWIDTH * TESTOUTPUTHEIGHT * 4);
outputFile.close();
out_texture->Release();
out_texture = nullptr;
context->Flush();
}
catch (const fs::filesystem_error& e) {
std::cerr << "FileSystem error: " << e.what() << std::endl;
}
catch (const std::exception& e) {
std::cerr << "General error: " << e.what() << std::endl;
}
}
return 0;
}
以下是程序的主要步骤和功能:
1.初始化 DirectX 11 设备和上下文:
使用 D3D11CreateDevice
函数创建 D3D11 设备和上下文对象。
2.准备输入图像数据:
从文件中读取 RGBA 格式的测试图像数据。
3.创建输入图像的 D3D11 纹理对象:
使用 CreateTexture2D
函数创建输入图像的 D3D11 纹理对象。
4.构建 BMF Graph:
使用生成器模式创建并初始化 bmf::builder::Graph
添加 2 个输入流(配置流和数据流)和输出流,以及需要的参数,通过 json 数据创建要缩放的图像宽高。
导入名为 d3dresizemodule
的多媒体处理模块,并连接输入流和输出流,调用 Start 函数启动 Graph
5.填充输入数据:
将创建的 D3D11 设备和上下文对象填充到配置流中,驱动模块内部完成 DirectX 侧的初始化
将创建的输入图像纹理送入到数据流中。
6.执行 BMF Graph:
通过调用 graph.Generate
方法驱动模块执行处理流程,获取数据帧
7.读取输出数据并写入文件:
从生成的数据中获取输出图像的 D3D11 纹理对象。
使用自定义函数 ReadDataFromTexture
读取纹理数据。
将输出图像数据写入文件 output.rgb
中。
8.调用 Flush 方法清空任务队列,同步释放 DirectX 相关资源
9.异常处理:
在程序执行过程中捕获可能出现的异常,并输出错误信息。
通过以上步骤,测试程序实现了对 Direct3D 11 实现的图像 resize 模块的测试,并将处理结果保存到文件中,用于后续的分析和验证。
构建脚本与运行
本节主要介绍构建并运行图像缩放处理程序 Demo 的流程与步骤,同样需要进入 msys2 环境,本文实现的 Demo 可以通过构建脚本实现多种不同 Module 的选择性构建,Demo 项目的目录结构如下所示:
bmf_demo/
│
├── cmake/
│ ├── win-toolchain.cmake
├── modules/
│ ├── common/
│ │ ├── d3d11/
│ │ │ ├── include/
│ │ │ ├ ├── d3d11_common.h
│ │ │ ├── src/
│ │ │ ├ ├── d3d11_common.cpp
│ ├── d3d11resizemodule/
│ │ ├── include/
│ │ ├ ├── d3dresizemodule.h
│ │ ├── src/
│ │ ├ ├── d3dresizemodule.cpp
│ │ ├── test/
│ │ ├ ├── test_d3dresizemodule.cpp
│ │ ├── CMakeLists.txt
│ └── ...
│
├── build.sh
├── CMakeLists.txt
└── CMakePresets.json
其中 cmake/win-toolchain.cmake 是一个 cmake 配置文件,其中包括 BMF 适配 Windows msvc 环境的编译选项配置,modules 文件夹下包含了诸多用户实现的 modules,其中 common 文件夹为公共文件目录,其中实现了上文所述的 DirectX API,如果用户想添加新实现的 module,可以在 modules 文件夹下再新建一个文件夹,命名为 module 名称,内部文件布局保持与 d3d11resizemodule 一致即可。
build.sh
脚本用于构建项目,并支持一些参数来配置构建过程。这个解释使用方法和每个参数的意义和作用
--module
指定要编译构建的 module 名称,需要与 modules 文件夹下的某个目录对齐命名
--msvc
指定 msvc 版本,支持[2013, 2015, 2017, 2019, 2022]
--bmf_lite
指定 lite 控制标识位,用于控制内部的一些 Win 的特化配置项
--test
控制是否编译对应测试 demo 程序,内部通过 cmake 控制
对于本例,本文使用的编译构建命令如下:
./build.sh --msvc=2022 --module=d3dresizemodule --bmf-lite --test
在构建项目前,需要设置 BMF 库的 INCLUDE 和 LIB 环境变量
export BMF_INCLUDE_DIR=/path/to/bmf/include
export BMF_LIBRARY_DIR=/path/to/bmf/lib
在 msys 执行脚本后,会在本地生成 build_windows_lite_{preset} 文件夹,与 BMF 框架相同,可以使用 Visual Studio 界面交互进行项目构建,或使用命令行直接构建
构建完成后,在项目目录本地会生成 output 文件夹,切换至 output/bin/{preset}/Release/,执行 ./test_d3dresizemodule.exe 程序,程序输出如下
发现在本地生成了 output.rgb 文件
使用 ffplay 查看生成的图像
ffplay -pix_fmt rgba -s 1080x1920 output.rgb
结果如下