Compare commits

2 Commits

Author SHA1 Message Date
RedNicStone
6a474963e5 Format chore 2026-08-26 11:49:16 +01:00
RedNicStone
436808a0d0 Added zerocopy, improved stream robustness and reduced latency 2026-08-26 11:41:54 +01:00
22 changed files with 1645 additions and 283 deletions

5
.gitignore vendored
View File

@@ -55,3 +55,8 @@ CMakeUserPresets.json
build/ build/
godot/ godot/
build-editor/
# Some testing files
project/*.mp4
project/*.mov

View File

@@ -74,6 +74,12 @@ target_sources(${LIBNAME}
src/register_types.h src/register_types.h
src/gdvapivideostream_class.cpp src/gdvapivideostream_class.cpp
src/gdvapivideostream_class.h src/gdvapivideostream_class.h
src/vulkan_detour.h
src/vulkan_detour.cpp
src/vaapi_decoder.h
src/vaapi_decoder.cpp
src/vk_image_import.h
src/vk_image_import.cpp
) )
# Fetch a list of the xml files to use for documentation and add to our target # Fetch a list of the xml files to use for documentation and add to our target

Binary file not shown.

View File

@@ -1,6 +1,11 @@
extends Node extends Node
func _ready() -> void: func _ready() -> void:
#$GDVAPIVideoStream.begin("udp://0.0.0.0:5000") $GDVAPIVideoStream.queue_depth = 4 # default; proves the property binding
pass $GDVAPIVideoStream.begin("/tmp/gdvapi_test.sdp")
func _process(_delta: float) -> void:
var tex: Texture2D = $GDVAPIVideoStream.get_texture()
if tex:
if $TextureRect.texture != tex:
$TextureRect.texture = tex

View File

@@ -6,5 +6,13 @@
script = ExtResource("1_jdh55") script = ExtResource("1_jdh55")
[node name="GDVAPIVideoStream" type="GDVAPIVideoStream" parent="." unique_id=1053894928] [node name="GDVAPIVideoStream" type="GDVAPIVideoStream" parent="." unique_id=1053894928]
offset_right = 40.0
offset_bottom = 40.0 [node name="TextureRect" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
stretch_mode = 6

Binary file not shown.

View File

@@ -1,4 +0,0 @@
#!/bin/sh
printf '\033c\033]0;%s\a' godot cpp template
base_path="$(dirname "$(realpath "$0")")"
"$base_path/godot cpp template.x86_64" "$@"

View File

@@ -21,5 +21,7 @@ config/icon="res://icon.svg"
[display] [display]
display/window/vsync/vsync_mode=0
window/size/viewport_width=1920 window/size/viewport_width=1920
window/size/viewport_height=1080 window/size/viewport_height=1080

Binary file not shown.

32
server-test4k.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import subprocess, sys
fps = sys.argv[1] if len(sys.argv) > 1 else "60"
size = sys.argv[2] if len(sys.argv) > 2 else "3840x2160"
port = sys.argv[3] if len(sys.argv) > 3 else "5000"
bitrate = sys.argv[4] if len(sys.argv) > 4 else "40M"
cmd = [
"ffmpeg", "-hide_banner", "-v", "error",
"-f", "lavfi", "-i",
f"testsrc2=size={size}:rate={fps}:duration=1000",
"-an",
"-c:v", "libx265",
"-preset", "ultrafast",
"-tune", "zerolatency",
"-pix_fmt", "yuv420p",
"-b:v", bitrate,
"-x265-params", "bframes=0:keyint=30:scenecut=0",
"-f", "rtp",
"-payload_type", "96",
"-sdp_file", "/tmp/gdvapi_test4k.sdp",
f"rtp://127.0.0.1:{port}",
]
print("running:", " ".join(cmd))
try:
subprocess.run(cmd, check=True)
except KeyboardInterrupt:
pass
except subprocess.CalledProcessError as e:
print("ffmpeg exited", e.returncode)
sys.exit(1)

View File

@@ -4,9 +4,15 @@
#include "godot_cpp/classes/rd_texture_view.hpp" #include "godot_cpp/classes/rd_texture_view.hpp"
#include "godot_cpp/classes/rendering_device.hpp" #include "godot_cpp/classes/rendering_device.hpp"
#include "godot_cpp/classes/rendering_server.hpp" #include "godot_cpp/classes/rendering_server.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#include "godot_cpp/variant/color.hpp" #include "godot_cpp/variant/color.hpp"
#include "godot_cpp/variant/rect2.hpp" #include "godot_cpp/variant/rect2.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#include "vaapi_decoder.h"
#include "vk_image_import.h"
#include <unistd.h>
#include <cstdlib>
extern "C" { extern "C" {
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
@@ -19,32 +25,59 @@ extern "C" {
using namespace godot; using namespace godot;
enum HWMode { HW_NONE, enum HWMode { HW_NONE,
HW_VULKAN, HW_VULKAN,
HW_VAAPI }; HW_VAAPI };
static HWMode active_hw_mode = HW_NONE; static HWMode active_hw_mode = HW_NONE;
void GDVAPIVideoStream::_bind_methods() { void GDVAPIVideoStream::_bind_methods() {
godot::ClassDB::bind_method(D_METHOD("begin", "source"), &GDVAPIVideoStream::begin); ClassDB::bind_method(D_METHOD("begin", "source"), &GDVAPIVideoStream::begin);
ClassDB::bind_method(D_METHOD("get_texture"), &GDVAPIVideoStream::get_texture);
ClassDB::bind_method(D_METHOD("get_desired_width"), &GDVAPIVideoStream::get_desired_width);
ClassDB::bind_method(D_METHOD("set_desired_width", "v"), &GDVAPIVideoStream::set_desired_width);
ClassDB::bind_method(D_METHOD("get_desired_height"), &GDVAPIVideoStream::get_desired_height);
ClassDB::bind_method(D_METHOD("set_desired_height", "v"), &GDVAPIVideoStream::set_desired_height);
ClassDB::bind_method(D_METHOD("get_desired_width"), &GDVAPIVideoStream::get_desired_width); ADD_PROPERTY(PropertyInfo(Variant::INT, "desired_width"), "set_desired_width", "get_desired_width");
ClassDB::bind_method(D_METHOD("set_desired_width", "v"), &GDVAPIVideoStream::set_desired_width); ADD_PROPERTY(PropertyInfo(Variant::INT, "desired_height"), "set_desired_height", "get_desired_height");
ClassDB::bind_method(D_METHOD("get_desired_height"), &GDVAPIVideoStream::get_desired_height);
ClassDB::bind_method(D_METHOD("set_desired_height", "v"), &GDVAPIVideoStream::set_desired_height);
ADD_PROPERTY(PropertyInfo(Variant::INT, "desired_width"), "set_desired_width", "get_desired_width"); ClassDB::bind_method(D_METHOD("set_reorder_buffer_ms", "v"), &GDVAPIVideoStream::set_reorder_buffer_ms);
ADD_PROPERTY(PropertyInfo(Variant::INT, "desired_height"), "set_desired_height", "get_desired_height"); ClassDB::bind_method(D_METHOD("get_reorder_buffer_ms"), &GDVAPIVideoStream::get_reorder_buffer_ms);
ClassDB::bind_method(D_METHOD("set_demux_fifo_bytes", "v"), &GDVAPIVideoStream::set_demux_fifo_bytes);
ClassDB::bind_method(D_METHOD("get_demux_fifo_bytes"), &GDVAPIVideoStream::get_demux_fifo_bytes);
ClassDB::bind_method(D_METHOD("set_socket_buffer_bytes", "v"), &GDVAPIVideoStream::set_socket_buffer_bytes);
ClassDB::bind_method(D_METHOD("get_socket_buffer_bytes"), &GDVAPIVideoStream::get_socket_buffer_bytes);
ClassDB::bind_method(D_METHOD("set_reorder_queue_size", "v"), &GDVAPIVideoStream::set_reorder_queue_size);
ClassDB::bind_method(D_METHOD("get_reorder_queue_size"), &GDVAPIVideoStream::get_reorder_queue_size);
ClassDB::bind_method(D_METHOD("set_io_timeout_ms", "v"), &GDVAPIVideoStream::set_io_timeout_ms);
ClassDB::bind_method(D_METHOD("get_io_timeout_ms"), &GDVAPIVideoStream::get_io_timeout_ms);
ClassDB::bind_method(D_METHOD("set_queue_depth", "v"), &GDVAPIVideoStream::set_queue_depth);
ClassDB::bind_method(D_METHOD("get_queue_depth"), &GDVAPIVideoStream::get_queue_depth);
ClassDB::bind_method(D_METHOD("set_frame_latency_ms", "v"), &GDVAPIVideoStream::set_frame_latency_ms);
ClassDB::bind_method(D_METHOD("get_frame_latency_ms"), &GDVAPIVideoStream::get_frame_latency_ms);
ClassDB::bind_method(D_METHOD("set_vpp_half_res", "v"), &GDVAPIVideoStream::set_vpp_half_res);
ClassDB::bind_method(D_METHOD("get_vpp_half_res"), &GDVAPIVideoStream::get_vpp_half_res);
ADD_PROPERTY(PropertyInfo(Variant::INT, "reorder_buffer_ms"), "set_reorder_buffer_ms", "get_reorder_buffer_ms");
ADD_PROPERTY(PropertyInfo(Variant::INT, "demux_fifo_bytes"), "set_demux_fifo_bytes", "get_demux_fifo_bytes");
ADD_PROPERTY(PropertyInfo(Variant::INT, "socket_buffer_bytes"), "set_socket_buffer_bytes", "get_socket_buffer_bytes");
ADD_PROPERTY(PropertyInfo(Variant::INT, "reorder_queue_size"), "set_reorder_queue_size", "get_reorder_queue_size");
ADD_PROPERTY(PropertyInfo(Variant::INT, "io_timeout_ms"), "set_io_timeout_ms", "get_io_timeout_ms");
ADD_PROPERTY(PropertyInfo(Variant::INT, "queue_depth"), "set_queue_depth", "get_queue_depth");
ADD_PROPERTY(PropertyInfo(Variant::INT, "frame_latency_ms"), "set_frame_latency_ms", "get_frame_latency_ms");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "vpp_half_res"), "set_vpp_half_res", "get_vpp_half_res");
ClassDB::bind_method(D_METHOD("get_frame_count"), &GDVAPIVideoStream::get_frame_count);
ClassDB::bind_method(D_METHOD("has_frame"), &GDVAPIVideoStream::has_frame);
ClassDB::bind_method(D_METHOD("get_stream_width"), &GDVAPIVideoStream::get_stream_width);
ClassDB::bind_method(D_METHOD("get_stream_height"), &GDVAPIVideoStream::get_stream_height);
} }
static enum AVPixelFormat get_hw_format(AVCodecContext *ctx, const enum AVPixelFormat *formats) { static enum AVPixelFormat get_hw_format(AVCodecContext *ctx, const enum AVPixelFormat *formats) {
for (const enum AVPixelFormat *p = formats; *p != -1; p++) { for (const enum AVPixelFormat *p = formats; *p != -1; p++) {
if (active_hw_mode == HW_VULKAN && *p == AV_PIX_FMT_VULKAN) { if (active_hw_mode == HW_VULKAN && *p == AV_PIX_FMT_VULKAN)
return AV_PIX_FMT_VULKAN; return AV_PIX_FMT_VULKAN;
} if (active_hw_mode == HW_VAAPI && *p == AV_PIX_FMT_VAAPI)
if (active_hw_mode == HW_VAAPI && *p == AV_PIX_FMT_VAAPI) {
return AV_PIX_FMT_VAAPI; return AV_PIX_FMT_VAAPI;
}
} }
UtilityFunctions::printerr("[gdvapi] hardware decoding unavailable"); UtilityFunctions::printerr("[gdvapi] hardware decoding unavailable");
active_hw_mode = HW_NONE; active_hw_mode = HW_NONE;
@@ -52,146 +85,333 @@ static enum AVPixelFormat get_hw_format(AVCodecContext *ctx, const enum AVPixelF
} }
void GDVAPIVideoStream::begin(const Variant &source) { void GDVAPIVideoStream::begin(const Variant &source) {
String res_path = source; stream_source = source;
String global_path = ProjectSettings::get_singleton()->globalize_path(res_path);
CharString path_utf8 = global_path.utf8();
fmt_ctx = nullptr;
if (avformat_open_input(&fmt_ctx, path_utf8.get_data(), nullptr, nullptr) < 0) {
UtilityFunctions::printerr("[gdvapi] failed to open input stream");
return;
}
if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
return;
}
video_stream_idx = -1;
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_idx = i;
break;
}
}
if (video_stream_idx == -1) return;
AVStream *stream = fmt_ctx->streams[video_stream_idx];
if (stream->avg_frame_rate.den > 0 && stream->avg_frame_rate.num > 0) {
double fps = av_q2d(stream->avg_frame_rate);
frame_delay = 1.0 / fps;
UtilityFunctions::print("[gdvapi] fps: ", fps, " frame delay: ", frame_delay, "s");
} else {
frame_delay = 1.0 / 60.0; // fallback
}
AVCodecParameters *codec_par = fmt_ctx->streams[video_stream_idx]->codecpar;
codec = avcodec_find_decoder(codec_par->codec_id);
if (!codec)
return;
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx)
return;
if (avcodec_parameters_to_context(codec_ctx, codec_par) < 0)
return;
active_hw_mode = HW_VULKAN;
if (av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_VULKAN, nullptr, nullptr, 0) < 0) {
UtilityFunctions::print("[gdvapi] vulkan video extensions missing on driver, trying va-api");
active_hw_mode = HW_VAAPI;
if (av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) < 0) {
UtilityFunctions::print("[gdvapi] va-api acceleration unavailable. using software");
active_hw_mode = HW_NONE;
}
}
if (active_hw_mode != HW_NONE) {
codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
codec_ctx->get_format = get_hw_format;
}
if (avcodec_open2(codec_ctx, codec, nullptr) < 0)
return;
frame = av_frame_alloc();
sw_frame = av_frame_alloc();
packet = av_packet_alloc();
width = codec_ctx->width;
height = codec_ctx->height;
texture_init();
sws_ctx = sws_getContext(
width, height, codec_ctx->pix_fmt,
width, height, AV_PIX_FMT_RGBA,
SWS_BILINEAR, nullptr, nullptr, nullptr);
time_accumulator = 0.0;
set_process(true);
} }
void GDVAPIVideoStream::_process(double delta) { void GDVAPIVideoStream::_process(double delta) {
if (!fmt_ctx || !codec_ctx) return; (void)delta;
if (!stream_started) {
time_accumulator += delta; stream_started = true;
RenderingDevice *rd = RenderingServer::get_singleton()->get_rendering_device();
if (time_accumulator < frame_delay) { if (rd) {
return; uint64_t vk_dev_u = rd->get_driver_resource(
} RenderingDevice::DRIVER_RESOURCE_LOGICAL_DEVICE, RID(), 0);
vk_dev = (VkDevice)vk_dev_u;
time_accumulator -= frame_delay; uint64_t vk_inst_u = rd->get_driver_resource(
RenderingDevice::DRIVER_RESOURCE_VULKAN_INSTANCE, RID(), 0);
bool frame_finished = false; vk_inst = (VkInstance)vk_inst_u;
while (!frame_finished && av_read_frame(fmt_ctx, packet) >= 0) { }
if (packet->stream_index == video_stream_idx) { wall_base = std::chrono::steady_clock::now();
if (avcodec_send_packet(codec_ctx, packet) >= 0) { String uri = stream_source;
while (avcodec_receive_frame(codec_ctx, frame) >= 0) { if (uri.ends_with(".sdp") || uri.ends_with(".mp4") || uri.ends_with(".mov") || uri.begins_with("/"))
frame_finished = true; worker_src = uri.utf8().get_data();
else {
RenderingDevice *rd = RenderingServer::get_singleton()->get_rendering_device(); const char *sdp = "/tmp/gdvapi_live.sdp";
if (!rd || !texture_rd_rid.is_valid()) String host = "127.0.0.1", port = "5000";
continue; if (uri.begins_with("udp://")) {
String rest = uri.substr(6);
if (frame->format == AV_PIX_FMT_VULKAN || frame->format == AV_PIX_FMT_VAAPI) { int c = rest.find(":");
if (av_hwframe_transfer_data(sw_frame, frame, 0) >= 0) { if (c > 0) {
PackedByteArray gpu_bytes; host = rest.substr(0, c);
gpu_bytes.resize(width * height * 4); port = rest.substr(c + 1);
uint8_t *dest_pointers[4] = { gpu_bytes.ptrw(), nullptr, nullptr, nullptr };
int dest_linesizes[4] = { width * 4, 0, 0, 0 };
enum AVPixelFormat src_fmt = (enum AVPixelFormat)sw_frame->format;
struct SwsContext *hw_sws = sws_getContext(
width, height, src_fmt,
width, height, AV_PIX_FMT_RGBA,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (hw_sws) {
sws_scale(hw_sws, sw_frame->data, sw_frame->linesize, 0, height, dest_pointers, dest_linesizes);
sws_freeContext(hw_sws);
rd->texture_update(texture_rd_rid, 0, gpu_bytes);
}
}
} else if (frame->data[0] != nullptr) {
PackedByteArray rgba_data;
rgba_data.resize(width * height * 4);
uint8_t *dest_pointers[4] = { rgba_data.ptrw(), nullptr, nullptr, nullptr };
int dest_linesizes[4] = { width * 4, 0, 0, 0 };
if (sws_ctx) {
sws_scale(sws_ctx, frame->data, frame->linesize, 0, height, dest_pointers, dest_linesizes);
rd->texture_update(texture_rd_rid, 0, rgba_data);
}
}
queue_redraw();
} }
} }
FILE *f = fopen(sdp, "w");
if (f) {
fprintf(f, "v=0\no=- 0 0 IN IP4 %s\ns=Live\nc=IN IP4 %s\nt=0 0\n"
"m=video %s RTP/AVP 96\na=rtpmap:96 H265/90000\n",
host.utf8().get_data(), host.utf8().get_data(),
port.utf8().get_data());
fclose(f);
}
worker_src = sdp;
} }
av_packet_unref(packet); source_live = uri.ends_with(".sdp") || uri.begins_with("udp://");
worker_thr = std::thread(&GDVAPIVideoStream::worker_loop, this);
} }
if (size_pending)
size_pending = false;
consume_ready();
}
void GDVAPIVideoStream::worker_loop() {
while (!worker_stop) {
if (!va_decoder.open(worker_src.c_str(), decoder_opts)) {
fprintf(stderr, "[gdvapi] worker: open failed (%s)%s\n",
worker_src.c_str(), source_live ? " - retrying" : "");
if (source_live) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
continue;
}
std::lock_guard<std::mutex> lk(q_mu);
stream_eof = true;
q_cv.notify_all();
break;
}
{
std::lock_guard<std::mutex> lk(q_mu);
size_pending = true;
pending_w = va_decoder.width();
pending_h = va_decoder.height();
worker_fps = va_decoder.fps();
for (int i = 0; i < gdvapi::VAAPIDecoder::kRgbaPool; i++)
slot_acked[i].store(true);
}
while (!worker_stop) {
{
std::unique_lock<std::mutex> lk(q_mu);
q_cv.wait(lk, [&] {
return worker_stop || slot_acked[vpp_slot].load();
});
if (worker_stop)
break;
}
AVFrame *avf = nullptr;
if (!va_decoder.next_frame(avf)) {
if (source_live) {
if (getenv("GDVAPI_DEBUG_WORKER"))
fprintf(stderr, "[gdvapi] worker: live gap, retrying\n");
va_decoder.close();
{
std::lock_guard<std::mutex> lk(q_mu);
pts_base = -1.0; // re-anchor pacing to the next epoch
last_pts = -1.0;
wall_base = std::chrono::steady_clock::now();
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
break;
}
if (getenv("GDVAPI_DEBUG_WORKER"))
fprintf(stderr, "[gdvapi] worker: next_frame fail/eof\n");
std::lock_guard<std::mutex> lk(q_mu);
stream_eof = true;
q_cv.notify_all();
break;
}
gdvapi::DecodedFrame f;
if (!va_decoder.convert_frame_to_rgba_slot(avf, vpp_slot, f)) {
if (getenv("GDVAPI_DEBUG_WORKER"))
fprintf(stderr, "[gdvapi] worker: convert fail slot=%d\n", vpp_slot);
av_frame_unref(avf);
continue;
}
av_frame_unref(avf);
if (getenv("GDVAPI_DEBUG_WORKER"))
fprintf(stderr, "[gdvapi] worker: push fd=%d slot=%d\n", f.fd, vpp_slot);
ReadyFrame rf;
rf.fd = f.fd;
rf.w = f.width;
rf.h = f.height;
rf.pts_sec = f.pts_sec;
rf.drm_fourcc = f.drm_fourcc;
rf.slot = vpp_slot;
slot_acked[vpp_slot].store(false);
{
std::lock_guard<std::mutex> lk(q_mu);
if (worker_stop) {
::close(f.fd);
break;
}
while (ready_q.size() >= queue_max) {
ReadyFrame old = ready_q.front();
ready_q.pop_front();
slot_acked[old.slot].store(true);
::close(old.fd);
}
ready_q.push_back(rf);
vpp_slot = (vpp_slot + 1) % gdvapi::VAAPIDecoder::kRgbaPool;
q_cv.notify_all();
}
}
va_decoder.close();
if (!source_live)
break;
}
std::lock_guard<std::mutex> lk(q_mu);
worker_finished = true;
q_cv.notify_all();
}
void GDVAPIVideoStream::stop_worker() {
{
std::lock_guard<std::mutex> lk(q_mu);
worker_stop = true;
q_cv.notify_all();
}
if (worker_thr.joinable()) {
worker_thr.join();
worker_thr = std::thread();
}
std::lock_guard<std::mutex> lk(q_mu);
while (!ready_q.empty()) {
::close(ready_q.front().fd);
ready_q.pop_front();
}
}
void GDVAPIVideoStream::consume_ready() {
RenderingDevice *rd = RenderingServer::get_singleton()->get_rendering_device();
if (!rd || !vk_dev)
return;
const double kDropAhead = max_frame_latency;
auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(now - wall_base).count();
ReadyFrame rf;
{
std::lock_guard<std::mutex> lk(q_mu);
if (ready_q.empty())
return;
rf = ready_q.front();
}
bool new_epoch = false;
if (rf.pts_sec > 0.0 && last_pts > 0.0) {
if (rf.pts_sec - last_pts < -1.0) {
last_pts = -1.0;
pts_base = -1.0;
wall_base = std::chrono::steady_clock::now();
new_epoch = true;
} else if (rf.pts_sec - last_pts > 4.0) {
last_pts = rf.pts_sec;
pts_base = -1.0;
wall_base = std::chrono::steady_clock::now();
new_epoch = true;
} else if (rf.pts_sec <= last_pts) {
{
std::lock_guard<std::mutex> lk(q_mu);
ready_q.pop_front();
}
slot_acked[rf.slot].store(true);
q_cv.notify_all();
::close(rf.fd);
return;
}
}
last_pts = rf.pts_sec;
if (rf.pts_sec > 0.0) {
if (pts_base < 0.0) {
pts_base = rf.pts_sec;
wall_base = std::chrono::steady_clock::now() - std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<double>(rf.pts_sec));
elapsed = 0.0;
}
const double stream_pos = rf.pts_sec - pts_base;
std::lock_guard<std::mutex> lk(q_mu);
const bool have_backlog = ready_q.size() > 1;
if (have_backlog && stream_pos - elapsed > (1.0 / 30.0)) {
ready_q.pop_front();
slot_acked[rf.slot].store(true);
q_cv.notify_all();
::close(rf.fd);
return;
}
}
if (rf.pts_sec > 0.0 && elapsed - rf.pts_sec > kDropAhead) {
{
std::lock_guard<std::mutex> lk(q_mu);
ready_q.pop_front();
}
slot_acked[rf.slot].store(true);
q_cv.notify_all();
::close(rf.fd);
return;
}
{
std::lock_guard<std::mutex> lk(q_mu);
ready_q.pop_front();
}
slot_acked[rf.slot].store(true);
q_cv.notify_all();
gdvapi::DmaBufSurface surf;
surf.fd = rf.fd;
surf.width = rf.w;
surf.height = rf.h;
surf.drm_fourcc = rf.drm_fourcc;
surf.fourcc = rf.drm_fourcc ? rf.drm_fourcc : 0x34325241;
if (present_image) {
gdvapi::destroy_imported_image(vk_dev, present_image, present_mem);
present_image = VK_NULL_HANDLE;
present_mem = VK_NULL_HANDLE;
}
VkImage img = VK_NULL_HANDLE;
VkDeviceMemory mem = VK_NULL_HANDLE;
img = gdvapi::import_dma_buf_image(vk_dev, surf, &mem);
if (!img || !mem) {
::close(rf.fd);
return;
}
present_image = img;
present_mem = mem;
::close(rf.fd);
if (!out_tex[0].is_valid()) {
for (int i = 0; i < 2; i++) {
Ref<RDTextureFormat> fmt;
fmt.instantiate();
fmt->set_texture_type(RenderingDevice::TEXTURE_TYPE_2D);
fmt->set_format(RenderingDevice::DATA_FORMAT_B8G8R8A8_UNORM);
fmt->set_width(rf.w);
fmt->set_height(rf.h);
fmt->set_depth(1);
fmt->set_array_layers(1);
fmt->set_mipmaps(1);
fmt->set_usage_bits(RenderingDevice::TEXTURE_USAGE_SAMPLING_BIT |
RenderingDevice::TEXTURE_USAGE_CAN_COPY_TO_BIT |
RenderingDevice::TEXTURE_USAGE_CAN_COPY_FROM_BIT);
Ref<RDTextureView> v;
v.instantiate();
out_tex[i] = rd->texture_create(fmt, v, TypedArray<PackedByteArray>());
}
if (!out_tex[0].is_valid()) {
fprintf(stderr, "[gdvapi] out_tex create failed\n");
return;
}
}
RID wrap = rd->texture_create_from_extension(
RenderingDevice::TEXTURE_TYPE_2D,
RenderingDevice::DATA_FORMAT_B8G8R8A8_UNORM,
RenderingDevice::TEXTURE_SAMPLES_1,
RenderingDevice::TEXTURE_USAGE_SAMPLING_BIT |
RenderingDevice::TEXTURE_USAGE_CAN_COPY_FROM_BIT,
uint64_t(img), rf.w, rf.h, 1, 1);
const int w = (draw_idx + 1) % 2;
if (wrap.is_valid()) {
rd->texture_copy(wrap, out_tex[w],
godot::Vector3(0, 0, 0), godot::Vector3(0, 0, 0),
godot::Vector3(rf.w, rf.h, 1),
0, 0, 0, 0);
rd->free_rid(wrap);
}
if (!draw_tex[w].is_valid()) {
draw_tex[w].instantiate();
draw_tex[w]->set_texture_rd_rid(out_tex[w]);
}
draw_idx = w;
present_wall_sec = std::chrono::duration<double>(
std::chrono::steady_clock::now() - wall_base)
.count();
if (!have_presentable) {
have_presentable = true;
}
if (getenv("GDVAPI_DEBUG_PACING")) {
using namespace std::chrono;
double el = duration<double>(steady_clock::now() - wall_base).count();
fprintf(stderr, "[gdvapi] present #%d pts=%.3f wall=%.3f\n",
present_count + 1, rf.pts_sec, el);
}
present_count++;
} }
GDVAPIVideoStream::~GDVAPIVideoStream() { GDVAPIVideoStream::~GDVAPIVideoStream() {
@@ -206,119 +426,112 @@ void GDVAPIVideoStream::_notification(int p_what) {
} }
} }
void GDVAPIVideoStream::texture_init() {
RenderingDevice *rd = RenderingServer::get_singleton()->get_rendering_device();
if (!rd)
return;
if (texture_rd_rid.is_valid()) {
rd->free_rid(texture_rd_rid);
}
Ref<RDTextureFormat> format;
format.instantiate();
format->set_texture_type(RenderingDevice::TEXTURE_TYPE_2D);
format->set_format(RenderingDevice::DATA_FORMAT_R8G8B8A8_UNORM);
format->set_width(width);
format->set_height(height);
format->set_depth(1);
format->set_array_layers(1);
format->set_mipmaps(1);
format->set_usage_bits(RenderingDevice::TEXTURE_USAGE_SAMPLING_BIT |
RenderingDevice::TEXTURE_USAGE_CAN_UPDATE_BIT);
PackedByteArray byte_array;
byte_array.resize(width * height * 4);
TypedArray<PackedByteArray> data;
data.push_back(byte_array);
Ref<RDTextureView> view;
view.instantiate();
texture_rd_rid = rd->texture_create(format, view, data);
if (texture_rd_rid.is_valid()) {
godot_texture.instantiate();
godot_texture->set_texture_rd_rid(texture_rd_rid);
}
}
void GDVAPIVideoStream::cleanup() { void GDVAPIVideoStream::cleanup() {
set_process(false); set_process(false);
stop_worker();
RenderingDevice *rd = RenderingServer::get_singleton()->get_rendering_device(); RenderingDevice *rd = RenderingServer::get_singleton()->get_rendering_device();
if (present_image && rd) {
gdvapi::destroy_imported_image(vk_dev, present_image, present_mem);
present_image = VK_NULL_HANDLE;
present_mem = VK_NULL_HANDLE;
}
for (int i = 0; i < 2; i++) {
if (out_tex[i].is_valid()) {
rd->free_rid(out_tex[i]);
out_tex[i] = RID();
}
}
va_decoder.close();
stream_eof = true;
godot_texture.unref(); godot_texture.unref();
if (rd && texture_rd_rid.is_valid()) {
rd->free_rid(texture_rd_rid);
texture_rd_rid = RID();
}
if (sws_ctx) {
sws_freeContext(sws_ctx);
sws_ctx = nullptr;
}
if (frame) {
av_frame_free(&frame);
}
if (sw_frame) {
av_frame_free(&sw_frame);
}
if (packet) {
av_packet_free(&packet);
}
if (codec_ctx) {
avcodec_free_context(&codec_ctx);
}
if (hw_device_ctx) {
av_buffer_unref(&hw_device_ctx);
hw_device_ctx = nullptr;
}
if (fmt_ctx) {
avformat_close_input(&fmt_ctx);
}
}
void GDVAPIVideoStream::_draw() {
UtilityFunctions::print("_draw called");
if (godot_texture.is_valid()) {
draw_texture(godot_texture, Point2(0, 0));
} else {
Rect2 rect = Rect2(0, 0, desiredWidth, desiredHeight);
Color color = Color(1.0, 1.0, 1.0, 1.0);
draw_rect(rect, color);
}
} }
void GDVAPIVideoStream::set_desired_width(const int v) { void GDVAPIVideoStream::set_desired_width(const int v) {
if (desiredWidth != v) { if (desiredWidth != v) {
desiredWidth = v; desiredWidth = v;
queue_redraw(); }
}
} }
void GDVAPIVideoStream::set_desired_height(const int v) { void GDVAPIVideoStream::set_desired_height(const int v) {
if (desiredHeight != v) { if (desiredHeight != v) {
desiredHeight = v; desiredHeight = v;
queue_redraw(); }
}
} }
int GDVAPIVideoStream::get_desired_width() const { int GDVAPIVideoStream::get_desired_width() const {
return desiredWidth; return desiredWidth;
} }
int GDVAPIVideoStream::get_desired_height() const { int GDVAPIVideoStream::get_desired_height() const {
return desiredHeight; return desiredHeight;
} }
void GDVAPIVideoStream::set_reorder_buffer_ms(const int v) {
decoder_opts.max_delay_us = v * 1000;
}
int GDVAPIVideoStream::get_reorder_buffer_ms() const {
return decoder_opts.max_delay_us / 1000;
}
void GDVAPIVideoStream::set_demux_fifo_bytes(const int v) {
decoder_opts.demux_fifo_bytes = v;
}
int GDVAPIVideoStream::get_demux_fifo_bytes() const {
return decoder_opts.demux_fifo_bytes;
}
void GDVAPIVideoStream::set_socket_buffer_bytes(const int v) {
decoder_opts.socket_buffer_bytes = v;
}
int GDVAPIVideoStream::get_socket_buffer_bytes() const {
return decoder_opts.socket_buffer_bytes;
}
void GDVAPIVideoStream::set_reorder_queue_size(const int v) {
decoder_opts.reorder_queue_size = v;
}
int GDVAPIVideoStream::get_reorder_queue_size() const {
return decoder_opts.reorder_queue_size;
}
void GDVAPIVideoStream::set_io_timeout_ms(const int v) {
decoder_opts.io_timeout_us = v * 1000;
}
int GDVAPIVideoStream::get_io_timeout_ms() const {
return decoder_opts.io_timeout_us / 1000;
}
void GDVAPIVideoStream::set_queue_depth(const int v) {
queue_max = v < 1 ? 1 : (v > 8 ? 8 : v);
}
int GDVAPIVideoStream::get_queue_depth() const {
return queue_max;
}
void GDVAPIVideoStream::set_frame_latency_ms(const int v) {
int c = v < 5 ? 5 : v;
max_frame_latency = c / 1000.0;
}
int GDVAPIVideoStream::get_frame_latency_ms() const {
return (int)(max_frame_latency * 1000.0);
}
void GDVAPIVideoStream::set_vpp_half_res(const bool v) {
decoder_opts.vpp_div = v ? 2 : 1;
}
bool GDVAPIVideoStream::get_vpp_half_res() const {
return decoder_opts.vpp_div == 2;
}
void GDVAPIVideoStream::_ready() { void GDVAPIVideoStream::_ready() {
UtilityFunctions::print("_ready called"); UtilityFunctions::print("_ready called");
queue_redraw();
RenderingDevice *rd = RenderingServer::get_singleton()->get_rendering_device();
if (rd) {
uint64_t vk_dev = rd->get_driver_resource(
RenderingDevice::DRIVER_RESOURCE_LOGICAL_DEVICE, RID(), 0);
uint64_t vk_phys = rd->get_driver_resource(
RenderingDevice::DRIVER_RESOURCE_PHYSICAL_DEVICE, RID(), 0);
uint64_t vk_inst = rd->get_driver_resource(
RenderingDevice::DRIVER_RESOURCE_VULKAN_INSTANCE, RID(), 0);
UtilityFunctions::print("[gdvapi] VkDevice=", vk_dev,
" VkPhysicalDevice=", vk_phys,
" VkInstance=", vk_inst);
} else
UtilityFunctions::print("[gdvapi] no RenderingDevice");
} }

View File

@@ -1,8 +1,18 @@
#pragma once #pragma once
#include "godot_cpp/classes/control.hpp" #include "godot_cpp/classes/node.hpp"
#include "godot_cpp/classes/texture2drd.hpp" #include "godot_cpp/classes/texture2drd.hpp"
#include "godot_cpp/variant/rid.hpp" #include "godot_cpp/variant/rid.hpp"
#include "vaapi_decoder.h"
#include <vulkan/vulkan_core.h>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <mutex>
#include <string>
#include <thread>
extern "C" { extern "C" {
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
@@ -13,32 +23,63 @@ extern "C" {
namespace godot { namespace godot {
class GDVAPIVideoStream : public Control { class GDVAPIVideoStream : public Node {
GDCLASS(GDVAPIVideoStream, Control) GDCLASS(GDVAPIVideoStream, Node)
private: private:
AVFormatContext *fmt_ctx = nullptr; struct ReadyFrame {
AVCodecContext *codec_ctx = nullptr; int fd = -1;
const AVCodec *codec = nullptr; int w = 0, h = 0;
AVFrame *frame = nullptr; double pts_sec = 0.0;
AVPacket *packet = nullptr; uint32_t drm_fourcc = 0;
struct SwsContext *sws_ctx = nullptr; int slot = -1;
int video_stream_idx = -1; };
int width = 256; std::thread worker_thr;
int height = 256; std::mutex q_mu;
AVBufferRef *hw_device_ctx = nullptr; std::condition_variable q_cv;
AVFrame *sw_frame = nullptr; std::deque<ReadyFrame> ready_q;
std::atomic<bool> slot_acked[gdvapi::VAAPIDecoder::kRgbaPool] = {};
int queue_max = 4; // bounded FIFO depth
double max_frame_latency = 0.5; // drop frames older than this
bool worker_stop = false;
bool worker_finished = false;
std::chrono::steady_clock::time_point wall_base;
double worker_fps = 30.0;
bool source_live = false; // RTP/SDP: retry on gaps instead of EOF
double frame_delay = 0.0; gdvapi::VAAPIDecoder va_decoder;
double time_accumulator = 0.0; gdvapi::DecoderOptions decoder_opts;
int vpp_slot = 0; // rotates 0..kRgbaPool-1
std::string worker_src;
RID texture_rd_rid; VkDevice vk_dev = VK_NULL_HANDLE;
VkInstance vk_inst = VK_NULL_HANDLE;
VkImage present_image = VK_NULL_HANDLE;
VkDeviceMemory present_mem = VK_NULL_HANDLE;
RID out_tex[2];
int out_tex_idx = 0;
Ref<Texture2DRD> draw_tex[2];
int draw_idx = 0;
double present_wall_sec = -1.0; // for debug overlay
double last_pts = -1.0; // monotonic order gate
double pts_base = -1.0; // first-presented PTS
bool stream_started = false;
bool stream_eof = false;
bool have_presentable = false;
String stream_source = "udp://127.0.0.1:5000"; // set via begin()
int present_count = 0;
bool size_pending = false;
int pending_w = 0, pending_h = 0;
Ref<Texture2DRD> godot_texture; Ref<Texture2DRD> godot_texture;
int desiredWidth = 256; int desiredWidth = 256;
int desiredHeight = 256; int desiredHeight = 256;
void worker_loop();
void stop_worker();
void consume_ready();
void cleanup(); void cleanup();
void texture_init(); void texture_init();
@@ -50,18 +91,40 @@ public:
~GDVAPIVideoStream() override; ~GDVAPIVideoStream() override;
void begin(const Variant &source); void begin(const Variant &source);
Ref<Texture2DRD> get_texture() const { return draw_tex[draw_idx]; }
void set_reorder_buffer_ms(const int v);
int get_reorder_buffer_ms() const;
void set_demux_fifo_bytes(const int v);
int get_demux_fifo_bytes() const;
void set_socket_buffer_bytes(const int v);
int get_socket_buffer_bytes() const;
void set_reorder_queue_size(const int v);
int get_reorder_queue_size() const;
void set_io_timeout_ms(const int v);
int get_io_timeout_ms() const;
void set_queue_depth(const int v);
int get_queue_depth() const;
void set_frame_latency_ms(const int v);
int get_frame_latency_ms() const;
void set_vpp_half_res(const bool v);
bool get_vpp_half_res() const;
int get_frame_count() const { return present_count; }
bool has_frame() const { return have_presentable; }
int get_stream_width() const { return pending_w; }
int get_stream_height() const { return pending_h; }
void _process(double delta) override; void _process(double delta) override;
void _notification(int p_what); void _notification(int p_what);
void _draw() override;
void _ready() override; void _ready() override;
void set_desired_width(const int v); void set_desired_width(const int v);
int get_desired_width() const; int get_desired_width() const;
void set_desired_height(const int v); void set_desired_height(const int v);
int get_desired_height() const; int get_desired_height() const;
}; };
} } //namespace godot

View File

@@ -1,4 +1,7 @@
#include "register_types.h" #include "register_types.h"
#include "vulkan_detour.h"
#include <cstdio>
#include <gdextension_interface.h> #include <gdextension_interface.h>
#include <godot_cpp/core/class_db.hpp> #include <godot_cpp/core/class_db.hpp>
@@ -10,18 +13,18 @@
using namespace godot; using namespace godot;
void initialize_gdextension_types(ModuleInitializationLevel p_level) { void initialize_gdextension_types(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { fprintf(stderr, "[gdvapi] initialize level=%d\n", (int)p_level);
if (p_level == MODULE_INITIALIZATION_LEVEL_CORE)
gdvapi::install_vk_create_device_hook();
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE)
return; return;
}
GDREGISTER_RUNTIME_CLASS(GDVAPIVideoStream); GDREGISTER_RUNTIME_CLASS(GDVAPIVideoStream);
} }
void uninitialize_gdextension_types(ModuleInitializationLevel p_level) { void uninitialize_gdextension_types(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE)
return; return;
}
} }
extern "C" { extern "C" {
@@ -29,7 +32,7 @@ GDExtensionBool GDE_EXPORT gdvapi_library_init(GDExtensionInterfaceGetProcAddres
GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);
init_obj.register_initializer(initialize_gdextension_types); init_obj.register_initializer(initialize_gdextension_types);
init_obj.register_terminator(uninitialize_gdextension_types); init_obj.register_terminator(uninitialize_gdextension_types);
init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_CORE);
return init_obj.init(); return init_obj.init();
} }

449
src/vaapi_decoder.cpp Normal file
View File

@@ -0,0 +1,449 @@
#include "vaapi_decoder.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern "C" {
#include <libavutil/error.h>
#include <libavutil/opt.h>
#include <va/va_vpp.h>
}
namespace gdvapi {
static enum AVPixelFormat get_hw_format(AVCodecContext *ctx,
const enum AVPixelFormat *pix_fmts) {
(void)ctx;
for (const enum AVPixelFormat *p = pix_fmts; *p != -1; p++)
if (*p == AV_PIX_FMT_VAAPI)
return *p;
return AV_PIX_FMT_NONE;
}
VAAPIDecoder::~VAAPIDecoder() {
close();
}
bool VAAPIDecoder::open(const char *path, const DecoderOptions &o) {
close();
memset(&pkt, 0, sizeof(pkt));
AVDictionary *opts = nullptr;
av_dict_set(&opts, "protocol_whitelist", "file,udp,rtp,unix", 0);
char buf[32];
snprintf(buf, sizeof(buf), "%d", o.io_timeout_us);
av_dict_set(&opts, "timeout", buf, 0);
snprintf(buf, sizeof(buf), "%d", o.analyze_duration_us);
av_dict_set(&opts, "analyzeduration", buf, 0);
snprintf(buf, sizeof(buf), "%d", o.probe_bytes);
av_dict_set(&opts, "probesize", buf, 0);
snprintf(buf, sizeof(buf), "%d", o.demux_fifo_bytes);
av_dict_set(&opts, "rtbufsize", buf, 0);
snprintf(buf, sizeof(buf), "%d", o.max_delay_us);
av_dict_set(&opts, "max_delay", buf, 0);
snprintf(buf, sizeof(buf), "%d", o.socket_buffer_bytes);
av_dict_set(&opts, "buffer_size", buf, 0);
snprintf(buf, sizeof(buf), "%d", o.reorder_queue_size);
av_dict_set(&opts, "reorder_queue_size", buf, 0);
vpp_div_ = o.vpp_div > 0 ? o.vpp_div : 1;
if (avformat_open_input(&fmt_ctx, path, nullptr, &opts) < 0) {
av_dict_free(&opts);
fprintf(stderr, "[gdvapi] vaapi open input failed: %s\n", path);
return false;
}
av_dict_free(&opts);
if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
fprintf(stderr, "[gdvapi] find stream info failed\n");
close();
return false;
}
(void)0;
video_stream_idx = -1;
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_idx = i;
break;
}
}
if (video_stream_idx < 0) {
fprintf(stderr, "[gdvapi] no video stream\n");
close();
return false;
}
const AVCodec *dec = avcodec_find_decoder(
fmt_ctx->streams[video_stream_idx]->codecpar->codec_id);
if (!dec) {
fprintf(stderr, "[gdvapi] no decoder\n");
close();
return false;
}
codec_ctx = avcodec_alloc_context3(dec);
if (!codec_ctx) {
close();
return false;
}
if (avcodec_parameters_to_context(codec_ctx,
fmt_ctx->streams[video_stream_idx]->codecpar) < 0) {
fprintf(stderr, "[gdvapi] params->ctx failed\n");
close();
return false;
}
if (av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_VAAPI,
nullptr, nullptr, 0) < 0) {
fprintf(stderr, "[gdvapi] vaapi device create failed\n");
close();
return false;
}
codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
codec_ctx->get_format = get_hw_format;
if (avcodec_open2(codec_ctx, dec, nullptr) < 0) {
fprintf(stderr, "[gdvapi] codec open failed\n");
close();
return false;
}
if (codec_ctx->hw_frames_ctx) {
AVHWFramesContext *fc = (AVHWFramesContext *)codec_ctx->hw_frames_ctx->data;
fc->initial_pool_size = 16;
if (av_hwframe_ctx_init(codec_ctx->hw_frames_ctx) >= 0)
fprintf(stderr, "[gdvapi] hw frames pool set to 16\n");
}
frame = av_frame_alloc();
eof = false;
wait_key = true;
video_width = codec_ctx->width;
video_height = codec_ctx->height;
AVRational fr = fmt_ctx->streams[video_stream_idx]->avg_frame_rate;
if (fr.num > 0 && fr.den > 0)
video_fps = av_q2d(fr);
fprintf(stderr, "[gdvapi] vaapi decoder opened: %s %dx%d @%.1ffps\n",
avcodec_get_name(codec_ctx->codec_id), video_width, video_height,
video_fps);
return true;
}
bool VAAPIDecoder::next_frame(AVFrame *&out_frame) {
out_frame = nullptr;
if (!is_open())
return false;
while (true) {
int r = avcodec_receive_frame(codec_ctx, frame);
if (r == 0) {
if (frame->format == AV_PIX_FMT_VAAPI && frame->hw_frames_ctx) {
out_frame = av_frame_clone(frame);
av_frame_unref(frame);
return out_frame != nullptr;
}
av_frame_unref(frame);
continue;
}
if (r == AVERROR(EAGAIN))
break;
if (r == AVERROR_EOF) {
eof = true;
return false;
}
avcodec_flush_buffers(codec_ctx);
wait_key = true;
break;
}
while (!eof && av_read_frame(fmt_ctx, &pkt) >= 0) {
if (pkt.stream_index != video_stream_idx) {
av_packet_unref(&pkt);
continue;
}
if (wait_key && !(pkt.flags & AV_PKT_FLAG_KEY)) {
av_packet_unref(&pkt);
continue;
}
wait_key = false;
int sr = avcodec_send_packet(codec_ctx, &pkt);
av_packet_unref(&pkt);
if (sr < 0) {
if (getenv("GDVAPI_DEBUG_DECODE"))
fprintf(stderr, "[gdvapi] send_packet rc=%d\n", sr);
wait_key = true;
continue;
}
{
int it = 0;
while (true) {
it++;
int r = avcodec_receive_frame(codec_ctx, frame);
if (r == 0) {
if (frame->format == AV_PIX_FMT_VAAPI && frame->hw_frames_ctx) {
out_frame = av_frame_clone(frame);
av_frame_unref(frame);
return out_frame != nullptr;
}
av_frame_unref(frame);
continue;
}
if (r == AVERROR(EAGAIN)) {
if (getenv("GDVAPI_DEBUG_DECODE") && it == 1)
fprintf(stderr, "[gdvapi] recv EAGAIN (need more packets)\n");
break;
}
if (r == AVERROR_EOF) {
eof = true;
return false;
}
avcodec_flush_buffers(codec_ctx);
wait_key = true;
if (getenv("GDVAPI_DEBUG_DECODE"))
fprintf(stderr, "[gdvapi] recv rc=%d (flush)\n", r);
break;
}
}
continue;
while (true) {
int r = avcodec_receive_frame(codec_ctx, frame);
if (r == 0) {
if (frame->format == AV_PIX_FMT_VAAPI && frame->hw_frames_ctx) {
out_frame = av_frame_clone(frame);
av_frame_unref(frame);
return out_frame != nullptr;
}
av_frame_unref(frame);
continue;
}
if (r == AVERROR(EAGAIN))
break;
if (r == AVERROR_EOF) {
eof = true;
return false;
}
break;
}
}
eof = true;
return false;
}
static void fill_dma_out(const VADRMPRIMESurfaceDescriptor &desc, DecodedFrame &out) {
out.width = desc.width;
out.height = desc.height;
out.fourcc = desc.fourcc;
if (desc.num_layers > 0) {
out.drm_fourcc = desc.layers[0].drm_format;
out.pitch = desc.layers[0].pitch[0];
out.offset = desc.layers[0].offset[0];
}
out.drm_format_modifier = desc.objects[0].drm_format_modifier;
out.fd = desc.objects[0].fd;
}
double VAAPIDecoder::pts_sec_of(const AVFrame *f) const {
if (!f || f->pts == AV_NOPTS_VALUE || !fmt_ctx || video_stream_idx < 0)
return 0.0;
AVRational tb = fmt_ctx->streams[video_stream_idx]->time_base;
if (tb.num <= 0 || tb.den <= 0)
return 0.0;
return av_q2d(tb) * (double)f->pts;
}
bool VAAPIDecoder::vpp_init() {
if (vpp_ready)
return true;
AVHWDeviceContext *dctx = (AVHWDeviceContext *)hw_device_ctx->data;
AVVAAPIDeviceContext *va = (AVVAAPIDeviceContext *)dctx->hwctx;
VADisplay dpy = va->display;
VAConfigAttrib rt = { VAConfigAttribRTFormat, VA_RT_FORMAT_RGB32 };
VAStatus st = vaCreateConfig(dpy, VAProfileNone, VAEntrypointVideoProc,
&rt, 1, &vpp_config);
if (st != VA_STATUS_SUCCESS) {
fprintf(stderr, "[gdvapi] vpp vaCreateConfig failed: %d (%s)\n",
st, vaErrorStr(st));
return false;
}
int vpp_div = vpp_div_ > 0 ? vpp_div_ : 1;
vpp_out_w = video_width / vpp_div;
vpp_out_h = video_height / vpp_div;
if (vpp_out_w < 1)
vpp_out_w = 1;
if (vpp_out_h < 1)
vpp_out_h = 1;
st = vaCreateContext(dpy, vpp_config, vpp_out_w, vpp_out_h,
VA_PROGRESSIVE, 0, 0, &vpp_ctx);
if (st != VA_STATUS_SUCCESS) {
fprintf(stderr, "[gdvapi] vpp vaCreateContext failed: %d (%s)\n",
st, vaErrorStr(st));
return false;
}
st = vaCreateSurfaces(dpy, VA_RT_FORMAT_RGB32,
vpp_out_w, vpp_out_h, rgba_pool, kRgbaPool, nullptr, 0);
if (st != VA_STATUS_SUCCESS) {
fprintf(stderr, "[gdvapi] vpp vaCreateSurfaces failed: %d (%s)\n",
st, vaErrorStr(st));
return false;
}
vpp_ready = true;
fprintf(stderr, "[gdvapi] VPP init ok: %dx%d RGB32 target (out %dx%d)\n",
video_width, video_height, vpp_out_w, vpp_out_h);
return true;
}
bool VAAPIDecoder::convert_frame_to_rgba_slot(const AVFrame *nv12_frame,
int slot, DecodedFrame &out) {
if (!nv12_frame || nv12_frame->format != AV_PIX_FMT_VAAPI ||
slot < 0 || slot >= kRgbaPool)
return false;
if (video_width == 0 || video_height == 0) {
video_width = nv12_frame->width;
video_height = nv12_frame->height;
}
if (!vpp_init())
return false;
AVHWDeviceContext *dctx = (AVHWDeviceContext *)hw_device_ctx->data;
AVVAAPIDeviceContext *va = (AVVAAPIDeviceContext *)dctx->hwctx;
VADisplay dpy = va->display;
VASurfaceID src_surf = (VASurfaceID)(intptr_t)nv12_frame->data[0];
VAProcPipelineParameterBuffer param;
memset(&param, 0, sizeof(param));
param.surface = src_surf; // src (NV12)
param.surface_region = nullptr; // whole source
VARectangle out_reg = { 0, 0, (uint16_t)vpp_out_w, (uint16_t)vpp_out_h };
param.output_region = &out_reg; // whole target (RGB32)
VABufferID buf = VA_INVALID_ID;
VAStatus st = vaCreateBuffer(dpy, vpp_ctx, VAProcPipelineParameterBufferType,
sizeof(param), 1, &param, &buf);
if (st != VA_STATUS_SUCCESS) {
fprintf(stderr, "[gdvapi] vpp vaCreateBuffer failed: %d (%s)\n",
st, vaErrorStr(st));
return false;
}
st = vaBeginPicture(dpy, vpp_ctx, rgba_pool[slot]);
if (st == VA_STATUS_SUCCESS)
st = vaRenderPicture(dpy, vpp_ctx, &buf, 1);
if (st == VA_STATUS_SUCCESS)
st = vaEndPicture(dpy, vpp_ctx);
if (st == VA_STATUS_SUCCESS)
st = vaSyncSurface(dpy, rgba_pool[slot]);
vaDestroyBuffer(dpy, buf);
if (st != VA_STATUS_SUCCESS) {
fprintf(stderr, "[gdvapi] vpp render failed: %d (%s)\n", st, vaErrorStr(st));
return false;
}
VADRMPRIMESurfaceDescriptor desc;
st = vaExportSurfaceHandle(dpy, rgba_pool[slot],
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_COMPOSED_LAYERS,
&desc);
if (st != VA_STATUS_SUCCESS) {
fprintf(stderr, "[gdvapi] vpp export failed: %d (%s)\n", st, vaErrorStr(st));
return false;
}
fill_dma_out(desc, out);
out.pts = nv12_frame->pts;
if (fmt_ctx && video_stream_idx >= 0) {
AVRational tb = fmt_ctx->streams[video_stream_idx]->time_base;
if (tb.num > 0 && tb.den > 0 && out.pts != AV_NOPTS_VALUE)
out.pts_sec = av_q2d(tb) * (double)out.pts;
}
return true;
}
bool VAAPIDecoder::export_surface(const AVFrame *f, DecodedFrame &out) {
out = DecodedFrame();
if (!f || f->format != AV_PIX_FMT_VAAPI || !f->hw_frames_ctx)
return false;
VASurfaceID surf = (VASurfaceID)(intptr_t)f->data[0];
AVHWDeviceContext *dctx = (AVHWDeviceContext *)hw_device_ctx->data;
AVVAAPIDeviceContext *va = (AVVAAPIDeviceContext *)dctx->hwctx;
VADisplay dpy = va->display;
VADRMPRIMESurfaceDescriptor desc;
VAStatus st = vaExportSurfaceHandle(dpy, surf,
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_COMPOSED_LAYERS,
&desc);
if (st != VA_STATUS_SUCCESS) {
fprintf(stderr, "[gdvapi] vaExportSurfaceHandle(%u) failed: %d (%s)\n",
(unsigned)surf, st, vaErrorStr(st));
return false;
}
out.width = desc.width;
out.height = desc.height;
out.fourcc = desc.fourcc;
if (desc.num_layers > 0) {
out.drm_fourcc = desc.layers[0].drm_format;
out.pitch = desc.layers[0].pitch[0];
out.offset = desc.layers[0].offset[0];
if (desc.layers[0].num_planes > 1) {
out.uv_pitch = desc.layers[0].pitch[1];
out.uv_offset = desc.layers[0].offset[1];
}
}
out.drm_format_modifier = desc.objects[0].drm_format_modifier;
out.fd = desc.objects[0].fd;
out.pts = f->pts;
return true;
}
void VAAPIDecoder::close() {
if (vpp_ready) {
AVHWDeviceContext *dctx = (AVHWDeviceContext *)hw_device_ctx->data;
AVVAAPIDeviceContext *va = (AVVAAPIDeviceContext *)dctx->hwctx;
VADisplay dpy = va->display;
for (int i = 0; i < kRgbaPool; i++) {
if (rgba_pool[i] != VA_INVALID_ID) {
VASurfaceID t = rgba_pool[i];
vaDestroySurfaces(dpy, &t, 1);
rgba_pool[i] = VA_INVALID_ID;
}
}
if (vpp_ctx != VA_INVALID_ID) {
vaDestroyContext(dpy, vpp_ctx);
vpp_ctx = VA_INVALID_ID;
}
if (vpp_config != VA_INVALID_ID) {
vaDestroyConfig(dpy, vpp_config);
vpp_config = VA_INVALID_ID;
}
vpp_ready = false;
}
if (frame) {
av_frame_free(&frame);
frame = nullptr;
}
if (codec_ctx) {
avcodec_free_context(&codec_ctx);
codec_ctx = nullptr;
}
if (hw_device_ctx) {
av_buffer_unref(&hw_device_ctx);
hw_device_ctx = nullptr;
}
if (fmt_ctx) {
avformat_close_input(&fmt_ctx);
fmt_ctx = nullptr;
}
video_stream_idx = -1;
eof = false;
}
} //namespace gdvapi

90
src/vaapi_decoder.h Normal file
View File

@@ -0,0 +1,90 @@
#pragma once
#include <cstdint>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/hwcontext.h>
#include <libavutil/hwcontext_vaapi.h>
#include <libavutil/pixfmt.h>
#include <va/va.h>
#include <va/va_drmcommon.h>
}
namespace gdvapi {
struct DecoderOptions {
int io_timeout_us = 1000000; // avio block timeout (dead-stream detection)
int analyze_duration_us = 500000; // find_stream_info analysis cap
int probe_bytes = 1000000; // probe-size cap
int demux_fifo_bytes = 33554432; // rtbufsize: 32 MB demux FIFO (keyframe bursts)
int socket_buffer_bytes = 8388608; // kernel SO_RCVBUF: 8 MB (RTP burst headroom)
int max_delay_us = 200000; // RTP reorder/jitter window (optimum on rig)
int reorder_queue_size = 2048; // RTP demuxer reorder queue (packets)
int vpp_div = 1; // VPP output downscale (1 = full res, 2 = bench)
};
struct DecodedFrame {
int width = 0;
int height = 0;
int fd = -1; // DRM dma-buf fd
uint32_t fourcc = 0;
uint32_t drm_fourcc = 0; // layers[0].drm_format from the export
double pts_sec = 0.0; // pts in seconds (stream time_base)
uint32_t pitch = 0; // layer[0].pitch[0] (Y plane)
uint32_t offset = 0; // layer[0].offset[0] (Y plane)
uint32_t uv_pitch = 0; // layer[0].pitch[1] (UV plane, real tiled)
uint32_t uv_offset = 0; // layer[0].offset[1] (UV plane, real tiled)
uint64_t drm_format_modifier = 0;
int64_t pts = AV_NOPTS_VALUE;
bool valid() const { return fd >= 0; }
};
class VAAPIDecoder {
public:
VAAPIDecoder() = default;
~VAAPIDecoder();
bool open(const char *path, const DecoderOptions &o = DecoderOptions());
bool next_frame(AVFrame *&out_frame);
bool export_surface(const AVFrame *frame, DecodedFrame &out);
void close();
bool is_open() const { return fmt_ctx && codec_ctx; }
int width() const { return video_width; }
int height() const { return video_height; }
double fps() const { return video_fps; }
bool vpp_ready = false;
VAConfigID vpp_config = VA_INVALID_ID;
VAContextID vpp_ctx = VA_INVALID_ID;
VASurfaceID rgba_pool[4] = { VA_INVALID_ID, VA_INVALID_ID, VA_INVALID_ID, VA_INVALID_ID };
int vpp_out_w = 0, vpp_out_h = 0;
static constexpr int kRgbaPool = 4;
bool vpp_init();
public:
double pts_sec_of(const AVFrame *f) const;
bool convert_frame_to_rgba_slot(const AVFrame *nv12_frame, int slot,
DecodedFrame &out);
private:
AVFormatContext *fmt_ctx = nullptr;
AVCodecContext *codec_ctx = nullptr;
AVBufferRef *hw_device_ctx = nullptr;
AVFrame *frame = nullptr;
AVPacket pkt = {};
int video_stream_idx = -1;
int video_width = 0;
int video_height = 0;
double video_fps = 30.0;
bool eof = false;
bool wait_key = true; // skip non-key packets until first IDR
int vpp_div_ = 1; // VPP output downscale from DecoderOptions
};
} //namespace gdvapi

226
src/vk_image_import.cpp Normal file
View File

@@ -0,0 +1,226 @@
#include "vk_image_import.h"
#include <dlfcn.h>
#include <stdio.h>
namespace gdvapi {
namespace {
using PFN_vkGetDeviceProcAddr_t = PFN_vkVoidFunction(VKAPI_PTR *)(VkDevice, const char *);
PFN_vkGetDeviceProcAddr_t resolve_get_device_proc_addr() {
static PFN_vkGetDeviceProcAddr_t fn = [] {
void *lib = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_GLOBAL);
if (!lib)
return (PFN_vkGetDeviceProcAddr_t) nullptr;
return reinterpret_cast<PFN_vkGetDeviceProcAddr_t>(dlsym(lib, "vkGetDeviceProcAddr"));
}();
return fn;
}
template <typename T>
T load_dev_fn(VkDevice device, const char *name) {
auto gpa = resolve_get_device_proc_addr();
if (!gpa) {
fprintf(stderr, "[gdvapi-import] vkGetDeviceProcAddr unavailable\n");
return nullptr;
}
PFN_vkVoidFunction p = gpa(device, name);
if (!p) {
fprintf(stderr, "[gdvapi-import] missing device fn %s\n", name);
return nullptr;
}
return reinterpret_cast<T>(p);
}
VkFormat fourcc_to_format(uint32_t fourcc, uint32_t pitch) {
(void)pitch;
switch (fourcc) {
case 0x3231564e: // 'NV12'
return VK_FORMAT_G8_B8R8_2PLANE_420_UNORM;
case 0x34325241: // DRM_FORMAT_ARGB8888 'AR24' -> bytes B,G,R,A
case 0x34325258: // DRM_FORMAT_XRGB8888 'XR24' -> bytes B,G,R,A
return VK_FORMAT_B8G8R8A8_UNORM;
case 0x34324241: // DRM_FORMAT_ABGR8888 'AB24' -> bytes R,G,B,A
return VK_FORMAT_R8G8B8A8_UNORM;
default:
fprintf(stderr, "[gdvapi-import] unknown drm fourcc 0x%x\n", fourcc);
return VK_FORMAT_UNDEFINED;
}
}
} //namespace
VkImage import_dma_buf_image(VkDevice device, const DmaBufSurface &surf,
VkDeviceMemory *out_memory) {
if (!device || surf.fd < 0)
return VK_NULL_HANDLE;
if (out_memory)
*out_memory = VK_NULL_HANDLE;
VkFormat format = fourcc_to_format(
surf.drm_fourcc ? surf.drm_fourcc : surf.fourcc, surf.pitch);
if (format == VK_FORMAT_UNDEFINED) {
fprintf(stderr, "[gdvapi-import] unsupported fourcc 0x%x\n", surf.fourcc);
return VK_NULL_HANDLE;
}
auto dev_alloc = load_dev_fn<PFN_vkAllocateMemory>(device, "vkAllocateMemory");
auto dev_free = load_dev_fn<PFN_vkFreeMemory>(device, "vkFreeMemory");
auto dev_create_image = load_dev_fn<PFN_vkCreateImage>(device, "vkCreateImage");
auto dev_destroy_image = load_dev_fn<PFN_vkDestroyImage>(device, "vkDestroyImage");
auto dev_bind_mem = load_dev_fn<PFN_vkBindImageMemory>(device, "vkBindImageMemory");
auto dev_mem_req = load_dev_fn<PFN_vkGetImageMemoryRequirements>(device, "vkGetImageMemoryRequirements");
auto dev_fd_props = load_dev_fn<PFN_vkGetMemoryFdPropertiesKHR>(device, "vkGetMemoryFdPropertiesKHR");
if (!dev_alloc || !dev_free || !dev_create_image || !dev_destroy_image ||
!dev_bind_mem || !dev_mem_req || !dev_fd_props) {
fprintf(stderr, "[gdvapi-import] required device functions unavailable\n");
return VK_NULL_HANDLE;
}
VkSubresourceLayout plane_layouts[2] = {};
plane_layouts[0].offset = surf.offset;
plane_layouts[0].rowPitch = surf.pitch;
plane_layouts[0].size = 0;
plane_layouts[1].offset = surf.offset + (VkDeviceSize)surf.pitch * surf.height;
plane_layouts[1].rowPitch = surf.pitch;
plane_layouts[1].size = 0;
const bool linear = (surf.drm_format_modifier == 0);
const bool multi_plane = (format == VK_FORMAT_G8_B8R8_2PLANE_420_UNORM);
VkImageDrmFormatModifierExplicitCreateInfoEXT drm_info{};
drm_info.sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT;
drm_info.drmFormatModifier = surf.drm_format_modifier;
drm_info.pPlaneLayouts = plane_layouts;
drm_info.drmFormatModifierPlaneCount = linear ? 0 : (multi_plane ? 2 : 1);
VkExternalMemoryImageCreateInfo ext_info{};
ext_info.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO;
ext_info.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;
if (!linear)
ext_info.pNext = &drm_info;
VkImageFormatListCreateInfo fmt_list{};
fmt_list.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
if (multi_plane) {
VkFormat view_formats[3] = {
format,
VK_FORMAT_R8_UNORM,
VK_FORMAT_R8G8_UNORM,
};
fmt_list.viewFormatCount = 3;
fmt_list.pViewFormats = view_formats;
fmt_list.pNext = &ext_info;
}
VkImageCreateInfo image_info{};
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
const void *next = &ext_info;
if (multi_plane)
image_info.pNext = &fmt_list;
else
image_info.pNext = &ext_info;
(void)next;
image_info.flags = multi_plane ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0;
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.format = format;
image_info.extent = { surf.width, surf.height, 1 };
image_info.mipLevels = 1;
image_info.arrayLayers = 1;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.tiling = linear ? VK_IMAGE_TILING_LINEAR
: VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT;
image_info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VkImage image = VK_NULL_HANDLE;
VkResult r = dev_create_image(device, &image_info, nullptr, &image);
if (r != VK_SUCCESS) {
fprintf(stderr, "[gdvapi-import] vkCreateImage failed: %d\n", (int)r);
return VK_NULL_HANDLE;
}
VkMemoryFdPropertiesKHR fd_props{};
fd_props.sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR;
r = dev_fd_props(device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, surf.fd, &fd_props);
if (r != VK_SUCCESS) {
fprintf(stderr, "[gdvapi-import] vkGetMemoryFdPropertiesKHR failed: %d\n", (int)r);
dev_destroy_image(device, image, nullptr);
return VK_NULL_HANDLE;
}
VkMemoryRequirements mem_req{};
dev_mem_req(device, image, &mem_req);
uint32_t type_idx = 0xFFFFFFFF;
for (uint32_t i = 0; i < 32; i++) {
if ((fd_props.memoryTypeBits & mem_req.memoryTypeBits) & (1u << i)) {
type_idx = i;
break;
}
}
if (type_idx == 0xFFFFFFFF) {
fprintf(stderr, "[gdvapi-import] no compatible memory type\n");
dev_destroy_image(device, image, nullptr);
return VK_NULL_HANDLE;
}
VkImportMemoryFdInfoKHR import_info{};
import_info.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR;
import_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;
import_info.fd = surf.fd;
VkMemoryDedicatedAllocateInfo dedicated_info{};
dedicated_info.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO;
dedicated_info.image = image;
import_info.pNext = &dedicated_info;
VkMemoryAllocateInfo alloc_info{};
alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
alloc_info.pNext = &import_info;
alloc_info.allocationSize = mem_req.size;
alloc_info.memoryTypeIndex = type_idx;
VkDeviceMemory memory = VK_NULL_HANDLE;
r = dev_alloc(device, &alloc_info, nullptr, &memory);
if (r != VK_SUCCESS) {
fprintf(stderr, "[gdvapi-import] vkAllocateMemory(import) failed: %d\n", (int)r);
dev_destroy_image(device, image, nullptr);
return VK_NULL_HANDLE;
}
r = dev_bind_mem(device, image, memory, 0);
if (r != VK_SUCCESS) {
fprintf(stderr, "[gdvapi-import] vkBindImageMemory failed: %d\n", (int)r);
dev_free(device, memory, nullptr);
dev_destroy_image(device, image, nullptr);
return VK_NULL_HANDLE;
}
if (out_memory)
*out_memory = memory;
fprintf(stderr, "[gdvapi-import] imported dma-buf fd=%d -> VkImage %p size=%ux%u mod=0x%llx\n",
surf.fd, (void *)image, surf.width, surf.height,
(unsigned long long)surf.drm_format_modifier);
return image;
}
void destroy_imported_image(VkDevice device, VkImage image, VkDeviceMemory mem) {
if (!device)
return;
auto destroy_image = load_dev_fn<PFN_vkDestroyImage>(device, "vkDestroyImage");
auto free_memory = load_dev_fn<PFN_vkFreeMemory>(device, "vkFreeMemory");
if (image && destroy_image)
destroy_image(device, image, nullptr);
if (mem && free_memory)
free_memory(device, mem, nullptr);
}
} //namespace gdvapi

26
src/vk_image_import.h Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <vulkan/vulkan_core.h>
namespace gdvapi {
struct DmaBufSurface {
int fd = -1;
uint32_t width = 0;
uint32_t height = 0;
uint64_t drm_format_modifier = 0;
uint32_t pitch = 0;
uint32_t offset = 0;
uint32_t uv_pitch = 0; // plane 1 (tiled chroma) pitch from descriptor
uint32_t uv_offset = 0; // plane 1 (tiled chroma) offset from descriptor
uint32_t fourcc = 0; // e.g. 0x3231564e "NV12"
uint32_t drm_fourcc = 0; // layers[0].drm_format from the descriptor
};
VkImage import_dma_buf_image(VkDevice device, const DmaBufSurface &surf,
VkDeviceMemory *out_memory);
void destroy_imported_image(VkDevice device, VkImage image, VkDeviceMemory mem);
} //namespace gdvapi

192
src/vulkan_detour.cpp Normal file
View File

@@ -0,0 +1,192 @@
#include "vulkan_detour.h"
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <atomic>
#include <vector>
#if !defined(__x86_64__)
#error "vulkan_detour: x86_64 only"
#endif
namespace {
const char *const kExtNames[] = {
"VK_KHR_external_memory",
"VK_KHR_external_memory_fd",
"VK_EXT_external_memory_dma_buf",
"VK_EXT_image_drm_format_modifier",
"VK_EXT_queue_family_foreign",
};
constexpr int kExtCount = (int)(sizeof(kExtNames) / sizeof(kExtNames[0]));
using VkCreateDeviceFn = VkResult(VKAPI_PTR *)(VkPhysicalDevice, const VkDeviceCreateInfo *, const VkAllocationCallbacks *, VkDevice *);
std::atomic<bool> s_hook_installed{ false };
uintptr_t s_target = 0; // address of vkCreateDevice in libvulkan
uintptr_t s_trampoline = 0; // executable copy of original prologue
constexpr int kPatchSize = 5; // E9 rel32
using PFN_vkEnumerateDeviceExtensionProperties =
VkResult(VKAPI_PTR *)(VkPhysicalDevice, const char *, uint32_t *, VkExtensionProperties *);
static PFN_vkEnumerateDeviceExtensionProperties s_enumerate_ext = nullptr;
static bool make_rwx(uintptr_t addr, size_t len) {
long page = (long)addr & ~(long)(sysconf(_SC_PAGESIZE) - 1);
size_t total = len + (size_t)(addr - (uintptr_t)page);
return mprotect((void *)page, total, PROT_READ | PROT_WRITE | PROT_EXEC) == 0;
}
static bool make_rx(uintptr_t addr, size_t len) {
long page = (long)addr & ~(long)(sysconf(_SC_PAGESIZE) - 1);
size_t total = len + (size_t)(addr - (uintptr_t)page);
return mprotect((void *)page, total, PROT_READ | PROT_EXEC) == 0;
}
static void emit_rel32_jmp(unsigned char *dst, uintptr_t from, uintptr_t to) {
dst[0] = 0xE9;
uint32_t rel = (uint32_t)(to - (from + 5));
memcpy(dst + 1, &rel, 4);
}
static VkResult call_original(VkPhysicalDevice pd, const VkDeviceCreateInfo *ci,
const VkAllocationCallbacks *alloc, VkDevice *dev) {
typedef VkResult(VKAPI_PTR * OrigFn)(VkPhysicalDevice, const VkDeviceCreateInfo *,
const VkAllocationCallbacks *, VkDevice *);
OrigFn fn = (OrigFn)s_trampoline;
return fn(pd, ci, alloc, dev);
}
static bool device_has_extension(VkPhysicalDevice dev, const char *name) {
if (!s_enumerate_ext)
return false;
uint32_t count = 0;
s_enumerate_ext(dev, nullptr, &count, nullptr);
if (count == 0)
return false;
std::vector<VkExtensionProperties> props(count);
s_enumerate_ext(dev, nullptr, &count, props.data());
for (auto &p : props)
if (strcmp(p.extensionName, name) == 0)
return true;
return false;
}
} //namespace
namespace gdvapi {
bool device_supports_external_extensions(VkPhysicalDevice physical_device) {
for (const char *name : kExtNames)
if (!device_has_extension(physical_device, name))
return false;
return true;
}
VKAPI_ATTR VkResult VKAPI_CALL hooked_vk_create_device(
VkPhysicalDevice physical_device,
const VkDeviceCreateInfo *p_create_info,
const VkAllocationCallbacks *p_allocator,
VkDevice *p_device) {
if (!s_trampoline || !p_create_info)
return VK_ERROR_INITIALIZATION_FAILED;
std::vector<const char *> missing;
uint32_t existing_count = p_create_info->enabledExtensionCount
? p_create_info->enabledExtensionCount
: 0;
const char *const *existing = p_create_info->ppEnabledExtensionNames;
for (const char *name : kExtNames) {
bool present = false;
for (uint32_t i = 0; i < existing_count; i++) {
if (existing && existing[i] && strcmp(existing[i], name) == 0) {
present = true;
break;
}
}
if (!present)
missing.push_back(name);
}
if (missing.empty()) {
fprintf(stderr, "[gdvapi-hook] all external extensions already enabled\n");
return call_original(physical_device, p_create_info, p_allocator, p_device);
}
VkDeviceCreateInfo modified = *p_create_info;
static std::vector<const char *> all;
all.clear();
if (existing_count && existing)
all.insert(all.end(), existing, existing + existing_count);
all.reserve(existing_count + kExtCount);
for (const char *name : missing)
all.push_back(name);
modified.enabledExtensionCount = (uint32_t)all.size();
modified.ppEnabledExtensionNames = all.data();
fprintf(stderr, "[gdvapi-hook] injecting %zu external-memory extension(s)\n", missing.size());
for (const char *name : missing)
fprintf(stderr, "[gdvapi-hook] + %s\n", name);
VkResult r = call_original(physical_device, &modified, p_allocator, p_device);
fprintf(stderr, "[gdvapi-hook] vkCreateDevice -> %d\n", (int)r);
return r;
}
bool install_vk_create_device_hook() {
if (s_hook_installed.exchange(true))
return true;
void *lib = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_GLOBAL);
if (!lib) {
fprintf(stderr, "[gdvapi-hook] dlopen libvulkan.so.1: %s\n", dlerror());
s_hook_installed = false;
return false;
}
VkCreateDeviceFn orig_fn = (VkCreateDeviceFn)dlsym(lib, "vkCreateDevice");
if (!orig_fn) {
fprintf(stderr, "[gdvapi-hook] dlsym vkCreateDevice: %s\n", dlerror());
s_hook_installed = false;
return false;
}
s_target = (uintptr_t)orig_fn;
s_enumerate_ext = (PFN_vkEnumerateDeviceExtensionProperties)dlsym(lib, "vkEnumerateDeviceExtensionProperties");
if (!s_enumerate_ext)
fprintf(stderr, "[gdvapi-hook] dlsym vkEnumerateDeviceExtensionProperties: %s\n", dlerror());
void *tramp = mmap(nullptr, sysconf(_SC_PAGESIZE), PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (tramp == MAP_FAILED) {
fprintf(stderr, "[gdvapi-hook] mmap trampoline failed\n");
s_hook_installed = false;
return false;
}
uintptr_t t = (uintptr_t)tramp;
memcpy((void *)t, (void *)s_target, kPatchSize);
// jump from t+5 back to target+5
emit_rel32_jmp((unsigned char *)(t + kPatchSize), t + kPatchSize, s_target + kPatchSize);
s_trampoline = t;
if (!make_rwx(s_target, kPatchSize)) {
fprintf(stderr, "[gdvapi-hook] mprotect target failed\n");
s_hook_installed = false;
munmap(tramp, sysconf(_SC_PAGESIZE));
return false;
}
emit_rel32_jmp((unsigned char *)s_target, s_target, (uintptr_t)hooked_vk_create_device);
make_rx(s_target, kPatchSize);
fprintf(stderr, "[gdvapi-hook] vkCreateDevice hook installed (trampoline @%p)\n", (void *)s_trampoline);
return true;
}
} //namespace gdvapi

10
src/vulkan_detour.h Normal file
View File

@@ -0,0 +1,10 @@
#pragma once
#include <vulkan/vulkan_core.h>
namespace gdvapi {
bool install_vk_create_device_hook();
bool device_supports_external_extensions(VkPhysicalDevice physical_device);
} //namespace gdvapi

36
stream-gst.py Normal file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gst', '1.0')
from gi.repository import Gst, GLib
from datetime import datetime
Gst.init(None)
device = sys.argv[1] if len(sys.argv) > 1 else "/dev/video0"
port = sys.argv[2] if len(sys.argv) > 2 else "5000"
pipeline = Gst.parse_launch(f"""
v4l2src device={device} io-mode=2 !
videoconvert !
textoverlay name=overlay halignment=left valignment=top font-desc="Monospace 36" !
x265enc tune=zerolatency speed-preset=ultrafast option-string="bframes=0:keyint=30:scenecut=0" !
h265parse config-interval=1 !
rtph265pay config-interval=1 mtu=1400 !
udpsink host=127.0.0.1 port={port} sync=false async=false
""")
overlay = pipeline.get_by_name("overlay")
def update_text():
overlay.set_property("text", datetime.now().strftime("%H:%M:%S.%f")[:-3])
return True
GLib.timeout_add(5, update_text)
pipeline.set_state(Gst.State.PLAYING)
loop = GLib.MainLoop()
try:
loop.run()
except KeyboardInterrupt:
pipeline.set_state(Gst.State.NULL)