Compare commits
17 Commits
7c3ab21127
...
zerocopy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a474963e5 | ||
|
|
436808a0d0 | ||
|
|
97ff779911 | ||
|
|
dacbb2e043 | ||
|
|
27693a8fbf | ||
|
|
427c64ef5d | ||
|
|
52662e129b | ||
|
|
8283d9632c | ||
|
|
c4641182e3 | ||
|
|
e33d564844 | ||
|
|
b90dcde305 | ||
|
|
2ec0c4b742 | ||
|
|
bb3b897c16 | ||
|
|
7747217b36 | ||
|
|
5f9cc1e42e | ||
|
|
c5d45ad8ab | ||
|
|
5f5a14f69f |
44
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
44
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
14
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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.
|
||||
88
.github/workflows/ci.yml
vendored
88
.github/workflows/ci.yml
vendored
@@ -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 }}
|
||||
104
.github/workflows/make_build.yml
vendored
104
.github/workflows/make_build.yml
vendored
@@ -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
|
||||
26
.gitignore
vendored
26
.gitignore
vendored
@@ -3,20 +3,10 @@
|
||||
.godot/
|
||||
|
||||
# Ignore library files but not the gdextension file
|
||||
project/bin/*
|
||||
!project/bin/android
|
||||
project/bin/android/*
|
||||
!project/bin/android/.gitkeep
|
||||
!project/bin/linux
|
||||
project/bin/linux/*
|
||||
!project/bin/linux/.gitkeep
|
||||
!project/bin/macos
|
||||
project/bin/macos/*
|
||||
!project/bin/macos/.gitkeep
|
||||
!project/bin/windows
|
||||
project/bin/windows/*
|
||||
!project/bin/windows/.gitkeep
|
||||
/bin/**
|
||||
!project/bin/*.gdextension
|
||||
|
||||
# Ignore scons database
|
||||
.sconsign*.dblite
|
||||
|
||||
# Ignore custom.py
|
||||
@@ -60,3 +50,13 @@ compile_commands.json
|
||||
# Cmake
|
||||
cmake-build*
|
||||
CMakeUserPresets.json
|
||||
|
||||
# stupid godot developers using SCons
|
||||
build/
|
||||
|
||||
godot/
|
||||
build-editor/
|
||||
|
||||
# Some testing files
|
||||
project/*.mp4
|
||||
project/*.mov
|
||||
|
||||
@@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.17)
|
||||
if(CMAKE_C_COMPILER)
|
||||
endif()
|
||||
|
||||
set(LIBNAME "EXTENSION-NAME" CACHE STRING "The name of the library")
|
||||
set(LIBNAME "gdvapi") # "The name of the library"
|
||||
set(GODOT_PROJECT_DIR "project" CACHE STRING "The directory of a Godot project folder")
|
||||
|
||||
# Make sure all the dependencies are satisfied
|
||||
@@ -26,6 +26,12 @@ if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/godot-cpp/src")
|
||||
endif()
|
||||
add_subdirectory(godot-cpp SYSTEM)
|
||||
|
||||
# When compiling for emscripten, this toolchainhack needs to be included.
|
||||
# set(CMAKE_PROJECT_<project-name>_INCLUDE ${godot-cpp_SOURCE_DIR}/cmake/emsdkHack.cmake)
|
||||
|
||||
# Create the gen directory at configure time so IDEs can see generated headers immediately after generate_bindings
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/godot-cpp/gen/include")
|
||||
|
||||
# Add godot-cpp's module path and include the exported functions.
|
||||
# This is made available for documentation generation
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${godot-cpp_SOURCE_DIR}/cmake")
|
||||
@@ -36,21 +42,44 @@ get_target_property(GODOTCPP_SUFFIX godot::cpp GODOTCPP_SUFFIX)
|
||||
get_target_property(GODOTCPP_PLATFORM godot::cpp GODOTCPP_PLATFORM)
|
||||
|
||||
# Now we can specify our own project which will inherit any global cmake properties or variables that have been defined.
|
||||
project(godot-cpp-template
|
||||
project(gdvapi
|
||||
VERSION 1.0
|
||||
DESCRIPTION "This repository serves as a quickstart template for GDExtension development with Godot 4.0+."
|
||||
HOMEPAGE_URL "https://github.com/enetheru/godot-cpp-template/tree/main"
|
||||
DESCRIPTION "Godot extension to recieve streams"
|
||||
HOMEPAGE_URL "https://git.epsncl.uk/rhys/gdvapi"
|
||||
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)
|
||||
|
||||
target_link_libraries(${LIBNAME} PRIVATE
|
||||
Vulkan::Headers
|
||||
PkgConfig::AVFORMAT
|
||||
PkgConfig::AVCODEC
|
||||
PkgConfig::SWSCALE
|
||||
PkgConfig::AVUTIL
|
||||
)
|
||||
|
||||
target_sources(${LIBNAME}
|
||||
PRIVATE
|
||||
src/register_types.cpp
|
||||
src/register_types.h
|
||||
src/example_class.cpp
|
||||
src/example_class.h
|
||||
src/gdvapivideostream_class.cpp
|
||||
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
|
||||
@@ -59,7 +88,7 @@ file(GLOB_RECURSE DOC_XML LIST_DIRECTORIES NO CONFIGURE_DEPENDS "${PROJECT_SOURC
|
||||
# conditionally add doc data to compile output
|
||||
if(DOC_XML)
|
||||
if(GODOTCPP_TARGET MATCHES "editor|template_debug")
|
||||
target_doc_sources(${LIBNAME} ${DOC_XML})
|
||||
target_doc_sources(${LIBNAME} "${DOC_XML}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
30
LICENSE.md
30
LICENSE.md
@@ -1,24 +1,12 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
Copyright (C) 2026 Rhys Tumelty <rhys@tumelty.co.uk>
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
|
||||
62
README.md
62
README.md
@@ -1,56 +1,8 @@
|
||||
# godot-cpp template
|
||||
This repository serves as a quickstart template for GDExtension development with Godot 4.0+.
|
||||
# GDVAPI
|
||||
A godot extension to recieve video steams over UDP and render them
|
||||
|
||||
## Contents
|
||||
* Preconfigured source files for C++ development of the GDExtension ([src/](./src/))
|
||||
* An empty Godot project in [project/](./project), to test the GDExtension
|
||||
* godot-cpp as a submodule (`godot-cpp/`)
|
||||
* GitHub Issues template ([.github/ISSUE_TEMPLATE.yml](./.github/ISSUE_TEMPLATE.yml))
|
||||
* GitHub CI/CD workflows to publish your library packages when creating a release ([.github/workflows/builds.yml](./.github/workflows/builds.yml))
|
||||
* An SConstruct file with various functions, such as boilerplate for [Adding documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/cpp/gdextension_docs_system.html)
|
||||
|
||||
## Usage - Template
|
||||
|
||||
To use this template, log in to GitHub and click the green "Use this template" button at the top of the repository page. This will let you create a copy of this repository with a clean git history.
|
||||
|
||||
To get started with your new GDExtension, do the following:
|
||||
|
||||
* clone your repository to your local computer
|
||||
* initialize the godot-cpp git submodule via `git submodule update --init`
|
||||
* change the name of the compiled library file inside the [SConstruct](./SConstruct) file by modifying the `libname` string.
|
||||
* change the paths of the to be loaded library name inside the [project/bin/example.gdextension](./project/bin/example.gdextension) file, by replacing `EXTENSION-NAME` with the name you chose for `libname`.
|
||||
* change the `entry_symbol` string inside [project/bin/example.gdextension](./project/bin/example.gdextension) file.
|
||||
* rename the `example_library_init` function in [src/register_types.cpp](./src/register_types.cpp) to the same name you chose for `entry_symbol`.
|
||||
* change the name of the `project/bin/example.gdextension` file
|
||||
|
||||
Now, you can build the project with the following command:
|
||||
|
||||
```shell
|
||||
scons
|
||||
```
|
||||
|
||||
If the build command worked, you can test it with the [project](./project) project. Import it into Godot, open it, and launch the main scene. You should see it print the following line in the console:
|
||||
|
||||
```
|
||||
Type: 24
|
||||
```
|
||||
|
||||
### Configuring an IDE
|
||||
You can develop your own extension with any text editor and by invoking scons on the command line, but if you want to work with an IDE (Integrated Development Environment), you can use a compilation database file called `compile_commands.json`. Most IDEs should automatically identify this file, and self-configure appropriately.
|
||||
To generate the database file, you can run one of the following commands in the project root directory:
|
||||
```shell
|
||||
# Generate compile_commands.json while compiling
|
||||
scons compiledb=yes
|
||||
|
||||
# Generate compile_commands.json without compiling
|
||||
scons compiledb=yes compile_commands.json
|
||||
```
|
||||
|
||||
## Usage - Actions
|
||||
|
||||
This repository comes with continuous integration (CI) through a GitHub action that tests building the GDExtension.
|
||||
It triggers automatically for each pushed change. You can find and edit it in [builds.yml](.github/workflows/ci.yml).
|
||||
|
||||
There is also a workflow ([make_build.yml](.github/workflows/make_build.yml)) that builds the GDExtension for all supported platforms that you can use to create releases.
|
||||
You can trigger this workflow manually from the `Actions` tab on GitHub.
|
||||
After it is complete, you can find the file `godot-cpp-template.zip` in the `Artifacts` section of the workflow run.
|
||||
# Building
|
||||
`nix develop`
|
||||
`cmake -B build`
|
||||
`make -C build -j$(nproc)`
|
||||
`# package coming later`
|
||||
|
||||
64
SConstruct
64
SConstruct
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
from methods import print_error
|
||||
|
||||
|
||||
libname = "EXTENSION-NAME"
|
||||
projectdir = "project"
|
||||
|
||||
localEnv = Environment(tools=["default"], PLATFORM="")
|
||||
|
||||
# Build profiles can be used to decrease compile times.
|
||||
# You can either specify "disabled_classes", OR
|
||||
# explicitly specify "enabled_classes" which disables all other classes.
|
||||
# Modify the example file as needed and uncomment the line below or
|
||||
# manually specify the build_profile parameter when running SCons.
|
||||
|
||||
# localEnv["build_profile"] = "build_profile.json"
|
||||
|
||||
customs = ["custom.py"]
|
||||
customs = [os.path.abspath(path) for path in customs]
|
||||
|
||||
opts = Variables(customs, ARGUMENTS)
|
||||
opts.Update(localEnv)
|
||||
|
||||
Help(opts.GenerateHelpText(localEnv))
|
||||
|
||||
env = localEnv.Clone()
|
||||
|
||||
if not (os.path.isdir("godot-cpp") and os.listdir("godot-cpp")):
|
||||
print_error("""godot-cpp is not available within this folder, as Git submodules haven't been initialized.
|
||||
Run the following command to download godot-cpp:
|
||||
|
||||
git submodule update --init --recursive""")
|
||||
sys.exit(1)
|
||||
|
||||
env = SConscript("godot-cpp/SConstruct", {"env": env, "customs": customs})
|
||||
|
||||
env.Append(CPPPATH=["src/"])
|
||||
sources = Glob("src/*.cpp")
|
||||
|
||||
if env["target"] in ["editor", "template_debug"]:
|
||||
try:
|
||||
doc_data = env.GodotCPPDocData("src/gen/doc_data.gen.cpp", source=Glob("doc_classes/*.xml"))
|
||||
sources.append(doc_data)
|
||||
except AttributeError:
|
||||
print("Not including class reference as we're targeting a pre-4.3 baseline.")
|
||||
|
||||
# .dev doesn't inhibit compatibility, so we don't need to key it.
|
||||
# .universal just means "compatible with all relevant arches" so we don't need to key it.
|
||||
suffix = env['suffix'].replace(".dev", "").replace(".universal", "")
|
||||
|
||||
lib_filename = "{}{}{}{}".format(env.subst('$SHLIBPREFIX'), libname, suffix, env.subst('$SHLIBSUFFIX'))
|
||||
|
||||
library = env.SharedLibrary(
|
||||
"bin/{}/{}".format(env['platform'], lib_filename),
|
||||
source=sources,
|
||||
)
|
||||
|
||||
copy = env.Install("{}/bin/{}/".format(projectdir, env["platform"]), library)
|
||||
|
||||
default_args = [library, copy]
|
||||
Default(*default_args)
|
||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1787360063,
|
||||
"narHash": "sha256-dt4WdcvsA8/RCe+VZZwqU0X+XMM3wBbGCWA0/sFWzGo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2c423e03bbafcff28bfadc6781a4a8257f205cb5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
37
flake.nix
Normal file
37
flake.nix
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = inputs: {
|
||||
|
||||
devShells = builtins.mapAttrs (system: pkgs: {
|
||||
default = (pkgs.mkShell.override { stdenv = pkgs.llvmPackages.stdenv; }) {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
ninja
|
||||
clang
|
||||
python3
|
||||
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 = ''
|
||||
export PS1="''${PS1//\\n/}"
|
||||
export PS1="\[\033[1;31m\][gdvapi]\[\033[0m\] $PS1"
|
||||
'';
|
||||
};
|
||||
}) inputs.nixpkgs.legacyPackages;
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
7
project/bin/gdvapi.gdextension
Normal file
7
project/bin/gdvapi.gdextension
Normal 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"
|
||||
1
project/bin/gdvapi.gdextension.uid
Normal file
1
project/bin/gdvapi.gdextension.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://vgmwthvwcljf
|
||||
@@ -1,6 +1,11 @@
|
||||
extends Node
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var example := ExampleClass.new()
|
||||
example.print_type(example)
|
||||
$GDVAPIVideoStream.queue_depth = 4 # default; proves the property binding
|
||||
$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
1
project/example.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bkwk40ybs8kgm
|
||||
@@ -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")
|
||||
|
||||
[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
|
||||
49
project/export_presets.cfg
Normal file
49
project/export_presets.cfg
Normal 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}\""
|
||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.cte
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -8,8 +8,20 @@
|
||||
|
||||
config_version=5
|
||||
|
||||
[animation]
|
||||
|
||||
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||
|
||||
[application]
|
||||
|
||||
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"
|
||||
|
||||
[display]
|
||||
|
||||
display/window/vsync/vsync_mode=0
|
||||
|
||||
window/size/viewport_width=1920
|
||||
window/size/viewport_height=1080
|
||||
|
||||
2
server-ffmpeg.sh
Executable file
2
server-ffmpeg.sh
Executable 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
5
server-gst.sh
Normal 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
32
server-test4k.sh
Executable 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
5
server_camera.sh
Executable 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
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
537
src/gdvapivideostream_class.cpp
Normal file
537
src/gdvapivideostream_class.cpp
Normal 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");
|
||||
}
|
||||
130
src/gdvapivideostream_class.h
Normal file
130
src/gdvapivideostream_class.h
Normal 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
|
||||
@@ -1,38 +1,39 @@
|
||||
#include "register_types.h"
|
||||
#include "vulkan_detour.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include <gdextension_interface.h>
|
||||
#include <godot_cpp/core/class_db.hpp>
|
||||
#include <godot_cpp/core/defs.hpp>
|
||||
#include <godot_cpp/godot.hpp>
|
||||
|
||||
#include "example_class.h"
|
||||
#include "gdvapivideostream_class.h"
|
||||
|
||||
using namespace godot;
|
||||
|
||||
void initialize_gdextension_types(ModuleInitializationLevel p_level)
|
||||
{
|
||||
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
|
||||
void initialize_gdextension_types(ModuleInitializationLevel p_level) {
|
||||
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;
|
||||
}
|
||||
GDREGISTER_CLASS(ExampleClass);
|
||||
|
||||
GDREGISTER_RUNTIME_CLASS(GDVAPIVideoStream);
|
||||
}
|
||||
|
||||
void uninitialize_gdextension_types(ModuleInitializationLevel p_level) {
|
||||
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
|
||||
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
{
|
||||
// Initialization
|
||||
GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
|
||||
{
|
||||
extern "C" {
|
||||
GDExtensionBool GDE_EXPORT gdvapi_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);
|
||||
init_obj.register_initializer(initialize_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
449
src/vaapi_decoder.cpp
Normal file
449
src/vaapi_decoder.cpp
Normal 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(¶m, 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, ¶m, &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
90
src/vaapi_decoder.h
Normal 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
226
src/vk_image_import.cpp
Normal 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
26
src/vk_image_import.h
Normal 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
192
src/vulkan_detour.cpp
Normal 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
10
src/vulkan_detour.h
Normal 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
36
stream-gst.py
Normal 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)
|
||||
Reference in New Issue
Block a user