Compare commits

..

6 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
Rhys Tumelty
97ff779911 tried (and failed) to get a white rectangle placeholder drawing, also streaming btw 2026-08-24 16:18:39 +01:00
Rhys Tumelty
dacbb2e043 file decoding works 2026-08-23 21:50:09 +01:00
Rhys Tumelty
27693a8fbf made it acutally work in godot, and made it draw a texture 2026-08-23 20:59:48 +01:00
Rhys Tumelty
427c64ef5d whoops 2026-08-23 18:18:37 +01:00
32 changed files with 1905 additions and 365 deletions

View File

@@ -1,44 +0,0 @@
name: Bug report
description: Report a bug with the godot-cpp template
body:
- type: markdown
attributes:
value: |
- Write a descriptive issue title above.
- Search [open](https://github.com/godotengine/godot-cpp-template/issues) and [closed](https://github.com/godotengine/godot-cpp-template/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported.
- type: input
attributes:
label: Godot version
description: >
Specify the Git commit hash of your Godot build.
placeholder: v4.0.stable.official [92bee43ad]
validations:
required: true
- type: input
attributes:
label: godot-cpp version
description: >
Specify the Git commit hash of the godot-cpp submodule in your project. You can run `git status` inside the folder to check it.
placeholder: v4.0.stable.official [9d1c396c5]
validations:
required: true
- type: input
attributes:
label: System information
description: |
Specify the OS version.
placeholder: Windows 10
validations:
required: true
- type: textarea
attributes:
label: Issue description
description: |
Describe your issue briefly. What doesn't work, and how do you expect it to work instead?
You can include images or videos with drag and drop, and format code blocks or logs with <code>```</code> tags.
validations:
required: true

View File

@@ -1,14 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Godot proposals
url: https://github.com/godotengine/godot-proposals
about: Please submit feature proposals on the Godot proposals repository, not here.
- name: Godot documentation repository
url: https://github.com/godotengine/godot-docs
about: Please report issues with documentation on the Godot documentation repository, not here.
- name: Godot community channels
url: https://godotengine.org/community
about: Please ask for technical support on one of the other community channels, not here.

View File

@@ -1,88 +0,0 @@
# For syntax, see https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
# This workflow is automatically triggered for every push and pull request, to verify that everything still builds.
name: Continuous Integration
on:
workflow_call:
push:
pull_request:
merge_group:
jobs:
build:
strategy:
fail-fast: false
matrix:
# Test a few selected combinations of parameters ("test case matrix").
# While it would be nice to test all needed combinations, in practice this would a waste of CI usage time.
# By building just with a few combinations, we only need a few runners,
# while still achieving adequate coverage.
# To make a full build with all combinations, use the make_build.yml workflow instead.
include:
- target: { platform: linux, arch: x86_64, os: ubuntu-22.04 }
target-type: template_debug
float-precision: double
- target: { platform: windows, arch: x86_64, os: windows-latest }
target-type: template_release
float-precision: single
- target: { platform: macos, arch: universal, os: macos-latest }
target-type: template_debug
float-precision: single
- target: { platform: android, arch: arm64, os: ubuntu-22.04 }
target-type: template_debug
float-precision: single
- target: { platform: web, arch: wasm32, os: ubuntu-22.04 }
target-type: template_release
float-precision: double
runs-on: ${{ matrix.target.os }}
steps:
# Clone this repository
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
# Lint
#- name: Setup clang-format
# shell: bash
# run: |
# python -m pip install clang-format
#- name: Run clang-format
# shell: bash
# run: |
# clang-format src/** --dry-run --Werror
# Add linux x86_32 toolchain
- name: Install multilib support
if: ${{ matrix.target.platform == 'linux' && matrix.target.arch == 'x86_32' }}
run: |
sudo apt-get update && sudo apt-get install -y gcc-multilib g++-multilib
# Setup dependencies
- name: Setup godot-cpp
uses: ./godot-cpp/.github/actions/setup-godot-cpp
with:
platform: ${{ matrix.target.platform }}
em-version: 3.1.62
# Build GDExtension (with caches)
- name: Restore .scons_cache
uses: ./godot-cpp/.github/actions/godot-cache-restore
with:
scons-cache: ${{ github.workspace }}/.scons-cache/
cache-name: ${{ matrix.target.platform }}_${{ matrix.target.arch }}_${{ matrix.float-precision }}_${{ matrix.target-type }}
- name: Build GDExtension Debug Build
shell: sh
env:
SCONS_CACHE: ${{ github.workspace }}/.scons-cache/
run: |
scons target=${{ matrix.target-type }} platform=${{ matrix.target.platform }} arch=${{ matrix.target.arch }} precision=${{ matrix.float-precision }}
- name: Save .scons_cache
uses: ./godot-cpp/.github/actions/godot-cache-save
with:
scons-cache: ${{ github.workspace }}/.scons-cache/
cache-name: ${{ matrix.target.platform }}_${{ matrix.target.arch }}_${{ matrix.float-precision }}_${{ matrix.target-type }}

View File

@@ -1,104 +0,0 @@
# For syntax, see https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
# This workflow is triggered manually on the "Actions" tab on GitHub, and can be used to create releases.
name: Make a GDExtension build for all supported platforms
on:
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
# A build is made for every possible combination of parameters
# You can add or remove entries from the arrays of each parameter to customize which builds you want to run.
# See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow
target:
[
{ platform: linux, arch: x86_64, os: ubuntu-22.04 },
{ platform: linux, arch: x86_32, os: ubuntu-22.04 },
{ platform: linux, arch: arm64, os: ubuntu-22.04-arm },
{ platform: linux, arch: arm32, os: ubuntu-22.04-arm },
{ platform: windows, arch: x86_64, os: windows-latest },
{ platform: windows, arch: x86_32, os: windows-latest },
{ platform: windows, arch: arm64, os: windows-latest },
{ platform: macos, arch: universal, os: macos-latest },
{ platform: android, arch: x86_64, os: ubuntu-22.04 },
{ platform: android, arch: x86_32, os: ubuntu-22.04 },
{ platform: android, arch: arm64, os: ubuntu-22.04 },
{ platform: android, arch: arm32, os: ubuntu-22.04 },
{ platform: ios, arch: arm64, os: macos-latest },
{ platform: web, arch: wasm32, os: ubuntu-22.04 },
]
target-type: [template_debug, template_release]
float-precision: [single]
runs-on: ${{ matrix.target.os }}
steps:
# Clone this repository
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
# Add linux x86_32 toolchain
- name: Install multilib support
if: ${{ matrix.target.platform == 'linux' && matrix.target.arch == 'x86_32' }}
run: |
sudo apt-get update && sudo apt-get install -y gcc-multilib g++-multilib
# Setup dependencies
- name: Setup godot-cpp
uses: ./godot-cpp/.github/actions/setup-godot-cpp
with:
platform: ${{ matrix.target.platform }}
em-version: 3.1.62
# Build GDExtension (with caches)
- name: Restore .scons_cache
uses: ./godot-cpp/.github/actions/godot-cache-restore
with:
scons-cache: ${{ github.workspace }}/.scons-cache/
cache-name: ${{ matrix.target.platform }}_${{ matrix.target.arch }}_${{ matrix.float-precision }}_${{ matrix.target-type }}
- name: Build GDExtension Debug Build
shell: sh
env:
SCONS_CACHE: ${{ github.workspace }}/.scons-cache/
run: |
scons target=${{ matrix.target-type }} platform=${{ matrix.target.platform }} arch=${{ matrix.target.arch }} precision=${{ matrix.float-precision }}
- name: Save .scons_cache
uses: ./godot-cpp/.github/actions/godot-cache-save
with:
scons-cache: ${{ github.workspace }}/.scons-cache/
cache-name: ${{ matrix.target.platform }}_${{ matrix.target.arch }}_${{ matrix.float-precision }}_${{ matrix.target-type }}
# Clean up compilation files
- name: Windows - Delete compilation files
if: ${{ matrix.target.platform == 'windows' }}
shell: pwsh
run: |
Remove-Item bin/* -Include *.exp,*.lib,*.pdb -Force
# Upload the build
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: godot-cpp-template-${{ matrix.target.platform }}-${{ matrix.target.arch }}-${{ matrix.float-precision }}-${{ matrix.target-type }}
path: |
${{ github.workspace }}/bin/**
# Merges all the build artifacts together into a single godot-cpp-template artifact.
# If you comment out this step, all the builds will be uploaded individually.
merge:
runs-on: ubuntu-22.04
needs: build
steps:
- name: Merge Artifacts
uses: actions/upload-artifact/merge@v4
with:
name: godot-cpp-template
pattern: godot-cpp-template-*
delete-merged: true

7
.gitignore vendored
View File

@@ -53,3 +53,10 @@ CMakeUserPresets.json
# stupid godot developers using SCons # stupid godot developers using SCons
build/ build/
godot/
build-editor/
# Some testing files
project/*.mp4
project/*.mov

View File

@@ -49,14 +49,37 @@ project(gdvapi
LANGUAGES CXX LANGUAGES CXX
) )
find_package(Vulkan REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat)
pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec)
pkg_check_modules(SWSCALE REQUIRED IMPORTED_TARGET libswscale)
pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil)
add_library(${LIBNAME} SHARED) add_library(${LIBNAME} SHARED)
target_link_libraries(${LIBNAME} PRIVATE
Vulkan::Headers
PkgConfig::AVFORMAT
PkgConfig::AVCODEC
PkgConfig::SWSCALE
PkgConfig::AVUTIL
)
target_sources(${LIBNAME} target_sources(${LIBNAME}
PRIVATE PRIVATE
src/register_types.cpp src/register_types.cpp
src/register_types.h src/register_types.h
src/example_class.cpp src/gdvapivideostream_class.cpp
src/example_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

View File

@@ -13,6 +13,18 @@
clang clang
python3 python3
godot godot
pkg-config
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-bad
];
buildInputs = with pkgs; [
vulkan-loader
vulkan-headers
ffmpeg
]; ];
shellHook = '' shellHook = ''

View File

@@ -1,57 +0,0 @@
[configuration]
entry_symbol = "example_library_init"
compatibility_minimum = "4.1"
reloadable = false
[libraries]
; Relative paths ensure that our GDExtension can be placed anywhere in the project directory.
macos.single.debug = "./macos/libEXTENSION-NAME.macos.template_debug.dylib"
macos.double.debug = "./macos/libEXTENSION-NAME.macos.template_debug.double.dylib"
macos.single.release = "./macos/libEXTENSION-NAME.macos.template_release.dylib"
macos.double.release = "./macos/libEXTENSION-NAME.macos.template_release.double.dylib"
ios.arm64.single.debug = "./ios/libEXTENSION-NAME.ios.template_debug.arm64.dylib"
ios.arm64.double.debug = "./ios/libEXTENSION-NAME.ios.template_debug.arm64.double.dylib"
ios.arm64.single.release = "./ios/libEXTENSION-NAME.ios.template_release.arm64.dylib"
ios.arm64.double.release = "./ios/libEXTENSION-NAME.ios.template_release.arm64.double.dylib"
windows.x86_32.single.debug = "./windows/EXTENSION-NAME.windows.template_debug.x86_32.dll"
windows.x86_32.double.debug = "./windows/EXTENSION-NAME.windows.template_debug.x86_32.double.dll"
windows.x86_32.single.release = "./windows/EXTENSION-NAME.windows.template_release.x86_32.dll"
windows.x86_32.double.release = "./windows/EXTENSION-NAME.windows.template_release.x86_32.double.dll"
windows.x86_64.single.debug = "./windows/EXTENSION-NAME.windows.template_debug.x86_64.dll"
windows.x86_64.double.debug = "./windows/EXTENSION-NAME.windows.template_debug.x86_64.double.dll"
windows.x86_64.single.release = "./windows/EXTENSION-NAME.windows.template_release.x86_64.dll"
windows.x86_64.double.release = "./windows/EXTENSION-NAME.windows.template_release.x86_64.double.dll"
linux.x86_64.single.debug = "./linux/libEXTENSION-NAME.linux.template_debug.x86_64.so"
linux.x86_64.double.debug = "./linux/libEXTENSION-NAME.linux.template_debug.x86_64.double.so"
linux.x86_64.single.release = "./linux/libEXTENSION-NAME.linux.template_release.x86_64.so"
linux.x86_64.double.release = "./linux/libEXTENSION-NAME.linux.template_release.x86_64.double.so"
linux.arm64.single.debug = "./linux/libEXTENSION-NAME.linux.template_debug.arm64.so"
linux.arm64.double.debug = "./linux/libEXTENSION-NAME.linux.template_debug.arm64.double.so"
linux.arm64.single.release = "./linux/libEXTENSION-NAME.linux.template_release.arm64.so"
linux.arm64.double.release = "./linux/libEXTENSION-NAME.linux.template_release.arm64.double.so"
linux.rv64.single.debug = "./linux/libEXTENSION-NAME.linux.template_debug.rv64.so"
linux.rv64.double.debug = "./linux/libEXTENSION-NAME.linux.template_debug.rv64.double.so"
linux.rv64.single.release = "./linux/libEXTENSION-NAME.linux.template_release.rv64.so"
linux.rv64.double.release = "./linux/libEXTENSION-NAME.linux.template_release.rv64.double.so"
android.x86_64.single.debug = "./android/libEXTENSION-NAME.android.template_debug.x86_64.so"
android.x86_64.double.debug = "./android/libEXTENSION-NAME.android.template_debug.x86_64.double.so"
android.x86_64.single.release = "./android/libEXTENSION-NAME.android.template_release.x86_64.so"
android.x86_64.double.release = "./android/libEXTENSION-NAME.android.template_release.x86_64.double.so"
android.arm64.single.debug = "./android/libEXTENSION-NAME.android.template_debug.arm64.so"
android.arm64.double.debug = "./android/libEXTENSION-NAME.android.template_debug.arm64.double.so"
android.arm64.single.release = "./android/libEXTENSION-NAME.android.template_release.arm64.so"
android.arm64.double.release = "./android/libEXTENSION-NAME.android.template_release.arm64.double.so"
web.wasm32.single.debug = "./web/libEXTENSION-NAME.web.template_debug.wasm32.nothreads.wasm"
web.wasm32.double.debug = "./web/libEXTENSION-NAME.web.template_debug.wasm32.double.nothreads.wasm"
web.wasm32.single.release = "./web/libEXTENSION-NAME.web.template_release.wasm32.nothreads.wasm"
web.wasm32.double.release = "./web/libEXTENSION-NAME.web.template_release.wasm32.double.nothreads.wasm"

View File

@@ -0,0 +1,7 @@
[configuration]
entry_symbol = "gdvapi_library_init"
compatibility_minimum = "4.7"
[libraries]
linux.editor.x86_64 = "res://bin/linux/gdvapi.linux.editor.x86_64.so"
linux.template_debug.x86_64 = "res://bin/linux/gdvapi.linux.template_debug.x86_64.so"

View File

@@ -0,0 +1 @@
uid://vgmwthvwcljf

View File

@@ -1,6 +1,11 @@
extends Node extends Node
func _ready() -> void: func _ready() -> void:
var example := ExampleClass.new() $GDVAPIVideoStream.queue_depth = 4 # default; proves the property binding
example.print_type(example) $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

1
project/example.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://bkwk40ybs8kgm

View File

@@ -1,6 +1,18 @@
[gd_scene load_steps=2 format=3 uid="uid://dh0oj81pufivs"] [gd_scene format=3 uid="uid://dh0oj81pufivs"]
[ext_resource type="Script" path="res://example.gd" id="1_jdh55"] [ext_resource type="Script" uid="uid://bkwk40ybs8kgm" path="res://example.gd" id="1_jdh55"]
[node name="Node" type="Node"] [node name="Node" type="Node" unique_id=2080300232]
script = ExtResource("1_jdh55") script = ExtResource("1_jdh55")
[node name="GDVAPIVideoStream" type="GDVAPIVideoStream" parent="." unique_id=1053894928]
[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

View File

@@ -0,0 +1,49 @@
[runnable_presets]
Linux="Linux"
[preset.0]
name="Linux"
platform="Linux"
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="exports/godot cpp template.x86_64"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.0.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
shader_baker/enabled=false
binary_format/architecture="x86_64"
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="#!/usr/bin/env bash
export DISPLAY=:0
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
\"{temp_dir}/{exe_name}\" {cmd_args}"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
pkill -x -f \"{temp_dir}/{exe_name} {cmd_args}\"
rm -rf \"{temp_dir}\""

View File

@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.cte
compress/mode=0 compress/mode=0
compress/high_quality=false compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
@@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/normal_map_invert_y=false process/normal_map_invert_y=false

View File

@@ -8,8 +8,20 @@
config_version=5 config_version=5
[animation]
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application] [application]
config/name="godot cpp template" config/name="godot cpp template"
config/features=PackedStringArray("4.1", "Forward Plus") run/main_scene="uid://dh0oj81pufivs"
config/features=PackedStringArray("4.7", "Forward Plus")
config/icon="res://icon.svg" config/icon="res://icon.svg"
[display]
display/window/vsync/vsync_mode=0
window/size/viewport_width=1920
window/size/viewport_height=1080

2
server-ffmpeg.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
ffmpeg -re -i $1 -c copy -f mpegts udp://127.0.0.1:5000

5
server-gst.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
gst-launch-1.0 filesrc location=$1 ! qtdemux name=demux \
demux.video_0 ! queue ! h264parse ! mpegtsmux name=mux ! udpsink host=127.0.0.1 port=5000 \
demux.audio_0 ! queue ! aacparse ! mux.

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)

5
server_camera.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
ffmpeg -f v4l2 -input_format mjpeg -video_size 1920x1080 -framerate 30 -i "$1" \
-c:v libx264 -preset ultrafast -tune zerolatency -g 30 -flags +global_header \
-f mpegts udp://127.0.0.1:5000?pkt_size=1316

View File

@@ -1,9 +0,0 @@
#include "example_class.h"
void ExampleClass::_bind_methods() {
godot::ClassDB::bind_method(D_METHOD("print_type", "variant"), &ExampleClass::print_type);
}
void ExampleClass::print_type(const Variant &p_variant) const {
print_line(vformat("Type: %d", p_variant.get_type()));
}

View File

@@ -1,20 +0,0 @@
#pragma once
#include "godot_cpp/classes/ref_counted.hpp"
#include "godot_cpp/classes/wrapped.hpp"
#include "godot_cpp/variant/variant.hpp"
using namespace godot;
class ExampleClass : public RefCounted {
GDCLASS(ExampleClass, RefCounted)
protected:
static void _bind_methods();
public:
ExampleClass() = default;
~ExampleClass() override = default;
void print_type(const Variant &p_variant) const;
};

View File

@@ -0,0 +1,537 @@
#include "gdvapivideostream_class.h"
#include "godot_cpp/classes/project_settings.hpp"
#include "godot_cpp/classes/rd_texture_format.hpp"
#include "godot_cpp/classes/rd_texture_view.hpp"
#include "godot_cpp/classes/rendering_device.hpp"
#include "godot_cpp/classes/rendering_server.hpp"
#include "godot_cpp/variant/color.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" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/hwcontext.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
using namespace godot;
enum HWMode { HW_NONE,
HW_VULKAN,
HW_VAAPI };
static HWMode active_hw_mode = HW_NONE;
void GDVAPIVideoStream::_bind_methods() {
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);
ADD_PROPERTY(PropertyInfo(Variant::INT, "desired_width"), "set_desired_width", "get_desired_width");
ADD_PROPERTY(PropertyInfo(Variant::INT, "desired_height"), "set_desired_height", "get_desired_height");
ClassDB::bind_method(D_METHOD("set_reorder_buffer_ms", "v"), &GDVAPIVideoStream::set_reorder_buffer_ms);
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) {
for (const enum AVPixelFormat *p = formats; *p != -1; p++) {
if (active_hw_mode == HW_VULKAN && *p == AV_PIX_FMT_VULKAN)
return AV_PIX_FMT_VULKAN;
if (active_hw_mode == HW_VAAPI && *p == AV_PIX_FMT_VAAPI)
return AV_PIX_FMT_VAAPI;
}
UtilityFunctions::printerr("[gdvapi] hardware decoding unavailable");
active_hw_mode = HW_NONE;
return AV_PIX_FMT_NONE;
}
void GDVAPIVideoStream::begin(const Variant &source) {
stream_source = source;
}
void GDVAPIVideoStream::_process(double delta) {
(void)delta;
if (!stream_started) {
stream_started = true;
RenderingDevice *rd = RenderingServer::get_singleton()->get_rendering_device();
if (rd) {
uint64_t vk_dev_u = rd->get_driver_resource(
RenderingDevice::DRIVER_RESOURCE_LOGICAL_DEVICE, RID(), 0);
vk_dev = (VkDevice)vk_dev_u;
uint64_t vk_inst_u = rd->get_driver_resource(
RenderingDevice::DRIVER_RESOURCE_VULKAN_INSTANCE, RID(), 0);
vk_inst = (VkInstance)vk_inst_u;
}
wall_base = std::chrono::steady_clock::now();
String uri = stream_source;
if (uri.ends_with(".sdp") || uri.ends_with(".mp4") || uri.ends_with(".mov") || uri.begins_with("/"))
worker_src = uri.utf8().get_data();
else {
const char *sdp = "/tmp/gdvapi_live.sdp";
String host = "127.0.0.1", port = "5000";
if (uri.begins_with("udp://")) {
String rest = uri.substr(6);
int c = rest.find(":");
if (c > 0) {
host = rest.substr(0, c);
port = rest.substr(c + 1);
}
}
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;
}
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() {
cleanup();
}
void GDVAPIVideoStream::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_EXIT_TREE: {
cleanup();
} break;
}
}
void GDVAPIVideoStream::cleanup() {
set_process(false);
stop_worker();
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();
}
void GDVAPIVideoStream::set_desired_width(const int v) {
if (desiredWidth != v) {
desiredWidth = v;
}
}
void GDVAPIVideoStream::set_desired_height(const int v) {
if (desiredHeight != v) {
desiredHeight = v;
}
}
int GDVAPIVideoStream::get_desired_width() const {
return desiredWidth;
}
int GDVAPIVideoStream::get_desired_height() const {
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() {
UtilityFunctions::print("_ready called");
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

@@ -0,0 +1,130 @@
#pragma once
#include "godot_cpp/classes/node.hpp"
#include "godot_cpp/classes/texture2drd.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" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
namespace godot {
class GDVAPIVideoStream : public Node {
GDCLASS(GDVAPIVideoStream, Node)
private:
struct ReadyFrame {
int fd = -1;
int w = 0, h = 0;
double pts_sec = 0.0;
uint32_t drm_fourcc = 0;
int slot = -1;
};
std::thread worker_thr;
std::mutex q_mu;
std::condition_variable q_cv;
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
gdvapi::VAAPIDecoder va_decoder;
gdvapi::DecoderOptions decoder_opts;
int vpp_slot = 0; // rotates 0..kRgbaPool-1
std::string worker_src;
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;
int desiredWidth = 256;
int desiredHeight = 256;
void worker_loop();
void stop_worker();
void consume_ready();
void cleanup();
void texture_init();
protected:
static void _bind_methods();
public:
GDVAPIVideoStream() = default;
~GDVAPIVideoStream() override;
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 _notification(int p_what);
void _ready() override;
void set_desired_width(const int v);
int get_desired_width() const;
void set_desired_height(const int v);
int get_desired_height() const;
};
} //namespace godot

View File

@@ -1,37 +1,38 @@
#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>
#include <godot_cpp/core/defs.hpp> #include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp> #include <godot_cpp/godot.hpp>
#include "example_class.h" #include "gdvapivideostream_class.h"
using namespace godot; using namespace godot;
void initialize_gdextension_types(ModuleInitializationLevel p_level) void initialize_gdextension_types(ModuleInitializationLevel p_level) {
{ fprintf(stderr, "[gdvapi] initialize level=%d\n", (int)p_level);
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { if (p_level == MODULE_INITIALIZATION_LEVEL_CORE)
gdvapi::install_vk_create_device_hook();
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE)
return; return;
}
GDREGISTER_CLASS(ExampleClass); 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" {
{ GDExtensionBool GDE_EXPORT gdvapi_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
// Initialization
GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
{
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)