在 Windows 下玩轉(zhuǎn)多媒體處理框架 BMF
一、簡介
現(xiàn)代科技網(wǎng)絡(luò)日益發(fā)達,視頻已經(jīng)成為人們生活中不可或缺的一部分。隨著互聯(lián)網(wǎng)和移動設(shè)備的普及,視頻內(nèi)容在傳播和分享方面發(fā)揮著越來越重要的作用。從社交媒體到在線教育,從數(shù)字廣告到遠程工作,視頻已經(jīng)成為人們獲取信息、娛樂和交流的主要方式之一。在這樣一個視頻日益普及的超視頻時代,開發(fā)一套跨語言、跨設(shè)備、跨系統(tǒng)的多媒體處理框架顯得尤為重要,這樣的框架可以為開發(fā)人員提供統(tǒng)一的解決方案,幫助他們在不同的平臺上快速、高效地處理多媒體內(nèi)容,從而提供一致的用戶體驗和功能,是迎接未來的必然趨勢。
在當今數(shù)字化的世界中,Windows 平臺的重要性和關(guān)鍵性無可置疑。作為普通用戶的首要選擇,Windows 提供了廣泛的硬件和軟件支持,為用戶提供了豐富多彩的體驗。特別是在多媒體處理領(lǐng)域,Windows 平臺憑借其強大的生態(tài)系統(tǒng)和穩(wěn)定的性能,基本是普通用戶的首選。Windows 平臺擁有龐大而完善的 DirectX 能力體系,這使得在Windows 環(huán)境下可以很方便地實現(xiàn)通過 GPU 加速圖像視頻處理的性能,這種強大的圖形處理能力可以更高效地處理和渲染視頻、音頻等多媒體內(nèi)容。特別是對于游戲主播、視頻編輯等相關(guān)領(lǐng)域的從業(yè)者,Windows 平臺提供了一個穩(wěn)定而強大的開發(fā)環(huán)境,為他們的創(chuàng)作和工作帶來了極大的便利和效率。因此,開發(fā)一套兼容 Windows 平臺的多媒體處理框架具有重要的意義。這不僅可以滿足普通用戶對于多媒體內(nèi)容的需求,還可以為專業(yè)從業(yè)者提供強大的工具和支持。無論是游戲行業(yè)、直播行業(yè)還是視頻編輯領(lǐng)域,都可以受益于這樣一套高效、穩(wěn)定的多媒體處理框架,為用戶帶來更優(yōu)質(zhì)的體驗和服務(wù)。
基于以上兩個前提,2023 年 8 月 22 日,火山引擎視頻云與NVIDIA正式開源多媒體處理框架 Babit Multimedia Framework (以下統(tǒng)稱 BMF 框架),BMF 在 Windows 側(cè)對齊 Linux,目前已經(jīng)打通了框架的編譯、構(gòu)建、同時支持模塊自定義開發(fā),在字節(jié)跳動內(nèi)部,BMF 在 Win 側(cè)已集成多種使用 CPU/GPU 的圖像處理算法,服務(wù)于抖音直播伴侶業(yè)務(wù),目前已有 5 個算法已被成功集成,BMF 框架作為搭建算法與業(yè)務(wù)的橋梁,通過自定義模塊實現(xiàn)算法邏輯與業(yè)務(wù)的完全解耦,其內(nèi)部可以很方便地在 Win 側(cè)集成不同圖像處理算法。本文將沿三個步驟,全面介紹 BMF 框架在 Windows 端的能力建設(shè)與技術(shù)實踐,首先介紹 BMF 框架在 Windows 環(huán)境下如何配置與編譯,其次介紹如何在 Windows 環(huán)境配置 BMF 開發(fā)環(huán)境,并展示一個簡單 Python 模塊的運行過程,最后展示一個基于 DirectX 的全鏈路圖像縮放模塊的開發(fā)與部署案例,展示 BMF 在 Windows 端友好的兼容性和強大的功能適配能力,助您在 Windows 下玩轉(zhuǎn)BMF!
二、編譯與構(gòu)建
編譯 BMF 框架需要依賴以下環(huán)境:
- MSYS2。提供了一個基于開源軟件的本機構(gòu)建環(huán)境,可以在 Windows 上用 Linux 的方式使用多種不同的環(huán)境和工具來執(zhí)行不同的任務(wù)。
- CMake。管理框架構(gòu)建過程,推薦版本 3.27。
- Visual Studio 2013 - 2022。BMF 在 Win 端選用兼容性較好的 msvc 編譯工具鏈,目前支持版本 2013 - 2022。
以上三個依賴是必須項,下面還有 2 個依賴可供選擇是否打開相關(guān)的框架能力:
- Python 3.7 - 3.10。用于編譯 BMF Python SDK,如果不提供,則框架無法編譯 Python 相關(guān)的調(diào)用能力
- FFmpeg 4.4 - 5.1。用于編譯 BMF built-in modules(ffmpeg_decoder、ffmpeg_encoder、ffmpeg_filter),如果不提供,默認取消編譯相關(guān)產(chǎn)物
本文在介紹編譯過程時,默認會打開以上兩個選項,實現(xiàn)一次全鏈路的編譯構(gòu)建,下面用圖文方式介紹 BMF 框架編譯過程:
- 打開 Visual Studio 的命令提示環(huán)境,建議以管理員方式
- 找到 MSYS2 的安裝目錄,執(zhí)行命令: .\msys2_shell.cmd -use-full-path,攜帶宿主機環(huán)境進入 msys2(注意:這里需要保證 CMake 工具已經(jīng)成功配置進系統(tǒng)環(huán)境變量,本文的實驗環(huán)境默認配置了 CMake 3.27、FFmpeg 4.4、Python 3.7.9 的環(huán)境變量)。
- 克隆 BMF 項目,在根目錄運行 build_win_lite 腳本,項目設(shè)有以下編譯控制選項:
Build Options:
--msvc 設(shè)定 msvc 版本,包括[2013, 2015, 2017, 2019, 2022]
bmf_ffmpeg 控制是否集成 FFmpeg 相關(guān)能力并編譯 built-in module
--preset 編譯配置,包括[x86-Debug, x86-Release, x64-Debug, x64-Release]
注意:如果您需要編譯 FFmpeg 相關(guān)的能力,且您本地的 CMake 版本高于等于 3.28,你還需要設(shè)置 ffmpeg 的 include 目錄,命令如下:
export INCLUDE="$INCLUDE;C:\path\to\include_for_ffmpeg"
以 msvc 2022 編譯 x64-Debug 版本為例,腳本執(zhí)行命令:
./build_win_lite.sh --msvc=2022 --preset=x64-Debug bmf_ffmpeg
執(zhí)行后會在項目目錄的 build_win_lite/x64-Debug 目錄生成 BMF.sln 解決方案
之后,您可以雙擊 sln 文件使用 Visual Studio 友好的界面進行項目構(gòu)建和編譯,您也可以使用以下 CMake 命令直接通過命令行進行項目構(gòu)建:
cmake --build build_win_lite/x64-Debug --config Debug --target ALL_BUILD
至此,您便可以在 Windows 環(huán)境中完成對 BMF 框架的編譯與構(gòu)建,在 Visual Studio 的編譯過程如圖所示
三、開發(fā)環(huán)境配置與模塊運行
本章介紹如何搭建 BMF 開發(fā)環(huán)境,繼上文所述,當我們完成 BMF 框架的編譯構(gòu)建后,會生成 output 文件夾,我們需要將 bin 目錄與 lib 目錄配置進系統(tǒng)環(huán)境變量,與此同時,BMF 開發(fā)環(huán)境還需依賴一個 Win 相關(guān)的依賴集合 win_rootfs(https://github.com/BabitMF/bmf/releases/download/files/win_rootfs.tar.gz),需配置環(huán)境變量,如圖所示:
配置完 BMF 環(huán)境變量后,我們需要重啟 msys2 環(huán)境,目的是讓 BMF 的環(huán)境變量生效,下面展示如何運行一個 Python Module 的測試程序:test_customize_module。首先,需要將 msys2 當前目錄切換至編譯產(chǎn)物的上級目錄,設(shè)置一些 msys2 環(huán)境的環(huán)境變量,配置 BMF 框架的 Python 運行環(huán)境
export PYTHONHOME="$(dirname "$(which python)")"
export PYTHONPATH=$(pwd)/output/bmf/lib:$(pwd)/output
配置完畢后,進入 python 環(huán)境應該可以正常 import bmf 框架,如圖所示
我們要運行的 customize_module 文件在 output/test/customize_module 目錄下的 my_module.py 文件,模塊的實現(xiàn)代碼如下:
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
可以看到,模塊僅僅將幀從輸入隊列取出,不做任何處理,直接傳遞至輸出隊列,我們將要執(zhí)行的測試程序 test_customize_module.py 的實現(xiàn)如下:
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 行中,我們構(gòu)建了一個 BMF Graph,首先對輸入視頻進行解碼,隨后調(diào)起事先我們寫好的 Python 模塊 my_module 對解碼后的視頻幀進行一次處理,最后調(diào)用 encode 模塊對視頻幀進行編碼,產(chǎn)出輸出文件,剩余部分是框架使用 Google Test 框架所進行的一些轉(zhuǎn)碼指標的驗證,這里不深入追溯,程序使用的輸入視頻是 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)
至此,所有執(zhí)行該程序的前置依賴均已準備完畢,切換到 customize_module 目錄執(zhí)行程序
cd test/customize_module
python test_customize_module.py
程序執(zhí)行結(jié)果如圖所示,可以看到在本地成功產(chǎn)出了 output.mp4 文件
四、實踐案例
本章將從 0 到 1 帶你實現(xiàn)一個 RGBA 圖像的 GPU 縮放功能,基于 DirectX Compute Shader 機制完成算法能力建設(shè),并使用 BMF 框架的模塊機制將算法能力封裝進 BMF 模塊中,同時實現(xiàn)一套 Host 端的 BMF 調(diào)用測試程序,實現(xiàn)對 GPU 圖像縮放模塊的調(diào)用,并提供構(gòu)建腳本的實現(xiàn),整體鏈路將算法層與調(diào)用層解耦,充分發(fā)揮并展示 BMF 框架在 Windows 端的良好的兼容性、適配性與易用性,本節(jié)流程主要分為三個部分:1. GPU 圖像縮放算法模塊的實現(xiàn) 2. 調(diào)用程序的實現(xiàn)。3. 構(gòu)建腳本的實現(xiàn)
圖像縮放模塊
與其他可編程著色器(例如頂點和幾何著色器)一樣,計算著色器(Compute Shader)是使用 HLSL 設(shè)計和實現(xiàn)的,但相似之處僅此而已。計算著色器提供高速通用計算,并利用圖形處理單元 (GPU) 上的大量并行處理器。計算著色器提供內(nèi)存共享和線程同步功能,這些特性讓 Win 端用戶具備輕易調(diào)用跨平臺框架 DirectX 調(diào)用 GPU 高效處理音視頻圖像領(lǐng)域的諸多計算任務(wù)。
一個完整的 DirectX Compute Shader 調(diào)用過程分為 Host 端和 Device 端,下面簡要闡述 Host 端的調(diào)用步驟:
當使用 DirectX 11 或更高版本執(zhí)行計算著色器時,通常需要以下步驟:
- 創(chuàng)建設(shè)備和設(shè)備上下文:
a. 創(chuàng)建 DirectX 設(shè)備對象,通常通過調(diào)用D3D11CreateDevice()
函數(shù)。
b. 為了執(zhí)行計算著色器,設(shè)備需要支持 DirectCompute 功能,因此需要檢查設(shè)備是否支持 DirectCompute??梢酝ㄟ^檢查設(shè)備屬性來實現(xiàn)。 - 創(chuàng)建計算著色器:
a. 創(chuàng)建計算著色器對象,通常通過編譯 HLSL(High-Level Shading Language)代碼而獲得??梢允褂?HLSL 編譯器將計算著色器代碼編譯為字節(jié)碼形式。
b. 使用ID3D11Device::CreateComputeShader()
函數(shù)創(chuàng)建計算著色器對象。 - 創(chuàng)建常量緩沖區(qū)和資源:
a. 如果計算著色器需要常量或者其他資源作為輸入,則需要創(chuàng)建對應的常量緩沖區(qū)或者資源。
b. 常量緩沖區(qū)通常通過ID3D11Device::CreateBuffer()
函數(shù)創(chuàng)建,然后通過ID3D11DeviceContext::CSSetConstantBuffers()
函數(shù)將常量緩沖區(qū)綁定到計算著色器上下文。
c. 其他資源,如紋理、UAV、SRV 視圖、常亮緩沖區(qū)等,可以通過相應的創(chuàng)建函數(shù)創(chuàng)建,并通過ID3D11DeviceContext::CSSetShaderResources()
函數(shù)將其綁定到計算著色器上下文。 - 設(shè)置執(zhí)行參數(shù):
a. 在執(zhí)行計算著色器之前,需要設(shè)置執(zhí)行參數(shù),包括計算著色器的線程組數(shù)等。
b. 使用ID3D11DeviceContext::Dispatch()
函數(shù)設(shè)置計算著色器執(zhí)行的線程組數(shù)。 - 執(zhí)行計算著色器:
a. 調(diào)用ID3D11DeviceContext::CSSetShader()
函數(shù)將計算著色器綁定到設(shè)備上下文。
b. 調(diào)用ID3D11DeviceContext::Dispatch()
函數(shù)執(zhí)行計算著色器。 - 等待執(zhí)行完成:可以通過插入事件或者查詢設(shè)備上下文的執(zhí)行狀態(tài)來等待計算著色器的執(zhí)行完成。
- 清理資源:在完成計算著色器的使用后,需要釋放相關(guān)資源,包括計算著色器對象、常量緩沖區(qū)、資源等。
基于以上流程,本節(jié)將構(gòu)建一個 BMF 模塊,命名為 d3dresizemodule ,d3dresizemodule 擁有兩個輸入流(InputStream),編號 0、1,0 號輸入流負責接收 Device、Devicecontext 等基礎(chǔ)資源管理對象,并控制 DirectX 側(cè)的初始化流程,在初始化流程中需要完成紋理、SRV/UAV 視圖、著色器、采樣器等資源的創(chuàng)建和初始化,因此 0 號輸入流也被命名為“配置流”(config_stream)。1 號流負責在 DirectX 資源成功被初始化后,接收外界調(diào)用方傳入的輸入紋理數(shù)據(jù),并職責,因此也被命名為“數(shù)據(jù)流”(data_stream),模塊整體架構(gòu)如下圖所示:
image.png
d3dresizemodule 模塊的聲明文件實現(xiàn)如下所示:
#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 側(cè)初始化函數(shù),需通過配置流成功配置 device_、device_context_ 后觸發(fā)
int32_t unsafe_init();
int32_t init_d3d11();
// 模塊處理函數(shù),由 BMF 框架驅(qū)動調(diào)用
int32_t process(Task &task);
int32_t unsafe_process(Task &task);
int32_t close();
// UAV 功能檢測函數(shù)
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 的調(diào)用能力,用戶可以調(diào)用這些接口輕易地實現(xiàn) DirectX 相關(guān)資源的創(chuàng)建和管理,文件內(nèi)部主要封裝了一些與 DirectX 11 相關(guān)的常用操作和數(shù)據(jù)結(jié)構(gòu)。讓我們逐一解析:
1.包含頭文件:
包含了一些與 DirectX 11 相關(guān)的頭文件,如 <d3d11.h>
, <wrl/client.h>
, <d3dcompiler.h>
等。這些頭文件包含了 DirectX 11 中定義的接口和數(shù)據(jù)結(jié)構(gòu)。
2.定義了一些結(jié)構(gòu)體:
a.D3D11DeviceWrapper
結(jié)構(gòu)體用于封裝了一個 DirectX 11 設(shè)備對象的指針。
b.D3D11DeviceContextWrapper
結(jié)構(gòu)體用于封裝了一個 DirectX 11 設(shè)備上下文對象的指針。
c.D3D11TextureWrapper
結(jié)構(gòu)體用于封裝了一個 DirectX 11 紋理對象的指針。
d.InputSizeBuffer
結(jié)構(gòu)體用于定義輸入尺寸緩沖區(qū)的數(shù)據(jù)結(jié)構(gòu),主要用于 DirectX 常量緩沖區(qū)。
e.OutputSizeBuffer
結(jié)構(gòu)體用于定義輸出尺寸緩沖區(qū)的數(shù)據(jù)結(jié)構(gòu),主要用于 DirectX 常量緩沖區(qū)。
3.定義了一個模板別名:
a.BMFComPtr
是一個模板別名,用于簡化使用 Microsoft::WRL::ComPtr類型的代碼。
4.聲明了一些外部函數(shù):
a.CreateTexture
函數(shù)用于創(chuàng)建一個 DirectX 11 紋理對象。
b.CreateUAV
函數(shù)用于創(chuàng)建一個 DirectX 11 無序訪問視圖對象。
c.CreateSRV
函數(shù)用于創(chuàng)建一個 DirectX 11 著色器資源視圖對象。
d.createComputeShader
函數(shù)用于創(chuàng)建一個 DirectX 11 計算著色器對象。
e.CreateStagingTexture
函數(shù)用于創(chuàng)建一個用于數(shù)據(jù)傳輸?shù)呐R時紋理對象。
f.CreateSampleState
函數(shù)用于創(chuàng)建一個 DirectX 11 采樣器狀態(tài)對象
總的來說,這個頭文件封裝了一些常用的 DirectX 11 操作函數(shù)和數(shù)據(jù)結(jié)構(gòu),提供了一種簡化 DirectX 11 編程的方式,使得開發(fā)者可以更方便地使用 DirectX 11 相關(guān)功能,下面是 d3d11_common.h 文件的實現(xiàn):
#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
關(guān)于 Shader 的編譯,默認的方式是將 shader 代碼寫在 HLSL 文件中,在程序初始化時調(diào)用編譯程序讀取文件進行編譯,這種方式會要求強制暴露 HLSL 代碼實現(xiàn),外界調(diào)用方才可以通過 BMF 框架正確加載模塊,不利于 Shader 代碼的封裝,Demo 中使用 map 封裝每個 Shader 的字符二進制,并分類管理,這樣的好處是在模塊編譯出 dll 時,相關(guān) hlsl 代碼已經(jīng)成功被封裝進模塊內(nèi)部,無需額外附上 hlsl 代碼文件,下面關(guān)于是 gpuresize Shader 的實現(xiàn):
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 定義了采樣器、輸入輸出紋理、常量緩沖區(qū)等資源,在計算主邏輯中,首先判斷當前線程是否處于輸出圖像范圍內(nèi),若超出范圍則直接返回。然后,根據(jù)當前線程的索引計算對應的采樣點坐標,并使用 SampleLevel
方法從輸入紋理中進行線性采樣,獲取采樣到的 RGBA 像素值,并將其寫入輸出紋理中。
關(guān)于模塊的具體實現(xiàn),這里重點分析三個函數(shù):init_d3d11、process、unsafe_process,首先來看 process 函數(shù):
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 函數(shù)由 BMF 框架層調(diào)用,本模塊將函數(shù)執(zhí)行邏輯主體封裝在 unsafe_process 函數(shù)中,process 函數(shù)使用 try catch 捕獲異常,起到兜底作用,unsafe_process 的實現(xiàn)如下:
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;
}
這段代碼實現(xiàn)的主要功能如下:
1.初始化階段(未初始化時):
通過從模塊的配置流中獲取數(shù)據(jù)包,初始化 D3D11 設(shè)備和設(shè)備上下文對象。
如果獲取到了設(shè)備和設(shè)備上下文對象,則調(diào)用 init_d3d11
函數(shù)進行 DirectX 11 的初始化。
2.處理階段:
- 清空之前的著色器資源視圖和無序訪問視圖。
- 設(shè)置輸入紋理的著色器資源視圖和輸出紋理的無序訪問視圖。
- 設(shè)置計算著色器,并執(zhí)行計算著色器的調(diào)度,實現(xiàn)了一個 D3D11 RGBA 紋理的 resize 功能
a .通過循環(huán)從模塊的數(shù)據(jù)流獲取數(shù)據(jù)包,直到獲取到結(jié)束標志(BMF_EOF
)為止。
b.如果獲取到的數(shù)據(jù)包類型為 D3D11TextureWrapper
,則表示獲取到了 D3D11 紋理對象。
c.根據(jù)獲取到的輸入紋理,創(chuàng)建一個新的輸出紋理對象。
d.調(diào)用上文描述的接口,創(chuàng)建輸入紋理的著色器資源視圖(SRV)和輸出紋理的無序訪問視圖(UAV)。
e.執(zhí)行一系列的 DirectX 11 操作:
f.封裝輸出紋理對象為數(shù)據(jù)包,并 push 到模塊的輸出隊列中,供外界調(diào)用代碼獲取
init_d3d11 函數(shù)的實現(xiàn)如下:
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;
}
}
代碼主要實現(xiàn)的邏輯如下:
- 檢查設(shè)備和設(shè)備上下文對象是否已經(jīng)初始化,如果沒有則拋出異常。
- 獲取當前設(shè)備的特性級別(Feature Level),并檢查是否支持 D3D11 特性級別 11.0 以上,如果不支持則拋出異常。
- 檢查硬件是否支持 UAV(Unordered Access View)加載特性,如果不支持則拋出異常。
- 編譯創(chuàng)建計算著色器,用于圖像的 resize 操作。
- 創(chuàng)建采樣器狀態(tài)(Sampler State),用于計算著色器中的紋理采樣。
- 創(chuàng)建輸出大小的常量緩沖區(qū)(Constant Buffer),用于傳遞輸出圖像的寬度和高度信息給計算著色器。
- 將輸出大小的緩沖區(qū)數(shù)據(jù)初始化,并創(chuàng)建 D3D11 緩沖區(qū)對象。
- 設(shè)置標志位
inited
為 true,表示 D3D11 初始化完成。
從上文 unsafe_process 函數(shù)的邏輯可以獲知:init_d3d11 函數(shù)的調(diào)用時機是當模塊成功接收了外界傳入的 Device 和 DeviceContext 之后。
以上便是基于 DirectX 的圖像縮放模塊 D3DResizeModule 的設(shè)計與實現(xiàn),通過集成 BMF 開發(fā)環(huán)境,可以編譯出對應的 BMF 模塊
調(diào)用程序
本節(jié)將實現(xiàn)一個測試 demo 程序,用于測試和調(diào)用上文中所構(gòu)建的圖像縮放模塊,測試程序?qū)崿F(xiàn)如下:
#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 設(shè)備和上下文:
使用 D3D11CreateDevice
函數(shù)創(chuàng)建 D3D11 設(shè)備和上下文對象。
2.準備輸入圖像數(shù)據(jù):
從文件中讀取 RGBA 格式的測試圖像數(shù)據(jù)。
3.創(chuàng)建輸入圖像的 D3D11 紋理對象:
使用 CreateTexture2D
函數(shù)創(chuàng)建輸入圖像的 D3D11 紋理對象。
4.構(gòu)建 BMF Graph:
使用生成器模式創(chuàng)建并初始化 bmf::builder::Graph
添加 2 個輸入流(配置流和數(shù)據(jù)流)和輸出流,以及需要的參數(shù),通過 json 數(shù)據(jù)創(chuàng)建要縮放的圖像寬高。
導入名為 d3dresizemodule
的多媒體處理模塊,并連接輸入流和輸出流,調(diào)用 Start 函數(shù)啟動 Graph
5.填充輸入數(shù)據(jù):
將創(chuàng)建的 D3D11 設(shè)備和上下文對象填充到配置流中,驅(qū)動模塊內(nèi)部完成 DirectX 側(cè)的初始化
將創(chuàng)建的輸入圖像紋理送入到數(shù)據(jù)流中。
6.執(zhí)行 BMF Graph:
通過調(diào)用 graph.Generate
方法驅(qū)動模塊執(zhí)行處理流程,獲取數(shù)據(jù)幀
7.讀取輸出數(shù)據(jù)并寫入文件:
從生成的數(shù)據(jù)中獲取輸出圖像的 D3D11 紋理對象。
使用自定義函數(shù) ReadDataFromTexture
讀取紋理數(shù)據(jù)。
將輸出圖像數(shù)據(jù)寫入文件 output.rgb
中。
8.調(diào)用 Flush 方法清空任務(wù)隊列,同步釋放 DirectX 相關(guān)資源
9.異常處理:
在程序執(zhí)行過程中捕獲可能出現(xiàn)的異常,并輸出錯誤信息。
通過以上步驟,測試程序?qū)崿F(xiàn)了對 Direct3D 11 實現(xiàn)的圖像 resize 模塊的測試,并將處理結(jié)果保存到文件中,用于后續(xù)的分析和驗證。
構(gòu)建腳本與運行
本節(jié)主要介紹構(gòu)建并運行圖像縮放處理程序 Demo 的流程與步驟,同樣需要進入 msys2 環(huán)境,本文實現(xiàn)的 Demo 可以通過構(gòu)建腳本實現(xiàn)多種不同 Module 的選擇性構(gòu)建,Demo 項目的目錄結(jié)構(gòu)如下所示:
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 環(huán)境的編譯選項配置,modules 文件夾下包含了諸多用戶實現(xiàn)的 modules,其中 common 文件夾為公共文件目錄,其中實現(xiàn)了上文所述的 DirectX API,如果用戶想添加新實現(xiàn)的 module,可以在 modules 文件夾下再新建一個文件夾,命名為 module 名稱,內(nèi)部文件布局保持與 d3d11resizemodule 一致即可。
build.sh
腳本用于構(gòu)建項目,并支持一些參數(shù)來配置構(gòu)建過程。這個解釋使用方法和每個參數(shù)的意義和作用
--module
指定要編譯構(gòu)建的 module 名稱,需要與 modules 文件夾下的某個目錄對齊命名
--msvc
指定 msvc 版本,支持[2013, 2015, 2017, 2019, 2022]
--bmf_lite
指定 lite 控制標識位,用于控制內(nèi)部的一些 Win 的特化配置項
--test
控制是否編譯對應測試 demo 程序,內(nèi)部通過 cmake 控制
對于本例,本文使用的編譯構(gòu)建命令如下:
./build.sh --msvc=2022 --module=d3dresizemodule --bmf-lite --test
在構(gòu)建項目前,需要設(shè)置 BMF 庫的 INCLUDE 和 LIB 環(huán)境變量
export BMF_INCLUDE_DIR=/path/to/bmf/include
export BMF_LIBRARY_DIR=/path/to/bmf/lib
在 msys 執(zhí)行腳本后,會在本地生成 build_windows_lite_{preset} 文件夾,與 BMF 框架相同,可以使用 Visual Studio 界面交互進行項目構(gòu)建,或使用命令行直接構(gòu)建
構(gòu)建完成后,在項目目錄本地會生成 output 文件夾,切換至 output/bin/{preset}/Release/,執(zhí)行 ./test_d3dresizemodule.exe 程序,程序輸出如下
發(fā)現(xiàn)在本地生成了 output.rgb 文件
使用 ffplay 查看生成的圖像
ffplay -pix_fmt rgba -s 1080x1920 output.rgb
結(jié)果如下